HSTS: A Complete, Rollback-Safe Setup Guide
The max-age ramp, subdomain audit and preload submission - in the order they won't burn you.
Loading...
The max-age ramp, subdomain audit and preload submission - in the order they won't burn you.
A browser's first-ever connection to your site is the weak link. If the user types example.com (no scheme), the browser sends HTTP. An attacker on the same network - hotel Wi-Fi, a compromised router, an ISP injecting ads - can answer before your server does, strip the 301 to HTTPS, and sit in the middle for the rest of the session. HSTS tells the browser 'never speak HTTP to this origin again.' Once cached, the browser upgrades internally before a single packet leaves the machine.
The preload list closes the remaining gap: the trust-on-first-use window. Browsers ship hard-coded with the preload list, so a user who has never visited your site still gets HTTPS-only behaviour from the first request.
Do not ship max-age=31536000 on day one. If your HTTPS setup is broken on any subdomain - expired cert, misconfigured listener, missing SNI - every browser that visits will refuse to load the site until the cache expires. HSTS has no opt-out for users.
Ramp the TTL up in stages. Each stage is a commitment: once a browser sees max-age=N, it will refuse HTTP for N seconds even if you remove the header.
includeSubDomains extends HSTS to every host under your apex - including ones you forgot about. An internal admin panel on admin.example.com that only speaks HTTP because it's behind a VPN will break. A legacy marketing microsite on promo.example.com with an expired cert will break. A subdomain that resolves but isn't yours (a dangling CNAME) can become an attack vector once someone registers the target.
Enumerate every subdomain, then test each over HTTPS.
# List from your DNS provider export, certificate transparency, and DNS brute force.
# Certificate Transparency is the best source for "forgotten" subdomains:
curl -s "https://crt.sh/?q=%.example.com&output=json" \
| jq -r '.[].name_value' \
| tr ',' '\n' \
| sed 's/\*\.//' \
| sort -u# Given a file subdomains.txt:
while read host; do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "https://$host")
echo "$host $code"
done < subdomains.txtEvery subdomain you want covered by HSTS must return a valid TLS response. Anything that does not needs a decision: fix it (issue a cert and serve HTTPS), retire it (remove the DNS record), or scope HSTS more narrowly (drop includeSubDomains and serve the header only on the hosts that work).
The header itself is trivial. The trick is making sure it is served on HTTPS responses only - browsers ignore HSTS served over HTTP - and that it survives every response path (redirects, error pages, CDN cache).
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# 'always' ensures the header is set on error responses too.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# ... your existing TLS and location blocks
}<VirtualHost *:443>
ServerName example.com
# 'always' applies the header to both successful and error responses.
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# ... your existing SSL and rewrite directives
</VirtualHost>// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
}
]
}
]
}SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS)
Enable HSTS: On
Max Age: 12 months
Apply HSTS to subdomains: On
Preload: On (only after Stage 5)
No-Sniff header: On// CloudFront response-headers-policy
{
"StrictTransportSecurity": {
"AccessControlMaxAgeSec": 31536000,
"IncludeSubdomains": true,
"Preload": true,
"Override": true
}
}The rollback for a cached but unsubmitted HSTS value is: remove the header, wait max-age seconds. Users whose caches have not expired will remain HSTS-protected until then. There is no way to invalidate the cache server-side.
Rollback for a preloaded domain is different: file a removal request at hstspreload.org, wait for the next Chromium release that omits you, and accept that a long tail of users on outdated browsers will stay HSTS-protected for the life of those installs.
No. The preload list requires includeSubDomains and preload both in the header. If your subdomains cannot all serve HTTPS, you cannot preload - this is by design.
If the SaaS serves valid HTTPS, fine. If it does not - or if it cannot serve the Strict-Transport-Security header at all - you still get HSTS protection from includeSubDomains because the protection is browser-side. But you cannot preload any domain whose subdomains cannot be validated.
Only if includeSubDomains is on the apex's response - and www is a subdomain of the apex, not the other way around. The clean pattern is: set HSTS on both the apex and www, with includeSubDomains on the apex.