Your TLS 1.2 Config Is a Museum Exhibit
If you stood up an Nginx server three years ago and copy-pasted a config from Stack Overflow, you’re probably still offering cipher suites that cryptographers politely describe as “yikes.” TLS 1.2 shipped with support for a pile of legacy algorithms — RC4, MD5 signatures, RSA key exchange without forward secrecy, 3DES. The list reads like a cryptography horror museum.
TLS 1.3, finalized in RFC 8446 (2018), didn’t negotiate with the past. It bulldozed it.
Here’s what changed, what you need to do about it, and why your users will thank you — even if they never know what TLS is.
What Actually Changed Between TLS 1.2 and 1.3
The Cipher Suite Massacre
TLS 1.2 supported 37 cipher suites. Some of them were fine. Many of them were grandfathered disasters that existed purely for backward compatibility with systems running software older than some of your junior devs.
TLS 1.3 ships with exactly five cipher suites:
TLS_AES_128_GCM_SHA256TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_CCM_SHA256TLS_AES_128_CCM_8_SHA256
All of them use AEAD (Authenticated Encryption with Associated Data). All of them are good. You don’t get to pick a bad one by accident anymore. This is the cryptographic equivalent of a restaurant removing the food poisoning section from the menu.
The 1-RTT Handshake
TLS 1.2 required two round trips before your browser could start sending data. TLS 1.3 does it in one. Here’s the rough difference:
TLS 1.2:
- Client Hello
- Server Hello + Certificate + Server Hello Done
- Client Key Exchange + Change Cipher Spec + Finished
- Server Change Cipher Spec + Finished
- Finally, your HTTP request
TLS 1.3:
- Client Hello (includes key share)
- Server Hello + Certificate + Finished (keys derived immediately)
- Client Finished
- HTTP request goes here — same flight as the Finished message
One round trip instead of two. At 50ms latency, that’s a 50ms head start on every new connection. Doesn’t sound like much until you’re loading a page with 40 sub-resources.
Perfect Forward Secrecy Is Now Mandatory
In TLS 1.2, you could use RSA key exchange — where the server’s long-term private key directly encrypts the session key. If someone records your traffic now and steals your private key later, they can decrypt everything retroactively. This is called a “harvest now, decrypt later” attack, and it’s absolutely what nation-state actors were doing.
TLS 1.3 removed RSA key exchange entirely. Every session uses ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) — meaning a fresh key pair is generated per connection. Your long-term private key never touches the session traffic. Old recordings stay encrypted forever. That’s perfect forward secrecy, and it’s now the only option.
0-RTT Resumption (The Double-Edged Sword)
TLS 1.3 introduced 0-RTT session resumption — if a client has connected to you before, it can send data in the very first message, cutting latency to zero for returning visitors.
Sounds great. The catch: 0-RTT data is replay-vulnerable.
If an attacker intercepts your 0-RTT request, they can replay it. This is fine for a GET request for a static page. It is absolutely not fine for a POST request that charges a credit card or changes a password.
For most self-hosted setups: disable 0-RTT or only enable it on truly idempotent endpoints. Don’t chase that last millisecond with your users’ money.
Checking What TLS Version Your Server Is Using
# Check what TLS versions a server supports
openssl s_client -connect yourdomain.com:443 -tls1_2 2>/dev/null | grep "Protocol"
openssl s_client -connect yourdomain.com:443 -tls1_3 2>/dev/null | grep "Protocol"
# Full handshake info
openssl s_client -connect yourdomain.com:443 </dev/null 2>&1 | grep -E "Protocol|Cipher"
# What cipher suite was negotiated
openssl s_client -connect yourdomain.com:443 </dev/null 2>&1 | grep "Cipher is"
If you see Protocol: TLSv1 or TLSv1.1 anywhere, stop reading this article and go fix that first.
Configuring Nginx for TLS 1.3 Only
Here’s a production-ready Nginx TLS block:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# TLS 1.3 only — if you need 1.2 for legacy clients, add TLSv1.2
ssl_protocols TLSv1.3;
# TLS 1.3 cipher suites are controlled by OpenSSL, not this directive
# But for TLS 1.2 fallback if you add it:
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_prefer_server_ciphers off; # Let clients pick in TLS 1.3
# ECDH curve
ssl_ecdh_curve X25519:prime256v1:secp384r1;
# Session cache (useful for TLS 1.2 fallback)
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# HSTS — tells browsers to always use HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Other security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
location / {
# your app config
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
Test your config before reloading: nginx -t && nginx -s reload
Caddy Does This for Free (Literally)
If you’re running Caddy, the entire TLS configuration above is roughly this:
yourdomain.com {
reverse_proxy localhost:8080
}
Caddy automatically:
- Gets and renews Let’s Encrypt certs
- Configures TLS 1.2 and 1.3 (with sane cipher suites)
- Enables HSTS
- Enables OCSP stapling
- Handles HTTP → HTTPS redirects
If you want TLS 1.3 only with Caddy:
yourdomain.com {
tls {
protocols tls1.3
}
reverse_proxy localhost:8080
}
Caddy makes the right choice the default choice. It’s honestly kind of embarrassing how much config it eliminates.
HSTS: Teaching Browsers to Never Trust HTTP
HSTS (HTTP Strict Transport Security) is a response header that tells browsers: “if you ever try to connect to this domain over HTTP again, upgrade yourself to HTTPS before even sending the request.” No round trip to the server required.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age=63072000— two years, the recommended minimum for preloadingincludeSubDomains— applies to all subdomains (make sure they all have HTTPS first)preload— submit to the browser preload list so new browsers know before the first visit
The preload list is permanent. Once you’re on it, every browser shipping with that list trusts only HTTPS for your domain. Don’t add preload if you’re not sure you can maintain HTTPS forever on all subdomains.
OCSP Stapling: Faster Certificate Validation
When a browser connects to your site, it needs to verify your certificate hasn’t been revoked. Without OCSP stapling, the browser contacts the Certificate Authority directly — adding a DNS lookup and HTTP request to your page load.
OCSP stapling moves this: your server fetches the OCSP response from the CA periodically and “staples” it to the TLS handshake. The browser gets revocation status from you, not from a third party. Faster, more private, and it works even if Let’s Encrypt’s OCSP servers are slow.
The Nginx config above enables it. Verify it’s working:
openssl s_client -connect yourdomain.com:443 -status </dev/null 2>&1 | grep -A 10 "OCSP response"
You should see OCSP Response Status: successful — not no staple.
Let’s Encrypt Automation (Because Manual Certs Are a Trap)
# Install certbot
apt install certbot python3-certbot-nginx
# Get a cert and auto-configure Nginx
certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Test auto-renewal
certbot renew --dry-run
# Check the timer
systemctl status certbot.timer
Certbot installs a systemd timer that runs twice daily. Your cert renews at 60 days remaining, and you never think about it again. If you’re managing more than two or three domains, look at acme.sh for more control.
The automation isn’t optional — a cert that expires at 2 AM on a Saturday is a rite of passage you only want to experience once.
Quick Sanity Check
Run your domain through SSL Labs or check locally:
# Verify TLS 1.3 is working
curl -v --tlsv1.3 https://yourdomain.com 2>&1 | grep "SSL connection"
# Verify old TLS is rejected
curl -v --tls-max 1.1 https://yourdomain.com 2>&1 | grep -E "failed|error|SSL"
If TLS 1.1 gets rejected and TLS 1.3 connects cleanly, you’re done. Get the A+ on SSL Labs and put it in your home lab README where it belongs.
The Practical Bottom Line
TLS 1.3 is faster, safer, and has fewer footguns than 1.2. Configuring it properly takes about ten minutes. The main things to remember:
- TLS 1.3 is mandatory if you care about perfect forward secrecy by default
- 0-RTT is clever but replay-vulnerable — skip it unless you know what you’re doing
- HSTS and OCSP stapling are easy wins you should always enable
- Caddy removes most of this config burden if you can use it
- Let’s Encrypt + certbot means you never think about cert renewals again
Your 2 AM self will appreciate having done this now rather than debugging an expired cert or a negotiation failure with a security scanner.