Skip to content
SumGuy's Ramblings
Go back

TLS 1.3: Modern Encryption Without the Existential Dread

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:

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:

  1. Client Hello
  2. Server Hello + Certificate + Server Hello Done
  3. Client Key Exchange + Change Cipher Spec + Finished
  4. Server Change Cipher Spec + Finished
  5. Finally, your HTTP request

TLS 1.3:

  1. Client Hello (includes key share)
  2. Server Hello + Certificate + Finished (keys derived immediately)
  3. Client Finished
  4. 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:

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

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:

Your 2 AM self will appreciate having done this now rather than debugging an expired cert or a negotiation failure with a security scanner.


Share this post on:

Next Post
IPFS: Peer-to-Peer File Storage for People Who've Seen Too Many 404s