HSTS Explained: Force HTTPS Without Breaking Anything
A practical guide to Strict-Transport-Security: how the max-age ramp works, when to add includeSubDomains and preload, and the cases where deploying HSTS too aggressively will break a site you cannot easily roll back.
by Dowon Oh
This is part of the HTTP Security Headers guide. HSTS — short for HTTP Strict Transport Security — is the header that tells a browser, "from now on, only ever talk to me over HTTPS." Without it, every typed http:// URL, every cached link, and every email-pasted bookmark gives an attacker on the same network one chance to intercept the request before the server gets to redirect it. With it, the browser refuses to even try the plaintext request. The mechanism is small — one response header, three directives — but it interacts with browser caches, subdomain trees, and the Chromium preload list in ways that catch teams off guard the first time they ship it. This article walks through what to put in the header, the deployment ramp I use to audit a third-party endpoint or my own production stack before I commit to a year-long lock, and the cases where you should not turn it on at all.
Why it matters
Imagine a user on hotel Wi-Fi who types example.com into their address bar. The browser, having no prior knowledge of the site, sends an http:// request on port 80. The hotel's captive portal — or anyone running an SSL-stripping proxy on the same access point — sees that request, swaps the inevitable HTTPS redirect for a plaintext page they control, and now reads every cookie and form field that follows. This is the classic "first-visit MITM" gap, and even sites that redirect every plaintext request to HTTPS still leak that very first request. Cookies marked Secure will not be sent on it, but session identifiers, CSRF tokens echoed in URLs, and any credentials posted to a forged login form absolutely will.
RFC 6797 defines the HSTS mechanism and explains why the first-visit problem matters: once a browser has seen a valid HSTS response from an origin, it records a policy for that host, and every subsequent navigation — typed, clicked, or programmatic — is upgraded to HTTPS internally before any packet leaves the device. The browser will not even resolve the plaintext URL. The trade-off is that this policy is sticky, which is exactly the property that makes the rollback story painful and the deployment ramp non-optional.
What the header looks like
The header itself is one line in the response:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Here it is in context, the way you would see it from curl -I against a hardened production endpoint:
$ curl -I https://example.com
HTTP/2 200
content-type: text/html; charset=utf-8
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'
x-content-type-options: nosniff
cache-control: max-age=600
Three directives matter. max-age is the policy lifetime in seconds — 31536000 is one year, 300 is five minutes. includeSubDomains extends the policy to every host under the parent (so api.example.com and staging.example.com are also locked to HTTPS). preload is an opt-in token that signals you intend to submit the host to the Chromium preload list described below.
The max-age ramp
Here is the ramp I run before flipping a year-long lock on a production hostname. The point of going slowly is that HSTS is sticky in the browser cache: if the policy lifetime is one year and you misconfigure HTTPS later — an expired certificate, a botched redirect, a CDN that drops a Subject Alternative Name — every user who has visited the site in the last year is locked out for the remainder of that year, with no override available from your side. The browser will refuse to load the page even if the user clicks past every warning, because there is no warning to click past; the request never reaches the network.
A safer cadence:
- Day 1: ship
max-age=300; includeSubDomains(5 minutes). If something is wrong with HTTPS on a subdomain, the lock evaporates after a short coffee break. - Day 2 to day 7: bump to
max-age=86400(1 day). Now you are exposed for a working day if the cert lapses, which is enough to surface configuration drift but recoverable. - Day 8 onwards, once you are confident every subdomain serves valid HTTPS and the redirect chain is clean: bump to
max-age=31536000(1 year). At this point you are ready to consider preload.
Mozilla MDN recommends ramping max-age gradually for exactly this reason — the cost of a too-eager rollout is a hard outage you cannot fix from your own infrastructure. The browser owns the policy now, and the only remediation is to wait out the timer.
The other detail to know: a fresh HSTS response with a smaller max-age does shorten the policy. If your year-long lock is causing pain, you can ship max-age=0 from a still-reachable HTTPS endpoint to release the policy on the next visit. That release path only works for browsers that can still reach you over HTTPS. If your problem is that they cannot, the timer has to run out the hard way.
The preload list (and why it scares me a little)
The preload list is a Chromium-curated set of hostnames that ship with HSTS baked into the browser binary. Once Chrome — and, by syndication, Firefox, Safari, and Edge — accept your submission at hstspreload.org, every fresh install of those browsers refuses plaintext for your origin from the very first navigation. There is no first-visit gap to worry about. There is also no easy way out.
To be accepted you must serve a valid certificate, redirect every plaintext request to HTTPS on the same host, and emit Strict-Transport-Security: max-age=31536000; includeSubDomains; preload from the apex. The preload token is the explicit consent signal — without it, the submission form rejects you. Once the entry ships in a Chromium release, removal takes a separate request to the maintainers and waits for the rollback to roll out to most users, which the documentation has historically described as taking on the order of three months. In the meantime, every browser update reinforces the lock.
OWASP Cheat Sheet Series walks through preload prerequisites and the rollback story, and I treat that page as the audit checklist before I recommend preload to a team. The Security/Auditor reader should ask the same questions I ask: are there any subdomains under this apex that do not serve HTTPS today? Is there a vendor relationship — a payment processor, an analytics tag, a help-desk widget — that pins us to a hostname we do not fully control? Is the team that owns the apex the same team that owns every leaf? If any answer is "not really," preload is too aggressive.
When NOT to use HSTS
There are real cases where HSTS is the wrong call, or where the safe variant is the one without includeSubDomains and without preload.
- Dev and staging subdomains under a preloaded apex. If you preload
example.comwithincludeSubDomains, you have also preloadeddev.example.comandinternal.example.com. Anyone using a self-signed cert on those leaves now has a hard outage on every browser. - Short-lived preview deployments. Branch-deploy URLs that come and go faster than the policy lifetime are a poor fit for any non-trivial
max-age. If you must enable HSTS on previews, keep it short and unpreloaded. - Sites you do not control end-to-end. Multi-tenant CDNs, white-label hosting, and any setup where a third party can change your TLS configuration without your involvement are not safe candidates for preload. The whole point of the lock is that no one can downgrade you, including you.
- Any host where rollback within 24 hours has to remain possible. If business continuity demands the ability to flip back to plaintext under emergency, do not enable HSTS at all on that host. Use it on the rest of the property.
The general rule: skip includeSubDomains until you have audited every subdomain, and skip preload until you are certain a year-plus lock is acceptable.
Test your own headers
If you want to confirm what your server is sending today, run a check with the HTTP Header Analyzer — it parses the response in browser with no upload, so you can audit production endpoints without leaving evidence in someone else's logs. The analyzer flags missing or weak HSTS configurations, calls out an absent includeSubDomains directive, and surfaces the max-age value in seconds and in human-readable units so you can see at a glance whether you are still on the ramp or already at the year-long lock.
Sources
Specifications
OWASP
Mozilla MDN
web.dev
Further reading
- Complete Guide to HTTP Security Headers — the parent pillar; the HSTS section there expands on browser internals and how the policy interacts with the Service Worker cache.
- What is Content-Security-Policy (CSP)? A Practical Guide — sibling supporting in the same cluster; pairs naturally with HSTS in any hardening checklist.