Nginx Client Certificate Verification: Deep Dive into ssl_client_certificate vs ssl_trusted_certificate


3 views

When implementing mutual TLS (mTLS) with Nginx, the certificate verification flow follows this sequence:

  1. Client presents its certificate chain during TLS handshake
  2. Server (Nginx) validates against its trusted CA store
  3. 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)