← News
/PLAYBOOK · 2026-09-03

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:

Prerequisites

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.