Using Mutual TLS (mTLS) in Next.js (Server-Side Only)

Using Mutual TLS (mTLS) in Next.js (Server-Side Only)



In the previous posts, we covered:

Now we focus on Next.js applications and how mTLS works depending on deployment.


Next.js Cannot Access TLS Handshake Directly

  • Next.js middleware and API routes run after the TLS handshake
  • They cannot see client certificates or verify them
  • Next.js built-in server does not expose Node's HTTPS options like requestCert

In short: Next.js middleware cannot enforce mTLS or access TLS handshake details. Any enforcement must happen before the request reaches Next.js.


Next.js as an mTLS Client (Server-Side API Calls)

Next.js can securely call mTLS-protected APIs from server-side code, such as:

  • API routes
  • Server actions
import fs from 'fs';
import https from 'https';
import axios from 'axios';

export async function GET(req) {
  const httpsAgent = new https.Agent({
    key: fs.readFileSync('/certs/client.key'),
    cert: fs.readFileSync('/certs/client.crt'),
    ca: fs.readFileSync('/certs/ca.pem'),
    rejectUnauthorized: true,
  });

  try {
    const response = await axios.get('https://internal-api.example.com', {
      httpsAgent,
    });
    return new Response(JSON.stringify(response.data), { status: 200 });
  } catch (err) {
    console.error(err);
    return new Response('mTLS request failed', { status: 500 });
  }
}

This works both on Vercel and self-hosted deployments because it happens server-side.


Next.js as an mTLS Server

Next.js cannot enforce mTLS directly. How you handle mTLS depends on your deployment:

1. Vercel Deployment

  • You cannot run a custom HTTPS server on Vercel.
  • Next.js middleware and API routes cannot access TLS handshake or client certs.
  • Solution: Terminate mTLS at a reverse proxy or API gateway in front of Vercel.
  • The proxy validates client certificates, then forwards requests to Vercel.
  • Inject client info into headers (e.g., X-Client-CN) so Next.js can read it.

Example Nginx snippet:

server {
  listen 443 ssl;
  ssl_certificate /etc/certs/server.crt;
  ssl_certificate_key /etc/certs/server.key;
  ssl_client_certificate /etc/certs/ca.pem;
  ssl_verify_client on;

  location / {
    proxy_pass https://my-vercel-app.vercel.app;
    proxy_set_header X-Client-CN $ssl_client_s_dn;
  }
}

Next.js API route reading the header:

export async function GET(req) {
  const clientCN = req.headers.get('x-client-cn');
  return new Response(`Hello ${clientCN}`, { status: 200 });
}
---

2. Self-Hosted Deployment (Docker, VPS, Kubernetes, OpenShift)

  • You can run a custom Node HTTPS server wrapping Next.js.
  • Use requestCert: true and rejectUnauthorized: true to enforce client certs.
  • Forward requests to app.getRequestHandler().

Example:

import https from 'https';
import fs from 'fs';
import next from 'next';

const app = next({ dev: true });
const handle = app.getRequestHandler();

const server = https.createServer(
  {
    key: fs.readFileSync('/certs/server.key'),
    cert: fs.readFileSync('/certs/server.crt'),
    ca: fs.readFileSync('/certs/ca.pem'),
    requestCert: true,
    rejectUnauthorized: true,
  },
  (req, res) => handle(req, res)
);

server.listen(8443, () => console.log('Next.js mTLS server running on port 8443'));

This is only possible for **self-hosted deployments**. Platforms like Vercel do not allow custom HTTPS servers.


Summary: Vercel vs Self-Hosted

  • Vercel: mTLS must be terminated upstream (reverse proxy/API gateway). Next.js reads client info from headers.
  • Self-hosted: You can wrap Next.js in a custom HTTPS server and enforce client certificates directly.
  • Next.js middleware cannot enforce mTLS or access TLS handshake details in any deployment.
  • Server-side outgoing requests (mTLS client) work in both environments.

Using these patterns ensures your Next.js app participates in enterprise mTLS workflows securely, without exposing sensitive certificates.

❤️ Support This Blog


If this post helped you, you can support my writing with a small donation. Thank you for reading.


Comments

Popular posts from this blog

fixed: embedded-redis: Unable to run on macOS Sonoma

Copying MDC Context Map in Web Clients: A Comprehensive Guide

Reset user password for your own Ghost blog