HSTS (HTTP Strict Transport Security) is one of those headers that looks trivial on paper — a single line of config — but gets misconfigured constantly. A wrong max-age, a missing includeSubDomains, or a premature preload submission can lock users out of your site or leave you exposed to downgrade attacks. This post walks through enabling HSTS properly across the common server stacks, what each directive actually does, and how to verify it's working.
What HSTS Actually Does
HSTS tells browsers: for the next N seconds, only connect to this domain over HTTPS, no exceptions. Once a browser sees the header on a valid HTTPS response, it refuses to make plaintext HTTP requests to that host — even if the user types http:// or clicks an old link. This blocks SSL-stripping attacks on public Wi-Fi and protects against accidental mixed content.
The header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Three directives matter:
- max-age — How long (in seconds) the browser should remember the HTTPS-only rule. 31536000 = 1 year.
- includeSubDomains — Applies the rule to every subdomain. Powerful, but irreversible until
max-ageexpires. - preload — Signals you want the domain added to the browser-baked preload list, so HSTS applies on first visit too.
Before You Enable HSTS: A Pre-Flight Checklist
HSTS is sticky. If you push includeSubDomains with a one-year max-age and one of your subdomains is HTTP-only, you've just broken it for every returning visitor. Run through this list first:
- Every subdomain you care about (
www,api,cdn,mail, staging, etc.) serves valid HTTPS with a trusted certificate. - All HTTP traffic on those hosts already 301-redirects to HTTPS.
- No internal tools or admin panels rely on plain HTTP.
- Your TLS certificates auto-renew reliably (Let's Encrypt, ACM, etc.).
- You've tested with a short
max-agefirst — say 300 seconds — before committing to a year.
Configuration by Server
Nginx
Add the header inside your HTTPS server block. Use always so it's sent on error responses too:
server {
listen 443 ssl http2;
server_name example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
Reload with nginx -t && nginx -s reload.
Apache
Enable mod_headers first (a2enmod headers), then add inside your HTTPS VirtualHost:
<VirtualHost *:443>
ServerName example.com
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>
Caddy
Caddy enables HTTPS by default. Add the header in your Caddyfile:
example.com {
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
Cloudflare
If you're behind Cloudflare, set HSTS in the dashboard: SSL/TLS → Edge Certificates → HTTP Strict Transport Security. Enable it, set max-age to 12 months, toggle Include subdomains and Preload when ready. Cloudflare will inject the header for you — don't double up at the origin.
IIS
Add to web.config under system.webServer:
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
</customHeaders>
</httpProtocol>
Node.js (Express)
Use Helmet, which sets sensible defaults:
const helmet = require('helmet');
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
The Rollout: Don't Go Straight to a Year
Even with the checklist passed, ramp up gradually. The recommended pattern:
- Week 1:
max-age=300(5 minutes), noincludeSubDomains. Confirm nothing breaks. - Week 2:
max-age=86400(1 day). Watch error logs and analytics for any HTTP-only resources failing. - Week 3:
max-age=2592000(30 days), addincludeSubDomainsif all subdomains are clean. - Week 4+:
max-age=31536000(1 year). Addpreloadonly when you're confident.
Getting on the HSTS Preload List
Browsers ship a hardcoded list of domains that get HSTS treatment on the very first visit — no header request needed. To submit yours at hstspreload.org, your site must:
- Serve a valid certificate.
- Redirect HTTP to HTTPS on the same host.
- Serve all subdomains over HTTPS (including
www). - Send the HSTS header on the base domain with
max-ageof at least 31536000,includeSubDomains, andpreload.
Think twice before submitting. Removal from the preload list takes months and rolls out only with new browser versions. If your business might ever need an HTTP subdomain, skip the preload directive.
Verifying It Works
Test with curl:
curl -I https://example.com | grep -i strict
You should see your header echoed back. For a deeper check, browsers expose the HSTS cache:
- Chrome:
chrome://net-internals/#hsts— query a domain to see its policy. - Firefox: Check the Network tab response headers.
For a one-shot audit across HSTS plus the rest of your security headers, CSP, TLS config, cookies and DNS, run the domain through WebSentry. It'll flag a missing includeSubDomains, a too-short max-age, or whether you're actually preload-eligible — and surface the related issues (mixed content, weak ciphers, missing Secure cookie flags) that often sit alongside an incomplete HSTS rollout.
Common Mistakes That Bite Later
- Setting HSTS over HTTP. Browsers ignore it. The header only counts on HTTPS responses.
- Forgetting
alwaysin Nginx. Without it, the header is skipped on 4xx/5xx responses. - Enabling
includeSubDomainswith a forgotten HTTP subdomain. Classic outage.legacy.example.comover plain HTTP suddenly stops loading. - Preloading too early. If you submit then realise
api.example.comneeds HTTP for a webhook callback, you're stuck waiting for the next browser release cycle. - Double headers from CDN + origin. Conflicting values cause unpredictable behaviour. Pick one place to set it.
Once HSTS is live and tested, scan your domain at websentry.dev for a free security grade — it'll confirm your HSTS policy is configured correctly and catch anything else weakening your HTTPS setup.
Check your own site
Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.