In normal HTTPS only the server proves its identity. With mutual TLS (mTLS) the client must present a certificate too, so only devices you issued a certificate to can even reach the application. It is a strong extra layer for admin panels, internal tools and server-to-server APIs. This guide sets up mutual TLS on Nginx.
Before you start
- A Linux server with Nginx and a normal server certificate (for example from Let's Encrypt).
- OpenSSL installed.
- Keep a way in that doesn't depend on mTLS (SSH) while testing.
Create a private certificate authority
Client certificates are issued by your own CA, not a public one. Work in a protected directory such as /root/mtls-ca:
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 -subj "/CN=Example Internal CA" -out ca.crtKeep ca.key secret and offline if possible – whoever has it can issue valid client certificates.
Issue a client certificate
openssl ecparam -name prime256v1 -genkey -noout -out alice.key
openssl req -new -key alice.key -subj "/CN=alice" -out alice.csr
openssl x509 -req -in alice.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -sha256 -out alice.crt
openssl pkcs12 -export -inkey alice.key -in alice.crt -certfile ca.crt -out alice.p12Give alice.p12 (protected with the export password) to the user to import into their browser or OS keychain.
Require client certificates in Nginx
- Copy
ca.crtto/etc/nginx/mtls/ca.crt. - In the HTTPS server block:
ssl_client_certificate /etc/nginx/mtls/ca.crt; ssl_verify_client on; ssl_verify_depth 2; - To protect only one area, use
ssl_verify_client optional;at server level and check in a location:location /admin/ { if ($ssl_client_verify != SUCCESS) { return 403; } proxy_pass http://127.0.0.1:8080; proxy_set_header X-Client-CN $ssl_client_s_dn; } - Test and reload:
sudo nginx -t && sudo systemctl reload nginx.
The app can read X-Client-CN to know which certificate holder connected (only trust it from the proxy).
Test it
- Without a certificate:
curl -I https://admin.example.com→ 400 or 403. - With one:
curl -I --cert alice.crt --key alice.key https://admin.example.com→ 200.
Revoke access
- Maintain a CRL with an OpenSSL CA configuration (
openssl ca -revokeandopenssl ca -gencrl), or simply rotate the whole CA for small setups. - Point Nginx at the CRL:
ssl_crl /etc/nginx/mtls/ca.crl;and reload. - Regenerate the CRL before its next-update date, or Nginx rejects all clients.
Troubleshooting
- "400 No required SSL certificate was sent": the browser has no certificate from your CA installed, or didn't offer it – restart the browser after import.
- "The SSL certificate error": the client certificate wasn't signed by the CA Nginx trusts, or has expired.
- Behind Cloudflare or another proxy: the proxy terminates TLS, so client certificates don't reach Nginx. Use the proxy's own mTLS feature instead.
FAQ: mutual TLS
Does mTLS replace passwords?
It's best as an extra layer: certificate plus normal login. Certificates can be stolen with the device.
Can I use mTLS for APIs between servers?
Yes – it's a common way to authenticate services to each other without shared secrets in headers.
Why use my own CA instead of a public one?
Public CAs would let anyone with any public certificate in; your own CA means only certificates you issued are accepted.
