HSTS Preload: A Safe Rollout Without Subdomain Lockout
DEV Community

HSTS Preload: A Safe Rollout Without Subdomain Lockout

HSTS is the one security header that can take part of your domain offline for a year if you get it wrong.

A Strict-Transport-Security header with includeSubDomains and preload, submitted to the browser preload list, tells every major browser to refuse plain HTTP for your domain and every subdomain under it. That is exactly what you want for a login page, and exactly what breaks the forgotten http:// printer panel, the staging box with an expired certificate, or a www host whose certificate never covered www.

I build PixelValut, a set of free browser-side security tools. I wrote this with help from AI tools and checked every command, output and claim myself. Below is what preloading really commits you to, real headers and API responses I pulled on Nov 14, 2026, and the staged rollout I use so nothing gets locked out.

What is HSTS preload and should you enable it?

Direct answer: HSTS preload puts your domain on a list built into browsers, so they use HTTPS for it and every subdomain before the first visit, closing the first-request downgrade gap. Enable it only after every subdomain serves valid HTTPS, you have run a long max-age with includeSubDomains, and you accept that removal takes months.

What HSTS fixes, and the gap preload closes

Without HSTS, a user who types example.com sends a first request over plain HTTP. Your server answers with a redirect to HTTPS, but that first hop can be intercepted on hostile Wi-Fi. A classic SSL-stripping proxy keeps the victim on HTTP and talks HTTPS to you on their behalf. Session cookies without the Secure flag leak along the way (I covered that side in auditing Secure, HttpOnly and SameSite cookie flags).

A normal HSTS header fixes every visit after the first one. Once the browser sees the header over a valid HTTPS response, it rewrites http:// to https:// internally for max-age seconds and refuses to let the user click through certificate errors.

The remaining hole is the very first visit, or the first visit after the policy expires. Preloading closes it, because the browser ships with your domain already marked HTTPS-only.

Real headers from production sites

Here is what a plain curl -sI returned on Nov 14, 2026:

$ curl -sI https://github.com | grep -i strict-transport
strict-transport-security: max-age=31536000 ; includeSubdomains ; preload

$ curl -sI https://www.paypal.com | grep -iE "^HTTP|strict-transport"
HTTP/2 302
strict-transport-security: max-age=63072000 ; includeSubDomains ; preload

$ curl -sI https://www.cloudflare.com | grep -i strict-transport
strict-transport-security: max-age=31536000 ; includeSubDomains

Three things stand out:

  • GitHub uses one year (31,536,000 seconds) plus both directives. That is the minimum the preload list accepts.
  • PayPal sends the header on a 302 redirect, not just on the final page. That matters: browsers only honor HSTS on HTTPS responses, and a redirect response counts, so set it on every response including redirects.
  • Cloudflare's marketing site covers subdomains but does not ask for preload. Strong HSTS without preload is a legitimate choice, not a mistake.

You can ask the preload list itself where a domain stands:

$ curl -s "https://hstspreload.org/api/v2/status?domain=github.com"
{ "name" : "github.com" , "status" : "preloaded" , "bulk" : true, "preloadedDomain" : "github.com" }

$ curl -s "https://hstspreload.org/api/v2/status?domain=paypal.com"
{ "name" : "paypal.com" , "status" : "preloaded" , "bulk" : false, "preloadedDomain" : "paypal.com" }

The lockout scenario, with a real error

The preloadable check is the most useful pre-flight test I know. I ran it against one of my own domains that already sends a long max-age, and it caught a real lockout risk before I had even added preload:

$ curl -s "https://hstspreload.org/api/v2/preloadable?domain=<my-domain>"
"code" : "header.preloadable.include_sub_domains.missing"
"code" : "header.preloadable.preload.missing"
"code" : "domain.www.no_tls" , "message" : "The www subdomain exists, but we couldn't connect to it using HTTPS (\"tls: failed to verify certificate: x509: certificate is valid for <my-domain>, not www.<my-domain>\"). Since many people type this by habit, HSTS preloading would likely cause issues for your site."

Translate that into user impact. If I had added includeSubDomains; preload and submitted, anyone typing www. would hit a hard certificate error with no "proceed anyway" button, in every major browser, until the certificate was fixed. The preload checker refuses the submission in this state, but plain includeSubDomains does not: the header alone would already break www for every returning visitor.

The same pattern hits subdomains people forget:

  • mail., vpn. or printer. hosts on an internal DNS zone under the public domain
  • old staging hosts with a self-signed or expired certificate
  • vendor CNAMEs (help desks, status pages) where the vendor's certificate does not list your name
  • http:// only IoT dashboards on the office network

HSTS stages compared

Stage Header First-visit protection Lockout risk How to undo
None (no header) None None Nothing to undo Probe
max-age=300 None Very low, 5-minute cache Wait 5 minutes Host only
max-age=31536000 None Only the apex/host Send max-age=0 to returning users Subdomains
max-age=31536000; includeSubDomains None Every subdomain, for returning users Send max-age=0 over valid HTTPS Preloaded
max-age=31536000; includeSubDomains; preload + submitted Yes Every subdomain, for every user Request removal; ships with browser updates over months

The key column is the last one. Every stage before preload can be rolled back by your own server. Preload can only be rolled back by browser releases.

The staged rollout I use

Step 1: Inventory every name. Pull subdomains from your DNS zone, your certificate history in CT logs (the CAA and CT logs guide shows how), and your load balancer config. Then test each one:

while read h ; do code = $( curl -s -o /dev/null -w '%{http_code}' --max-time 8 "https://$h/" ) echo "$h https=$code" done < subdomains.txt

Anything that returns 000 (no TLS connection) is a blocker.

Step 2: Redirect HTTP to HTTPS on the same host first. http://example.com should go to https://example.com, not straight to https://www.example.com. The preload checker expects the HTTPS hop on the same host so the header is seen for the apex.

Step 3: Ramp max-age. In Nginx:

server { listen 443 ssl ; server_name example . com www . example . com ; # week 1: 300, week 2: 604800, week 4: 2592000, then 31536000 add_header Strict - Transport - Security "max-age=604800; includeSubDomains" always ; }

Use always, or Nginx drops the header on 4xx and 5xx responses. Watch for the classic Nginx trap: an add_header inside a location block replaces every header inherited from the server block, so a single add_header Cache-Control in one location silently removes HSTS there.

Step 4: Verify from outside. Check the live header on the apex, www, a redirect URL and an error page. In the browser, the Security Headers Checker shows the parsed max-age and directives without installing anything.

Step 5: Run at one year with includeSubDomains for a few weeks. Watch support tickets and monitoring for certificate errors on anything internal.

Step 6: Add preload, re-run the preloadable API, then submit at hstspreload.org. Inclusion arrives with normal browser releases, not instantly.

Preload readiness checklist

  • [ ] Every subdomain in DNS answers HTTPS with a valid, publicly trusted certificate
  • [ ] www exists only if its certificate covers it
  • [ ] Internal-only names live on a separate domain, not under the public one
  • [ ] HTTP redirects to HTTPS on the same host before any cross-host redirect
  • [ ] Header sent with always on redirects and error pages
  • [ ] No location block drops the header through add_header inheritance
  • [ ] Ran max-age=31536000; includeSubDomains in production for weeks with no incidents
  • [ ] The preloadable API returns no errors or warnings
  • [ ] The team accepts that removal takes months

Failure modes I see

Header only on the app, not the CDN edge. The origin sends HSTS, but the CDN serves cached redirects without it.

includeSubDomains added during an incident. Someone copies a "secure headers" snippet, and a week later the VPN portal on a self-signed certificate stops working.

Assuming max-age=0 fixes preload. It clears the dynamic policy, not the built-in list.

Shared parent domains. Preloading company.com also covers team.company.com that another department runs.

Frequently asked questions

Is HSTS preload required for a secure site?

No. A one-year max-age with includeSubDomains protects returning visitors. Preload only adds first-visit protection, at the cost of a slow exit. Banks, login providers and high-value targets benefit most.

What max-age does the preload list require?

At least 31,536,000 seconds (one year), plus includeSubDomains and preload, served on the base domain over HTTPS.

Can I preload a subdomain only?

No. Submissions are for the registrable domain, and the policy covers all of its subdomains. If one area cannot do HTTPS, move it to a different domain first.

How long does removal from the preload list take?

Removal requests are processed and then shipped in browser updates, so expect months before most users stop enforcing it. Plan as if it is permanent.

Does HSTS work over plain HTTP responses?

No. Browsers ignore the header on HTTP responses, which is why the first redirect must land on HTTPS on the same host.

Do entire TLDs use preload?

Yes. Some TLDs, such as .dev and .app, are preloaded as a whole, so every site on them must use HTTPS from day one.

Follow & Connect

YouTube: PixelValut (@pixel-valut)

TikTok: PixelValut (@pixelvalut007)

GitHub: Muhammad Ali Mughal (@ali111887)

LinkedIn: Muhammad Ali Mughal

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.