HSTS: force https on the first visit, not the second
Strict-Transport-Security closes the window where a first visit gets silently downgraded to http
The threat
You redirect http to https, so you assume you are covered. You are not, on the first visit. When someone types your domain, or clicks an old http:// link, their browser’s very first request goes out over plain http, before your redirect ever runs. On shared or public wifi that first request is the whole game: someone on the same network answers it, keeps the visitor on http, and reads or rewrites everything from there. This is SSL stripping, and it runs from a point-and-click tool.
Strict-Transport-Security closes the window. It tells the browser one thing: for this domain, never use http again, for the next two years. After a visitor has seen the header once, their browser silently upgrades every future request to https on its own, before anything touches the network. preload closes even the first-ever visit by shipping your domain inside the browser itself.
The exact fix
One header. On Netlify, add it to netlify.toml:
# netlify.toml
[[headers]]
for = "/*"
[headers.values]
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
Or the _headers form:
/*
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
What each part does:
max-age=63072000: two years, in seconds. This is the value browsers expect for preload. Do not ship a short “test” value and forget it, a long max-age is the entire point.includeSubDomains: coverswww,app,mail, every subdomain. Required for preload.preload: your opt-in to the browser’s built-in HSTSHSTSHTTP headerHTTP Strict Transport Security: a header that forces browsers to always use HTTPS for your domain, defeating downgrade and SSL-strip attacks. list, which is what protects the genuine first visit.
Before you add preload, three things must be true, or you can lock users out of a subdomain you forgot about:
- A valid TLS certificate on the root domain and every subdomain.
- http redirects to https (you almost certainly have this already).
- The header is served on the root domain over https.
Once it has been live for a day, submit the domain once at hstspreload.org. That is the only step that reaches past your own config, and it is a one-time submission.
Want a cautious rollout? Ship
max-age=300; includeSubDomainsfirst (five minutes of coverage), confirm nothing on any subdomain breaks, then raise it to63072000and addpreload. Skip this only if you already know every subdomain is https-clean.
Verify it
curl -sI https://yourdomain.com | grep -i strict-transport
You want Strict-Transport-Security: max-age=63072000; includeSubDomains; preload to come back on an https response. That is the whole check.
Proof
apecyber.com serves this exact header on every response and is submitted for preload, so a browser that has never seen the site still refuses to load it over http. Five minutes of config, a clean −10 off the posture score, and the first-visit downgrade simply stops existing.