How to set up HSTS without locking yourself out
A staged, reversible rollout plan for Strict-Transport-Security that improves privacy without turning one bad certificate into an outage.
Table of contents
- HSTS is simple until it is not
- What the HSTS header actually does
- The lockout scenarios to avoid
- 1. A forgotten subdomain is not HTTPS-ready
- 2. A certificate expires
- 3. Staging or internal tools live under the production domain
- 4. Preload is treated as a routine checkbox
- A safe rollout plan
- Step 1: Audit every hostname you control
- Step 2: Fix HTTPS before adding HSTS
- Step 3: Start with a very short max-age
- Step 4: Increase gradually
- Step 5: Add includeSubDomains only after the audit is real
- Step 6: Treat preload as a separate project
- Configuration examples
- Nginx
- Apache
- CDN or edge platform
- How to undo HSTS safely
- Testing checklist before you ship
- The privacy case for HSTS
HSTS is simple until it is not
HTTP Strict Transport Security, usually shortened to HSTS, tells browsers: “for this site, always use HTTPS.” Once a browser receives the header over a valid HTTPS connection, it remembers the rule for the duration you specify.
That is useful. It prevents protocol downgrade attacks, reduces accidental insecure requests, and avoids the awkward moment where a user types example.com and briefly touches plain HTTP before being redirected.
It is also sticky. If you publish the wrong HSTS policy, browsers may keep enforcing it long after you remove the header from your server. That is how teams lock themselves out: not from their own admin panel exactly, but from users’ browsers, subdomains, staging systems, legacy endpoints, and forgotten services that are not ready for forced HTTPS.
The goal is not to avoid HSTS. The goal is to deploy it like a migration, not like a toggle.
What the HSTS header actually does
A typical HSTS header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
It has three important parts:
max-age: how long, in seconds, the browser should enforce HTTPS for this host.includeSubDomains: whether the rule also applies to every subdomain.preload: a signal that you want the domain included in browser preload lists.
The browser only trusts this header when it receives it over valid HTTPS. If the certificate is invalid, expired, or mismatched, the browser should not accept a new HSTS policy from that response.
Once the policy is stored, future attempts to visit http://example.com are upgraded by the browser to https://example.com before the request is sent. That is the privacy win: the insecure request never leaves the device.
The lockout scenarios to avoid
Most HSTS failures are not caused by the main website. They happen at the edges.
1. A forgotten subdomain is not HTTPS-ready
includeSubDomains sounds tidy, but it is absolute. If you set it on example.com, it applies to:
www.example.comapi.example.comold-crm.example.comprinter-setup.example.comstaging.example.com- anything else under that domain
If any of those hosts cannot serve valid HTTPS, users with the HSTS policy cached will be unable to reach them over HTTP.
2. A certificate expires
Without HSTS, users sometimes click through certificate warnings. That is not good security practice, but it happens.
With HSTS, modern browsers do not allow easy bypass of certificate errors for that host. This is the point. It also means certificate renewal needs to be boring, monitored, and tested.
3. Staging or internal tools live under the production domain
Putting internal tools under *.example.com can become painful once the parent domain uses includeSubDomains. If those tools use self-signed certificates, private certificate authorities, old TLS configurations, or no HTTPS at all, HSTS will expose the shortcut.
This is one reason many teams keep internal and experimental systems under a separate domain that has its own security policy.
4. Preload is treated as a routine checkbox
HSTS preload is not just another directive. It means your domain can be shipped inside browsers as HTTPS-only before any user has visited your site.
That closes the “first visit” gap, but it is much harder to undo. Removal from preload lists can take weeks or months to reach users, depending on browser release cycles. Preload is appropriate for stable, mature domains. It is not appropriate for a site that is still discovering its subdomain inventory.
A safe rollout plan
Step 1: Audit every hostname you control
Before setting includeSubDomains, list every hostname under the domain. DNS records are a start, but not the whole story. Check CDN configurations, hosting dashboards, email-related hostnames, old marketing tools, storage buckets, and internal documentation.
For each hostname, answer:
- Does it serve HTTP, HTTPS, or both?
- Is the HTTPS certificate valid and automatically renewed?
- Does it redirect HTTP to HTTPS cleanly?
- Is it meant to be public?
- Is it still needed?
If your team already has production header debugging habits, this fits naturally beside redirect and header checks. We covered that workflow in a small toolkit for debugging redirects and HTTP headers in production.
Step 2: Fix HTTPS before adding HSTS
HSTS does not make a broken HTTPS setup secure. It only makes HTTPS mandatory.
Before enabling it, verify:
- TLS certificates cover the correct hostnames.
- Certificates renew automatically.
- HTTP redirects to HTTPS with a single clean hop where possible.
- Canonical host redirects are consistent, for example non-
wwwtowww, or the reverse. - Application assets do not depend on insecure
http://URLs.
Mixed content is less common than it used to be, but it still appears in old CMS themes, analytics snippets, embedded media, and hard-coded image paths.
Step 3: Start with a very short max-age
Do not begin with one year. Begin with five minutes:
Strict-Transport-Security: max-age=300
Deploy that only on the hostname you are testing, usually the canonical production website. Leave out includeSubDomains for now.
Then test in real browsers and with command-line requests:
curl -I https://example.com
You should see exactly one Strict-Transport-Security header. Duplicate HSTS headers from an app server and a CDN are a common source of confusion. Browsers generally apply the effective policy, but humans debugging an incident do not need ambiguity.
Step 4: Increase gradually
If nothing breaks, increase the duration in stages:
Strict-Transport-Security: max-age=86400
Then:
Strict-Transport-Security: max-age=604800
Then perhaps:
Strict-Transport-Security: max-age=2592000
A practical schedule is:
- 5 minutes
- 1 day
- 1 week
- 1 month
- 6 months or 1 year
There is no prize for rushing. The whole point of staged rollout is to give your monitoring, support inbox, and edge cases time to tell you what your checklist missed.
Step 5: Add includeSubDomains only after the audit is real
Once every public subdomain is HTTPS-ready, you can consider:
Strict-Transport-Security: max-age=31536000; includeSubDomains
This is the moment to be conservative. If one legacy service still needs HTTP, do not add includeSubDomains to the parent domain. Either migrate that service, move it to another domain, or accept that your HSTS policy must stay narrower for now.
Security headers should reflect reality. They should not be used as motivational posters for infrastructure you hope to have later.
Step 6: Treat preload as a separate project
Only consider preload when all of the following are true:
- The domain and all subdomains support valid HTTPS.
- HTTP redirects to HTTPS.
- The HSTS header uses
max-ageof at least 31536000 seconds. - The header includes
includeSubDomains. - The header includes
preload. - You are confident you will not need plain HTTP anywhere under the domain.
A preload-ready header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Submitting to the preload list is a long-term commitment. If the site is a campaign microsite, a temporary product domain, or a domain with unclear ownership boundaries, skip it.
Configuration examples
Nginx
Use always so the header is sent on error responses as well:
add_header Strict-Transport-Security "max-age=300" always;
After the rollout is stable:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache
With mod_headers enabled:
Header always set Strict-Transport-Security "max-age=300"
Later:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
CDN or edge platform
If your CDN sets response headers, prefer managing HSTS in one place. Do not set one policy at the origin and another at the edge unless you have a very clear reason.
Also check whether the CDN applies headers to redirects, cached errors, and custom error pages. A production site is not only its 200 OK response.
How to undo HSTS safely
If you need to disable HSTS, send:
Strict-Transport-Security: max-age=0
But there is a catch: the browser must successfully reach the site over valid HTTPS to receive that header. If HTTPS itself is broken, users with a cached HSTS policy cannot fetch the instruction that would clear it.
So the usual recovery order is:
- Restore valid HTTPS.
- Serve
Strict-Transport-Security: max-age=0. - Keep it in place long enough for returning users to receive it.
- Remove or replace the header after the incident is resolved.
If the domain is preloaded, serving max-age=0 is not enough for new browser profiles. You also need to request removal from the preload list and wait for that change to ship through browser updates.
Testing checklist before you ship
Use this checklist before increasing max-age or adding includeSubDomains:
- The canonical HTTPS URL returns a valid certificate.
- HTTP redirects to HTTPS.
- There is only one HSTS header.
- The header appears on redirects and error responses where appropriate.
- All public subdomains have valid HTTPS.
- Certificate renewal is monitored.
- No critical internal system depends on HTTP under the same parent domain.
- Preload has been discussed explicitly, not added by habit.
Lighthouse may also flag missing or weak security headers in some contexts, but it should not be your only verification method. If you use it as part of a wider review, read the findings as signals rather than verdicts; the same mindset applies when you read a Lighthouse report without panicking.
<!-- tool-cta:start -->
💡 Try this: Before and after each HSTS change, inspect the Strict-Transport-Security response with Get Headers to confirm max-age, includeSubDomains and preload are what you expect.
<!-- tool-cta:end -->
The privacy case for HSTS
HSTS is often framed as a security header, and it is. It also has a privacy benefit: it reduces the chance that a user’s first request leaks over plain HTTP on an untrusted network.
That matters on airport Wi-Fi, hotel networks, corporate guest networks, and anywhere a user’s traffic might be observed or modified. A plain HTTP request can expose the hostname, path, cookies without the Secure flag, and other request details. HTTPS is not magic, but forcing it consistently removes an entire class of avoidable leakage.
The best HSTS deployments are uninteresting. They are rolled out slowly, backed by reliable certificates, and boring enough that nobody notices. That is exactly what you want from a header whose failure mode can be dramatic.