TLS Termination & HTTPS Config
TLS Termination & HTTPS Config
Running plaintext HTTP in production is not a configuration choice — it is a security liability. TLS is the cryptographic layer that authenticates your server to clients and encrypts every byte in transit. In a modern DevOps stack, Nginx almost always acts as the TLS termination point: it decrypts incoming HTTPS traffic, then forwards plain HTTP (or Unix-socket traffic) to your application backends running on localhost or an internal network where encryption would be redundant overhead.
This lesson goes beyond "drop in a ssl_certificate line." You will learn how to obtain certificates, write a production-grade Nginx TLS block, enforce redirects, configure HSTS, tune cipher suites, and avoid the failure modes that silently degrade security or cause client connection errors at 3 a.m.
How TLS Termination Works
Obtaining Certificates with Certbot (Let's Encrypt)
Let's Encrypt issues free, 90-day DV (Domain Validated) certificates that are trusted by all major browsers. certbot automates issuance and renewal. For a fresh server:
*.example.com) via the DNS-01 challenge instead of HTTP-01. This requires a DNS provider plugin (e.g. certbot-dns-cloudflare) but eliminates the need for a publicly reachable HTTP server during renewal, which matters for internal services. Store wildcard certs centrally and distribute them to all Nginx nodes via a secrets manager or a dedicated certificate tool like step-ca.
A Production-Grade Nginx TLS Block
Never accept the bare-minimum config that "just works." Every setting below has a concrete reason.
ssl_session_tickets off? Session tickets encrypt the session state with a server-side key. If that key is ever compromised, an attacker can decrypt all past traffic (no forward secrecy). Disabling tickets forces Nginx to use its in-memory session cache, which is per-worker-set and rotates naturally. Most production configs at Google, Cloudflare, and Mozilla disable tickets for this reason.
HTTP Strict Transport Security (HSTS)
HSTS is an HTTP response header that instructs browsers to never connect to your domain over plain HTTP — not even for the very first request after the max-age expires. Once a browser has seen the HSTS header, it will internally redirect http:// to https:// before sending any bytes on the wire, eliminating the window for SSL-stripping attacks.
max-age=31536000— one year; browsers remember this for 365 days after the last response.includeSubDomains— extends the policy to every subdomain. Only add this after every subdomain has a valid certificate.preload— opts your domain into browser-vendor HSTS preload lists (ships inside Chrome, Firefox, Safari). Submit athstspreload.org. This is essentially permanent — removal takes months.
preload and submit to the list, then later need to move back to HTTP (e.g. for a staging domain), browsers will refuse to connect for up to a year. Only enable preload on domains you are certain will serve HTTPS forever.
Validating Your TLS Configuration
After applying config, test before assuming anything is correct. Two essential checks:
Common Failure Modes
- Mixed content warnings — your HTML loads over HTTPS but embeds
http://resource URLs. The browser blocks or warns. Fix: ensure all asset URLs usehttps://or protocol-relative//. Configure your app framework to generate HTTPS URLs when behind a proxy (APP_URL=https://...,FORCE_HTTPS=true, or Laravel'sURL::forceScheme('https')). - Certificate chain incomplete — servers must send the full chain (leaf + intermediates). Use
fullchain.pem, notcert.pem. Mobile clients in particular fail silently when the intermediate is missing. - Expired certificate — Let's Encrypt certs expire after 90 days. Certbot's systemd timer renews at 60 days, but if the timer stops (system reboot with failed services), you get a 60-day runway to notice. Monitor expiry:
certbot certificatesor an external monitor like UptimeRobot's SSL check. - Wrong
server_name— Nginx serves the first matchingserverblock if noserver_namematches, which can serve the wrong certificate. Always set an explicit default server or verify the block ordering.
openssl s_client -connect $HOST:443 < /dev/null 2>&1 | openssl x509 -noout -dates and alerts if notAfter is within 30 days. This catches renewal failures before users do.