Frontend Labs
⋯

Chapter 31 — TLS in production

In one sentence

Running TLS well is mostly operational: where you terminate it, how you configure it, how you renew certs, whether you authenticate clients, and avoiding a handful of classic misconfigurations.

Where TLS terminates

   PATTERN A -- terminate at the edge (load balancer / CDN / reverse proxy)
     client --TLS--> [ LB / Cloudflare / nginx ] --plaintext or new TLS--> app

     + central cert management, offload crypto from app servers
     + WAF, rate limiting, HTTP routing all see plaintext
     - traffic from LB to app is in the clear unless you re-encrypt
       -> inside a trusted network boundary? maybe OK. Zero-trust? no.
     - the app doesn't see the real client IP / TLS details unless the
       LB forwards them (X-Forwarded-For, PROXY protocol) -- and if it
       forwards them, you must NOT trust those headers from anywhere else

   PATTERN B -- terminate at the app (or a sidecar)
     client --TLS--> app

     + end-to-end encrypted to the process
     - each app instance manages certs, does its own crypto

   PATTERN C -- re-encrypt / TLS everywhere (service mesh, mTLS)
     client --TLS--> [ LB ] --mTLS--> [ sidecar ] --mTLS--> service
     + zero-trust: every hop authenticated and encrypted
     - operational complexity; mesh (Istio, Linkerd) or careful automation

Most real systems are A with re-encryption for sensitive backends. Know which one you have and where plaintext exists.

mTLS — authenticating the client too

Normal TLS authenticates only the server. Mutual TLS also makes the client present a certificate.

   Server config: request (or require) a client cert, verify it against a
   CA you control.

   USE FOR:  service-to-service auth inside your infra, API clients,
             admin access, IoT fleets, bank/partner integrations.
   NOT FOR:  general public web users (cert distribution to humans is
             miserable; use WebAuthn/passkeys instead -- Part 10).

   mTLS gives you strong, phishing-proof client identity. The hard parts
   are ISSUING client certs, ROTATING them, and REVOKING them -- i.e. you
   now run a mini-PKI. SPIFFE/SPIRE, step-ca, or a mesh automate this.

A sane TLS configuration (2026)

   Protocols:     TLS 1.3 + TLS 1.2.  Disable 1.1, 1.0, SSLv3.
   TLS 1.2 suites (if you must support it): ECDHE + AES-GCM or
                  CHACHA20-POLY1305 only. No CBC, RC4, 3DES, static RSA,
                  NULL, EXPORT, anon.
   TLS 1.3 suites: the default five are fine; prefer AES-256-GCM /
                  CHACHA20-POLY1305.
   Key exchange:  X25519 (and P-256). Add X25519MLKEM768 if supported (Ch 25).
   Certs:         ECDSA P-256 leaf (smaller, faster) with an RSA fallback
                  chain only if you still serve very old clients.
   OCSP stapling: on.
   Session:       tickets with frequent key rotation, or stateful cache.
                  0-RTT: off, unless you've audited every early-data path.
   HSTS:          Strict-Transport-Security: max-age=31536000;
                  includeSubDomains; preload   (see below)
   Renewal:       automated (ACME). Alert if a cert is < 20 days from expiry.

   Don't hand-write this. Use Mozilla's SSL Config Generator ("intermediate"
   profile) for your server, then verify with testssl.sh / SSL Labs.

HSTS — forcing HTTPS

   Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

   Tells the browser: "for the next year, NEVER talk to this host over
   plain HTTP, and don't let the user click through cert errors."

   * Defeats sslstrip (attacker downgrades your redirect to HTTP).
   * First visit is still unprotected -> the PRELOAD LIST (hstspreload.org)
     ships the rule in the browser itself. Submitting is a commitment:
     removal takes months. Make sure ALL subdomains do HTTPS first.

The classic misconfigurations (all seen in the wild)

   [ ] Disabled certificate verification in a client
       curl -k / verify=False / rejectUnauthorized:false /
       a TrustManager that does nothing / NSURLSession delegate that
       always trusts. -> total MITM exposure. GREP YOUR CODEBASE FOR THIS.
   [ ] Mixed content: HTTPS page loads http:// scripts -> downgrade point.
   [ ] Missing intermediate in the served chain (Ch 29).
   [ ] Wildcard cert shared across trust boundaries / teams / tenants.
   [ ] Private key committed to git, or world-readable, or copied to laptops.
   [ ] Same key reused when renewing for years (rotate the key, not just
       the cert).
   [ ] TLS 1.0/1.1 still enabled "for that one old client".
   [ ] Cert covers apex but not www (or vice versa).
   [ ] Internal services on self-signed certs with verification turned off
       "because it's internal" -> run an internal CA instead.
   [ ] Trusting X-Forwarded-For / X-Forwarded-Proto from outside the LB
       -> spoofed client IP, bypassed "HTTPS-only" checks.
   [ ] Expired cert in a background job / cron / mobile pinning config that
       nobody monitors.
   [ ] HSTS preload submitted before a subdomain was HTTPS-ready -> outage.

Certificate pinning — powerful and dangerous

   Pin = your app refuses any cert/key except specific known ones,
   ignoring the normal CA trust decision.

   + defeats a mis-issued or rogue-CA cert entirely
   - if you pin and then need to rotate keys/CAs and forgot to ship a
     backup pin, your app BRICKS itself until an update propagates.
     (This has caused multi-day outages at large companies.)

   Rules if you pin (mostly mobile apps talking to your own API):
     * pin to a CA or intermediate SPKI, not the leaf
     * always ship >= 2 pins including an offline backup key
     * set an expiry; have a killswitch
   For the general web, HPKP (the header version) was removed by browsers
   for exactly these footguns. Use CT monitoring + CAA instead.

Practice (40 min)

cd ~/sec-lab/scratch

# 1. Grade a real site
docker run --rm -ti drwetter/testssl.sh https://wikipedia.org
# or: testssl.sh --fast https://your-service

# 2. Stand up a properly-configured local HTTPS server with your Ch 29 CA
cd ~/sec-lab/ca
openssl s_server -accept 8443 -cert shop.crt -key shop.key \
        -CAfile ca.crt -tls1_3 -www &
curl --cacert ca.crt https://localhost:8443 -sI      # trusts your CA -> works
curl                 https://localhost:8443 -sI      # no CA -> cert error (good)
curl -k              https://localhost:8443 -sI      # -k bypasses -> DANGER demo
kill %1

# 3. Add mutual TLS
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out client.key
openssl req -new -key client.key -subj "/CN=orders-api-client" -out client.csr
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
        -days 30 -out client.crt
openssl s_server -accept 8443 -cert shop.crt -key shop.key \
        -CAfile ca.crt -Verify 1 -tls1_3 -www &
curl --cacert ca.crt https://localhost:8443 -sI                    # 400/handshake fail: no client cert
curl --cacert ca.crt --cert client.crt --key client.key \
     https://localhost:8443 -sI                                    # OK: client authenticated
kill %1

# 4. Find verification-disabled code (run against your own repos)
grep -rniE "verify=false|rejectUnauthorized|InsecureSkipVerify|NSAllowsArbitraryLoads|curl.*-k|--insecure|trustAllCerts|ALLOW_ALL_HOSTNAME" . 2>/dev/null

# 5. Check HSTS and headers on your services
curl -sI https://wikipedia.org | grep -i -E "strict-transport|content-security"

Mini-project (60 min): "TLS posture" report

   Write a tool `tls-audit <host:port>` that reports:
     - negotiated protocol + cipher + key-exchange group
     - full chain: each cert's subject, issuer, SANs, validity, days left,
       key type/size, signature algorithm
     - whether the served chain is complete (no missing intermediate)
     - HSTS present? max-age? includeSubDomains? preload-eligible?
     - OCSP stapling present?
     - CAA records for the domain
     - recent CT log entries for the domain (crt.sh)
     - a PASS/WARN/FAIL summary against the "sane config 2026" list above
   Run it against 5 sites you use. Then against your own.

Common confusions

  • "Internal traffic doesn't need TLS." Flat internal networks are where breaches spread. Zero-trust means encrypt + authenticate every hop.
  • "-k is fine for testing." It trains fingers and gets copied to prod. Add your test CA to the trust store instead, or use --cacert.
  • "Terminating TLS at the LB means it's end to end." It's encrypted to the LB. Everything past that is your responsibility.
  • "HSTS is risky, it might lock users out." With HTTPS working, HSTS is strictly good. Only preload + includeSubDomains is a hard commitment — stage it.
  • "Pinning is best practice, always pin." Pinning without a tested backup pin and killswitch is how apps brick themselves. For the web, don't; for your own mobile app to your own API, carefully.

Check yourself

  1. Name the three TLS-termination patterns and where plaintext exists in each.
  2. When is mTLS the right choice, and what operational burden does it add?
  3. What attack does HSTS defeat, and what's the gap it doesn't cover (and the fix)?
  4. Give five TLS misconfigurations you'd grep a codebase for.
  5. Why is certificate pinning both powerful and dangerous? State two rules for doing it safely.
  6. Why must you not trust X-Forwarded-For from arbitrary sources?

Further reading

  • Tool: Mozilla SSL Configuration Generator (ssl-config.mozilla.org) — the starting point for every server config.
  • Tool: testssl.sh and Qualys SSL Labs (ssllabs.com/ssltest) — grade and diagnose. Aim for A/A+.
  • Book: Bulletproof TLS and PKI, Ristić — Part III (deployment) is essentially this chapter, expanded.
  • Project: SPIFFE/SPIRE (spiffe.io), step-ca (smallstep) — automated internal PKI and mTLS.
  • Guidance: OWASP "Transport Layer Security Cheat Sheet" and "Certificate and Public Key Pinning" pages.
  • Article: "The Sad State of Certificate Revocation" and Scott Helme's blog (HSTS, CAA, security headers, real-world data).

Generated from real-life-guide.md · 8 Oct 2026 16:36