Where the handshake cost actually goes
A full TLS 1.2 handshake costs two round trips: ClientHello to ServerHello, potentially with a certificate exchange in between, and only after the client validates the certificate do ChangeCipherSpec and Finished occur. With TLS 1.3 enabled, Nginx compresses this to a single round trip.
Session resumption is the main lever for eliminating repeated handshake cost. It comes in two forms: the server-side session cache keeps state in Nginx and clients return with a session id, while session tickets hand the state to the client in encrypted form and the server keeps only the key.
Nginx enables session tickets and disables the session cache by default. If you intend to use the cache, set ssl_session_cache explicitly and turn tickets off. Running both makes some clients prefer tickets, which shows up as mysteriously low cache hit rates.
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/ssl/certs/site.pem;
ssl_certificate_key /etc/ssl/private/site.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 1d;
ssl_session_tickets off;
}What actually changes in TLS 1.3
TLS 1.3 removes a large set of legacy algorithms: static RSA key exchange, CBC-mode suites, and every SHA-1 and MD5 signature algorithm, and it drops the compression extension. The handshake messages were also streamlined, removing round trips such as ServerHelloDone.
Forward secrecy becomes the default. Every TLS 1.3 key exchange suite is ECDHE or DH based, so forward secrecy arrives without extra configuration and ephemeral keys are wiped automatically. Correspondingly, SSLv3, TLS 1.0 and TLS 1.1 should be removed from ssl_protocols.
Enabling TLS 1.3 in Nginx only requires adding TLSv1.3 to ssl_protocols; it is available by default with OpenSSL 1.1.1 and newer. Mind legacy clients: anything that supports only TLS 1.2 will fail the handshake outright, so keep TLSv1.2 as a transition option. With HTTP/2 enabled, TLS 1.3 spreads handshake cost over more requests.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # TLS 1.3 下客户端选套件,1.2 段建议单独指定
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;OCSP stapling removes a round trip
Revocation status can be fetched online through OCSP, but a direct client-to-CA query adds latency and can block rendering when the CA is unreachable. Stapling lets the server attach the CA-signed revocation response during the handshake so the client never issues a separate query.
Enabling it in Nginx takes three steps: turn on ssl_stapling, supply a CA bundle that includes the issuer certificate via ssl_trusted_certificate, and configure a resolver for DNS. Nginx must resolve the CA OCSP hostname during the handshake, so without a resolver stapling fails silently.
After enabling, inspect the stapling status with `nginx -T` and verify from the client side with an external checker. The usual failures are a CA bundle missing the intermediate certificate or an unreadable ssl_trusted_certificate file. While debugging you can set ssl_stapling_verify off to skip issuer validation, but only for investigation.
resolver 223.5.5.5 223.6.6.6 valid=300s;
resolver_timeout 5s;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/ca-bundle.crt; # 含中间证书
# 排障用:nginx -T | grep -i staplingA prioritized optimization path for short-lived connections
First priority is the number of handshakes. Session resumption, whether via cache or tickets, drives the cost of a repeat handshake close to zero and gives the largest gain for the smallest change. Verify it works by observing whether a second connection from the same client is noticeably faster.
Second priority is reducing connection creation itself. Keepalive connection pools upstream and HTTP/2 multiplexing on the client side both let a single handshake serve more requests. For Nginx as a reverse proxy, upstream keepalive frequently pays off more directly than tuning TLS parameters.
Only then move to micro-optimizations such as shorter certificate chains or avoiding needless DH parameter exchange. And measure: the $ssl_session_reused variable gives the resumption ratio, while $ssl_protocol and $ssl_cipher confirm what was actually negotiated. Do not declare victory based on the config file alone.
log_format ssl "$remote_addr $ssl_protocol $ssl_cipher reused=$ssl_session_reused rt=$request_time";
access_log /var/log/nginx/ssl.log ssl;
upstream backend {
server 10.0.0.11:8080;
keepalive 64; # 上游连接复用
}
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://backend;
}HSTS and certificate operations
Session resumption only pays off if clients actually come back over TLS. Enabling HSTS with add_header Strict-Transport-Security makes browsers force HTTPS for the max-age window, which avoids a user hitting a 301 redirect first; that extra round trip would otherwise cancel most of the gain.
Certificate and key operations determine whether the handshake completes. Point ssl_certificate at the full chain (leaf plus intermediate) rather than the leaf alone, since an incomplete chain is the most common handshake failure. Wire up automated renewal with a reload hook, because a single expiry takes the whole site offline instantly.
Always run `nginx -t` before reloading, then verify what was actually negotiated: inspect the protocol version, cipher and chain with `openssl s_client -connect`, and confirm from a real browser that nothing fell back to HTTP. Do not start HSTS with a huge max-age either; validate that every subdomain already serves HTTPS, then extend it gradually.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/site.key;
nginx -t && nginx -s reload
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null 2>/dev/null | openssl x509 -noout -dates