TLS 1.3 has been an RFC since 2018 and is now the dominant protocol on the public internet. If you are still running a service that supports 1.2 and 1.0/1.1 for "legacy compatibility", you are paying a real cost in attack surface and a measurable cost in handshake latency. The migration is mostly mechanical — but a few gotchas keep tripping up engineers.
The shape of the upgrade
The headline differences:
- Handshake. 1.2 requires two round trips to set up a session. 1.3 does it in one (and 0-RTT for resumed sessions, with caveats). On a 100ms RTT link that is a 100ms page-load improvement on the first request.
- Cipher suite list. 1.3 ships with five cipher suites, period. All AEAD. No CBC, no RC4, no SHA-1, no static RSA key exchange, no compression. The "we should disable bad ciphers" config job goes away.
- Forward secrecy mandatory. All 1.3 cipher suites use ephemeral Diffie-Hellman. A compromised long-term private key cannot decrypt past sessions.
- Encrypted handshake. Everything after the ClientHello/ServerHello is encrypted, including the certificate. Network passive observers no longer learn which cert was presented.
What this breaks
Three things bite teams during the migration:
- Middleboxes. Old corporate TLS inspection proxies, DLP appliances, layer-7 firewalls — anything that parses TLS — was often built against 1.2 and treats unknown extensions as malformed. The 1.3 spec was actually rewritten twice during draft to make it look more like 1.2 on the wire so middleboxes wouldn't drop the connection. Even so, expect to find one device on your network that needs a firmware update.
- Old client libraries. Anything compiled against OpenSSL < 1.1.1, GnuTLS < 3.6.4, or Java < 11 (and even 11 needs a flag) cannot speak 1.3. CI runners, embedded devices, very old curl in a corporate base image — audit the long tail.
- HSM-backed certificate keys. Some HSMs don't support the elliptic curve operations 1.3 prefers, or do them so slowly that the per-connection CPU cost becomes a bottleneck. Benchmark before flipping the switch on a high-traffic edge.
Configuration you actually want
For a server (nginx):
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_dhparam /etc/nginx/dhparam.pem;
Note ssl_session_tickets off — session tickets in 1.2 had a documented forward-secrecy weakness in default OpenSSL configurations. 1.3 redesigned tickets but the safest configuration for most installations is to use stateful session resumption (the cache) or no resumption at all.
The cipher list above is the Mozilla "intermediate" recommendation. It only applies to TLS 1.2 — 1.3 ignores ssl_ciphers entirely because its cipher suite set is fixed.
0-RTT: the footgun
1.3's 0-RTT mode lets a client send application data with the first message, using a pre-shared key from a previous session. This shaves another round trip. The catch: 0-RTT data is replayable. An attacker who captures a 0-RTT request can replay it later, and the server cannot tell. This is fine for idempotent GETs. It is catastrophic for non-idempotent POSTs.
If you enable 0-RTT, scope it to specific safe endpoints (GET requests with no side effects) and reject 0-RTT data on anything mutative. Cloudflare's explainer covers the threat model well.
How to verify what you negotiated
From the command line:
openssl s_client -connect example.com:443 -tls1_3 < /dev/null 2>&1 | grep "Protocol\\|Cipher"
Or, for a multi-protocol matrix in one shot, the TLS panel on dnscheck shows which versions a host accepts plus the cipher and chain details — try cloudflare.com for a host that is on the bleeding edge.
References: RFC 8446 (TLS 1.3), Mozilla Server Side TLS.