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.
- "
-kis 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+includeSubDomainsis 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
- Name the three TLS-termination patterns and where plaintext exists in each.
- When is mTLS the right choice, and what operational burden does it add?
- What attack does HSTS defeat, and what's the gap it doesn't cover (and the fix)?
- Give five TLS misconfigurations you'd grep a codebase for.
- Why is certificate pinning both powerful and dangerous? State two rules for doing it safely.
- Why must you not trust
X-Forwarded-Forfrom arbitrary sources?
Further reading
- Tool: Mozilla SSL Configuration Generator (
ssl-config.mozilla.org) — the starting point for every server config. - Tool:
testssl.shand 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).