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:
- The client connects over HTTPS
- The server presents its certificate
- The client presents its certificate
- 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 certificaterejectUnauthorized: 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 nameissuer.CN– issuing CAvalid_from/valid_tofingerprint256
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
rejectUnauthorizedtofalsein 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
Post a Comment