Nginx mTLS Mutual Auth in Practice: Client Certificate Verification
Nginx mTLS mutual authentication tutorial: private CA, client cert issuance, ssl_verify_client enforcement, full OpenSSL commands, curl verification, and certificate revocation.
Scope
Nginx mTLS (mutual TLS) fits cases that need strong caller identity:
- Internal admin consoles and ops platforms limited to office endpoints that hold a client certificate;
- Open APIs served only to a few trusted partners, with a certificate layer on top of passwords;
- Service-to-service traffic that requires mutual identity so a leaked URL cannot be called by anyone;
- Zero-trust setups that replace source-IP allowlists with certificates, avoiding dynamic-IP maintenance.
Prerequisites
- Nginx installed with working HTTPS (one-way TLS already healthy);
- OpenSSL on the server (CentOS/RHEL:
yum install openssl; Debian/Ubuntu:apt install openssl); - curl on the client test host (7.53+ for PEM via cert args);
- For browser users over the public internet, install the CA cert into the client trust store.
How it works
Plain HTTPS verifies only the server. mTLS adds a step in the TLS handshake: the server sends CertificateRequest; the client must present a cert issued by a CA the server trusts; both sides validate the peer chain before the connection is up. Nginx enables this with ssl_verify_client and ssl_client_certificate for the trusted CA. An attacker who has the URL still cannot finish the handshake without the client cert and private key. Identity moves from a guessable password to an unforgeable asymmetric key pair.
Steps
1. Create a private CA
mkdir -p /etc/nginx/mtls && cd /etc/nginx/mtls
# Generate CA private key (4096-bit)
openssl genrsa -out ca.key 4096
# Generate CA certificate, valid for 10 years
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt -subj "/CN=Internal CA"
2. Issue the server certificate
# server private key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN=helixcyber.ai"
# Sign with SAN extension (browser verification depends on SAN, no longer trusts CN)
cat > server.ext client.ext
Client certificate extendedKeyUsage must be clientAuth; otherwise Nginx verification fails.
4. Configure Nginx
server {
listen 443 ssl;
server_name helixcyber.ai;
ssl_certificate /etc/nginx/mtls/server.crt;
ssl_certificate_key /etc/nginx/mtls/server.key;
# Enable mutual authentication: on means client certificate is mandatory
ssl_client_certificate /etc/nginx/mtls/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
ssl_protocols TLSv1.2 TLSv1.3;
# Pass client certificate information through to the backend application (optional)
proxy_set_header X-Client-DN $ssl_client_s_dn;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
ssl_verify_client on is mandatory mode. To allow cert-less read-only visitors, set optional and decide in the app with $ssl_client_verify. After edits: nginx -t && systemctl reload nginx.
Verify
# Access with client certificate: should return 200 and backend content
curl --cacert ca.crt --cert client.crt --key client.key \
https://helixcyber.ai/
# Access without certificate: should return 400
curl --cacert ca.crt https://helixcyber.ai/
No-cert requests get 400 No required SSL certificate was sent—expected deny. Confirm Nginx error.log has no client SSL certificate verify error.
FAQ
FAQ 1: Client cert check returns 400 "No required SSL certificate was sent"?
Check in order: client actually sent cert and key; extendedKeyUsage is clientAuth; no CDN/cloud WAF stripped TLS—mTLS needs Nginx to terminate TLS. L4 passthrough (CDN TCP mode) is required to preserve the client cert.
FAQ 2: How to revoke one client cert?
Private CA via CRL: openssl ca -gencrl to produce crl.pem, set ssl_crl /etc/nginx/mtls/crl.pem in Nginx, then reload. Keep client cert lifetime ≤ 1 year and run a rotation process.
Summary
mTLS adds client-cert checks to the TLS handshake and upgrades one-way server auth to mutual strong auth. Cheapest effective access control for internal systems and partner APIs. Deploy checklist: private CA owns cert lifetime, server cert must include SAN, client cert extendedKeyUsage is clientAuth, Nginx terminates TLS. After go-live, ssl_verify_client optional plus $ssl_client_verify supports a gray rollout so a hard cutover does not break traffic.