Nginx can handle TLS 1.3 connections, but its default configuration is often stuck in the past, exposing you to unnecessary security risks and limiting performance.
Here’s how Nginx actually negotiates TLS 1.3 in the real world. Imagine a client (your browser) wants to connect to your Nginx server securely.
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# This is where the magic happens for TLS 1.3
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off; # Important for TLS 1.3
# Optional: Enable OCSP stapling for faster certificate validation
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/example.com.ca.crt; # Chain certificate
resolver 8.8.8.8 8.8.4.4 valid=300s; # Google DNS, cache for 300 seconds
resolver_timeout 5s;
# Optional: Session resumption for performance
ssl_session_cache shared:SSL:10m; # 10MB cache, shared across worker processes
ssl_session_timeout 10m; # Sessions valid for 10 minutes
ssl_session_tickets off; # Recommended for TLS 1.3 to prevent downgrade attacks
location / {
proxy_pass http://backend_app;
}
}
When a client connects, Nginx first advertises the TLS protocols it supports. If the client also supports TLS 1.3, they’ll attempt a TLS 1.3 handshake. The key difference here is that TLS 1.3 dramatically simplifies the handshake. Instead of multiple round trips to negotiate cipher suites and authentication, TLS 1.3 often achieves this in just one round trip. This is facilitated by the fact that TLS 1.3 has a fixed set of cipher suites and uses a mechanism called "pre-shared keys" (PSK) for faster resumption.
The ssl_protocols TLSv1.2 TLSv1.3; directive explicitly tells Nginx to offer both TLS 1.2 and TLS 1.3. If the client supports TLS 1.3, it will be preferred.
The ssl_ciphers directive is still present, but for TLS 1.3, it’s primarily used to select the key exchange mechanism and the authentication method, not the symmetric encryption cipher itself, as TLS 1.3 has a standardized set of strong AEAD (Authenticated Encryption with Associated Data) ciphers. The ssl_prefer_server_ciphers off; setting is crucial here. In TLS 1.2, Nginx would use this to prioritize its own cipher list. However, in TLS 1.3, the client and server negotiate the cipher more dynamically, and turning this off allows the client’s preference to be more impactful, which is generally good for compatibility and security.
OCSP stapling (ssl_stapling on; ssl_stapling_verify on;) is an optimization. Instead of the client having to check the revocation status of your certificate with the Certificate Authority (CA) itself, Nginx periodically fetches this information from the CA and "staples" it to the certificate it sends to the client during the handshake. This speeds up the handshake for the client and reduces the load on CAs. The resolver directive is necessary for Nginx to perform these DNS lookups for OCSP.
Session resumption (ssl_session_cache, ssl_session_timeout) is a performance booster. Once a full TLS handshake is complete, the server can issue a "session ticket" to the client. On subsequent connections, the client can present this ticket, allowing Nginx to resume the session without a full handshake, saving CPU cycles and reducing latency. For TLS 1.3, ssl_session_tickets off; is often recommended. While tickets can speed up resumption, they can also be a vector for downgrade attacks if not implemented carefully, especially when mixing TLS 1.2 and TLS 1.3. TLS 1.3’s 0-RTT data capabilities are a separate, more advanced feature.
The true power of TLS 1.3 lies in its simplification and enhanced security. By removing obsolete and insecure cipher suites and handshake steps, it reduces the attack surface and makes connections faster. The fixed set of cipher suites means you don’t have to agonize over endless combinations of encryption algorithms; Nginx and the client will agree on one of the strong, standardized options.
What most people miss is the subtle interaction between ssl_protocols and ssl_ciphers when TLS 1.3 is enabled. While ssl_ciphers still defines what Nginx can use, for TLS 1.3, it effectively becomes a filter on the key exchange and authentication algorithms that are then used to negotiate one of the fixed, strong AEAD ciphers. If you set ssl_protocols to include TLS 1.3 but your ssl_ciphers list is extremely restrictive or only contains TLS 1.2-specific ciphers, you might not actually get TLS 1.3 connections, or worse, you might break them entirely because the client and server can’t agree on a compatible key exchange method.
The next hurdle is understanding and configuring HTTP/2 or HTTP/3, which are often enabled alongside TLS 1.3 for maximum performance.