Enabling Mutual TLS (mTLS) in a Node.js Server

 

Enabling Mutual TLS (mTLS) in a Node.js Server



In the previous post, we covered how to call an mTLS-protected API from Node.js. In this guide, we flip the perspective and configure Node.js as the server that requires client certificates.

This setup is common for:

  • Internal enterprise APIs
  • Service-to-service communication
  • Zero-trust architectures
  • OpenShift / Kubernetes workloads

By the end of this post, your Node.js server will:

  • Accept only TLS connections
  • Require valid client certificates
  • Reject unauthorized clients automatically
  • Expose client identity information to your app

How Mutual TLS Works (Server Perspective)

When mTLS is enabled:

  1. The client connects over HTTPS
  2. The server presents its certificate
  3. The client presents its certificate
  4. Both sides verify each other against trusted CAs

Node.js can enforce all of this at the TLS layer, before your application code even runs.


Required Certificate Files

A Node.js mTLS server typically needs:

  • server.key – the server private key
  • server.crt – the server certificate
  • ca.pem – trusted CA(s) used to verify clients

⚠️ The ca.pem file must contain the CA that issued client certificates, not the server certificate itself.


Creating an HTTPS Server with Client Authentication

Use Node’s built-in https module:

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

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) => {
    res.writeHead(200);
    res.end('mTLS connection successful');
  }
);

server.listen(8443, () => {
  console.log('mTLS server listening on port 8443');
});

With these options:

  • requestCert: true – the client must send a certificate
  • rejectUnauthorized: true – invalid clients are rejected automatically

If the client certificate is missing or untrusted, the TLS handshake fails and your handler is never executed.


Accessing Client Certificate Information

Once the TLS handshake succeeds, you can inspect the client certificate:

const cert = req.socket.getPeerCertificate();

console.log(cert);

Common fields include:

  • subject.CN – client common name
  • issuer.CN – issuing CA
  • valid_from / valid_to
  • fingerprint256

This information is often used to:

  • Identify calling services
  • Implement allowlists
  • Map certificates to roles or tenants

Using mTLS with Express

Express runs on top of the HTTPS server. You still configure TLS at the Node level:

import https from 'https';
import fs from 'fs';
import express from 'express';

const app = express();

app.get('/health', (req, res) => {
  res.json({
    client: req.socket.getPeerCertificate().subject,
  });
});

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,
  },
  app
);

server.listen(8443);

Express middleware is only executed after the TLS handshake succeeds.


Common mTLS Server Errors

Client certificate required

If the client does not present a certificate:

tlsv13 alert certificate required

This is expected behavior.

Unknown CA

If the client certificate is signed by an untrusted CA:

unable to get local issuer certificate

Ensure the correct client-issuing CA is in ca.pem.


Security Best Practices

  • Never set rejectUnauthorized to false in production
  • Rotate certificates regularly
  • Use short-lived client certs when possible
  • Store certs in Kubernetes / OpenShift secrets
  • Do not log private keys or full certificates

When to Use mTLS

mTLS is ideal when:

  • Both client and server are controlled systems
  • You need strong service identity
  • API keys or OAuth are insufficient
  • Regulatory or internal security policies require it

It is not suitable for public browser-based clients.


What’s Next

In the next post, we’ll look at:

  • Using mutual TLS in Next.js
  • Why mTLS only works server-side
  • API routes and App Router considerations
  • Deployment patterns for enterprise environments

Mutual TLS is a powerful tool when used correctly. Once configured, it eliminates entire classes of authentication bugs.

❤️ 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