Mutual TLS and operations
Require client authentication
Configure the local server to request and verify certificates from the lab CA.
In mutual TLS the client verifies the server, and the server verifies the client’s certificate chain too. The server needs three things for that: ask for a certificate, refuse connections that fail verification, and know which CA to verify against.
In our Node server that’s three new options:
https.createServer({
key: fs.readFileSync('app.key'),
cert: fs.readFileSync('app-fullchain.crt'),
requestCert: true,
rejectUnauthorized: true,
ca: fs.readFileSync('lab-ca.crt'),
}, (request, response) => {
response.end(`hello ${request.socket.getPeerCertificate().subject.CN}\n`)
}).listen(8443, '127.0.0.1')
requestCert: true makes the server ask every client for a certificate during the handshake. rejectUnauthorized: true drops clients whose certificate doesn’t verify. ca is the trust anchor for that check: only certificates chaining to our lab CA pass.
The handler shows the payoff. getPeerCertificate() returns the verified client certificate, so the application can read lab-client straight from the socket. No session, no token lookup.
Now let’s test both sides of the door. First without a client certificate:
curl --cacert lab-ca.crt --resolve app.lab.test:8443:127.0.0.1 https://app.lab.test:8443/
# curl: (56) LibreSSL SSL_read: ... error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0
The request fails, but not with an HTTP 401. It fails at the TLS layer. The 1116 hides TLS alert 116, certificate required. openssl s_client spells it out as tlsv13 alert certificate required if you send a request through it. An unauthenticated client never reaches the application code.
Then connect with the client credentials from the previous lesson:
curl --cacert lab-ca.crt --cert client.crt --key client.key --resolve app.lab.test:8443:127.0.0.1 https://app.lab.test:8443/
# hello lab-client
Same server, same URL. One connection rejected during the handshake, one greeted by name. Record both results, because this pair is your proof that mTLS is enforced.
A failure worth recognizing: everything is configured, but the client certificate was signed by a different CA than the one in the server’s ca option. I tried it with a self-signed client certificate. curl reports Connection reset by peer, which looks like a network problem and isn’t. When mTLS “doesn’t work”, run openssl verify -CAfile lab-ca.crt client.crt first. If that fails, no server code will fix it.
One boundary to keep sharp: authentication is not authorization. A valid client certificate proves who is connecting. The application still decides what that identity may do. Map the verified name to narrow permissions, exactly as you would with any logged-in user.
Lesson completed