When implementing mutual TLS (mTLS) with Nginx, the certificate verification flow follows this sequence:
- Client presents its certificate chain during TLS handshake
- Server (Nginx) validates against its trusted CA store
- Server optionally sends its CA certificates back to client
ssl_client_certificate /path/to/ca_cert.pem; # Sent to clients
ssl_trusted_certificate /path/to/ca_cert.pem; # Not sent to clients
The key distinction lies in whether Nginx includes these certificates in the TLS handshake response. This behavior has practical implications:
- Bandwidth efficiency: Large CA bundles increase handshake size
- Security posture: Revealing internal CA structure may provide reconnaissance info
- OCSP stapling: Both directives affect stapling verification
When ssl_verify_client on;
is set, you must provide ssl_client_certificate
. The ssl_trusted_certificate
alone won't suffice:
# Minimum working configuration for client cert verification
ssl_verify_client on;
ssl_client_certificate /etc/nginx/client_cas.pem;
ssl_trusted_certificate /etc/nginx/trusted_cas.pem; # Optional for OCSP
For production deployments, consider these optimizations:
# Trim unnecessary intermediates from client-facing bundle
openssl crl2pkcs7 -nocrl -certfile full_bundle.pem | \
openssl pkcs7 -print_certs -out trimmed_bundle.pem
# Sample Nginx config for optimized deployment
ssl_client_certificate /etc/nginx/trimmed_client_cas.pem; # 3KB
ssl_trusted_certificate /etc/nginx/full_cas.pem; # 15KB
ssl_verify_client optional_no_ca;
When certificates aren't validating correctly:
# Diagnostic commands
openssl verify -CAfile /etc/nginx/client_cas.pem client_cert.pem
nginx -T 2>&1 | grep ssl.*certificate # Verify loaded paths
# Check TLS handshake details
openssl s_client -connect example.com:443 -showcerts -state
For enterprises requiring granular control:
http {
# Global trusted store (not sent to clients)
ssl_trusted_certificate /etc/nginx/global_cas.pem;
server {
listen 443 ssl;
server_name api.example.com;
# App-specific client CAs (sent during handshake)
ssl_client_certificate /etc/nginx/api_client_cas.pem;
ssl_verify_client on;
# OCSP stapling configuration
ssl_stapling on;
ssl_stapling_verify on;
}
}
Remember that the certificate chain sent to clients should typically include only the necessary intermediates, not your root CA.
When implementing mutual TLS (mTLS) with Nginx, two directives often cause confusion: ssl_client_certificate
and ssl_trusted_certificate
. While both deal with certificate verification, their behaviors differ significantly in ways that impact both security and performance.
The standard TLS handshake with client certificate authentication follows this flow:
1. Client → Server: ClientHello
2. Server → Client: ServerHello, Certificate (server cert), CertificateRequest
3. Client → Server: Certificate (client cert + intermediates), Finished
4. Server verifies client certificate chain against trusted CAs
This directive serves a dual purpose:
ssl_client_certificate /path/to/ca_bundle.pem;
ssl_verify_client on;
- Defines the CA certificates used to verify client certificates
- Sends the entire CA bundle to clients during the handshake
This alternative approach provides more control:
ssl_trusted_certificate /path/to/ca_bundle.pem;
ssl_client_certificate /path/to/empty.pem;
ssl_verify_client on;
- Used for verification without sending CA bundle to clients
- Requires
ssl_client_certificate
to be present (can point to empty file) - Particularly useful for OCSP stapling verification
The choice between these directives affects:
- Handshake size: Large CA bundles increase payload
- Security exposure: Revealing all trusted CAs may help attackers
- OCSP stapling:
ssl_trusted_certificate
required for proper validation
Here's an optimized setup for production:
server {
listen 443 ssl;
# Server certificates
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
# Client verification
ssl_client_certificate /etc/nginx/client_cas/empty.pem;
ssl_trusted_certificate /etc/nginx/client_cas/trusted_cas.pem;
ssl_verify_client on;
ssl_verify_depth 2;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
}
Problem: Nginx fails to start with only ssl_trusted_certificate
Solution: Always include ssl_client_certificate
(can point to empty file)
Problem: Intermediate CA verification failures
Solution: Ensure your trusted certificate file includes the full chain:
# trusted_cas.pem should contain:
# - Root CA
# - Intermediate CA 1
# - Intermediate CA 2
# (in any order)