Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

6.2 Hardening: Protocols, Ciphers, and Headers

Security policy is a list of things you refuse. This lesson writes that list — then verifies the server actually enforces it, handshake by handshake.

Levels: Intermediate ~35 min read

Intermediate

What you will learn

  1. The protocol policy: what TLS 1.2/1.3 mean, and why 1.0/1.1 are gone.

  2. The cipher list: ECDHE-only suites, forward secrecy, and the TLS 1.3 exception (they're not in ssl_ciphers at all).

  3. ssl_prefer_server_ciphers — the debate, settled.

  4. HSTS — the header that outlives your redirect.

  5. The verification ritual: proving policy with real handshakes (§1's TLS 1.1 refusal, captured).


1. The protocol policy

nginx
ssl_protocols  TLSv1.2 TLSv1.3;

The entire protocol stance, one line. The reasoning:

  • TLS 1.0 / 1.1 (1999/2006) — formally deprecated (RFC 8996, 2021), broken by BEAST/POODLE-era weaknesses, mandated-off by every compliance regime that matters (PCI-DSS dropped them in 2018). NGINX defaults to TLSv1.2 TLSv1.3 in official builds since 1.23.4 — your job is to not regress it.

  • TLS 1.2 (2008) — still sound with modern ciphers only. It's the compatibility floor: enterprise fleets of older JDKs, appliances, and the long tail of clients.

  • TLS 1.3 (2018) — faster (1-RTT handshakes, 0-RTT resumption), simpler (a short list of strong suites, mandatory forward secrecy), and where the traffic is. The log line from the last lesson — proto=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 — is the modern norm.

And the refusal, proven live (a real capture — TLS 1.1 offered, server says no):

bash
$ echo | openssl s_client -connect 127.0.0.1:8643 -servername app.example.test -tls1_1
(no session is established; s_client reports the handshake failure —
 alert protocol version, and no certificate is served)
# TLS 1.1 refused by policy

2. The cipher list

nginx
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;

Read the list like a bouncer's clipboard — every name must pass three checks:

  1. ECDHE (elliptic-curve Diffie-Hellman ephemeral) — key exchange where a fresh key is generated per handshake → forward secrecy: a stolen server key can't decrypt past traffic. This is why plain RSA key-exchange suites are gone (they lack it) and why DHE-suites (finite-field, slower) are optional at best.

  2. GCM / POLY1305 — AEAD modes (authenticated encryption): the cipher detects tampering, not just hides content. CBC-mode suites (Lucky13-family weaknesses) are out.

  3. AES128/AES256 or CHACHA20 — the modern bulk ciphers; AES-GCM for hardware-accelerated x86/ARM, CHACHA20-POLY1305 beloved by battery-powered devices without AES instructions.

3. ssl_prefer_server_ciphers — the settled debate

nginx
ssl_prefer_server_ciphers  off;    # our choice

Who picks the cipher when client and server overlap — the client's preference ( off) or the server's list order ( on)?

  • off (client chooses): mobile clients pick CHACHA20 when it's better for them; desktops pick AES-GCM. With a modern-only list, every choice is fine — so let the party with the best information (the client's hardware) decide. This is also Mozilla's modern guidance.

  • on (server order): the old posture for heterogeneous lists where the server wanted to steer traffic to its fastest suites. With TLS 1.2-only fleets and an ordered list, still defensible.

The example runs off because its list contains no bad answer — the principle: harden the list first; the preference flag then barely matters. (TLS 1.3 ignores this flag too.)

4. HSTS — the header that outlives the redirect

nginx
add_header  Strict-Transport-Security  "max-age=31536000"  always;

Verified on every response from the hardened server:

bash
$ curl -sI --cacert certs/ca.crt --resolve app.example.test:8643:127.0.0.1 \
       https://app.example.test:8643/ | grep -i strict
strict-transport-security: max-age=31536000

(HTTP/2 lowercases header names on the wire — Lesson 2.4's $sent_http_* and your assertions should expect that.)

What HSTS does: after the first clean HTTPS visit, the browser refuses plain HTTP for this origin for a year — no man-in-the-middle downgrade, no "just this once over http" attack window. The redirect from Lesson 6.1 protects future clicks; HSTS protects even the first keystroke after memory. Production variants: includeSubDomains (powerful — audit every subdomain first!), preload (submit to browser HSTS preload lists: protection even for the very first visit).

5. The verification ritual

Policy without proof is decoration. The ritual, in five checks:

bash
# 1. the floor holds (old protocols refused):
openssl s_client -connect … -tls1_1   →  no session    ✓ refused
# 2. the ceiling works:
openssl s_client -connect … -tls1_2   →  TLSv1.2 session
openssl s_client -connect … -tls1_3   →  TLSv1.3 session
# 3. the cipher actually negotiated is what you intended:
openssl s_client -connect … | grep "Cipher is"   →  TLS_AES_256_GCM_SHA384
# 4. the audience truth (what real clients negotiate):
grep 'proto=' /var/log/nginx/access.log | sort | uniq -c
# 5. the headers arrive (and survive error responses — "always"):
curl -sI … https://…/nonexistent | grep -i strict

External scans (an "A" from Qualys' SSL Server Test being the standard badge) run the same checks from outside; your ritual catches regressions before they ship. Wire it into CI on any cert/cipher change.

Try it yourself

  1. Reproduce the refusal (§1) with -tls1_1, and feel the difference with -tls1_2/ -tls1_3.

  2. Cipher archaeology. Offer only a banned suite:

    bash
       echo | openssl s_client -connect 127.0.0.1:8643 -tls1_2 \
             -cipher 'AES256-SHA' | tail -3

    No shared cipher → refused. Then offer a modern one by name ( -cipher ECDHE-RSA-AES256-GCM-SHA384) → accepted.

  3. Watch the audience (§5, step 4): hit the site with curl a few times and tally proto=/ cipher= in the access log.

  4. Break HSTS safely. Change the server header to max-age=300; includeSubDomains, reload, verify, revert. (You can't unsend HSTS to a real browser — practice on curl.)

Q&A

Q1. Why do TLS 1.3 cipher names never appear in a correct ssl_ciphers list?

Answer

TLS 1.3's suite set is fixed by the protocol (all AEAD, all forward-secret) and isn't governed by ssl_ciphers, which configures TLS 1.2-and-below negotiation. Listing them is a no-op copied from confused templates.

Q2. What does ECDHE buy that plain RSA key exchange doesn't?

Answer

Ephemeral per-handshake keys → forward secrecy: compromising the server's long-term private key does not decrypt previously captured traffic. RSA key exchange wraps the session key under the static key — one theft, all history.

Q3. Your security team wants ssl_prefer_server_ciphers on "to force the strongest cipher." Right call with the modern-only list? With a legacy list?

Answer

With the modern-only list: harmless but pointless — every negotiable suite is strong; client preference lets devices pick their fastest (CHACHA20 on mobile). With a mixed/legacy list: server order becomes the steering wheel, so on (plus an ordered list) is defensible.

Q4. HSTS max-age=31536000 shipped to an origin where static.example.test still serves plain HTTP. What breaks, and what was the correct rollout?

Answer

Browsers refuse plain HTTP on that subdomain (once the parent's HSTS with includeSubDomains is seen) — the static site becomes unreachable for a year for affected users. Correct rollout: small max-age first, audit all subdomains for HTTPS readiness, then ramp — and only then consider includeSubDomains.

Takeaways

  • Policy = refusals: TLS 1.0/1.1 gone, TLS 1.2 the floor, TLS 1.3 the norm — and §1 proves the floor with a refused handshake.

  • Ciphers: ECDHE (forward secrecy) + AEAD (GCM/CHACHA20-POLY1305)only; TLS 1.3 suites are protocol-fixed and untouched by ssl_ciphers; harden the list and the preference flag fades.

  • HSTS converts your redirect into client-side law — ramp max-age carefully and never let it leak to plain-HTTP origins.

  • Verification is a ritual: floor, ceiling, negotiated cipher, audience log, header survival — five checks, five commands.

Was this page helpful?

Mark this learning resource complete to track your learning progress.