Remediation guide
How to Enable the HSTS Header (Strict-Transport-Security)
HardenCheck flagged your site for a missing or weak Strict-Transport-Security header. This guide explains what HSTS does that an HTTPS redirect can't, walks you through the four-step deployment process — including the critical warnings about includeSubDomains and preload — and gives you the exact snippet for every major hosting platform.
preload. The HSTS preload list is very hard to undo — removal takes 3–6 months and cannot be expedited. Read the preload section in full before adding that directive.
- Why HSTS matters
- The header explained
- Step 1: Test with short max-age
- Step 2: Set one year
- Step 3: includeSubDomains
- Step 4: Preload (read first)
- Deploy on your platform
- What HSTS doesn't cover
- Verify with HardenCheck
- FAQ
What HSTS does — and why an HTTPS redirect alone isn’t enough
When a user types yoursite.com into a browser, the first request goes out over HTTP — not HTTPS. The browser has no way to know HTTPS exists until it makes that initial connection and your server responds. An attacker sitting between the user and your server (on shared Wi-Fi, a corporate proxy, or a compromised router) can intercept that unencrypted first request and respond themselves, serving a fake version of your site while the real HTTPS connection never happens. This attack is called SSL stripping, and the tool that demonstrated it publicly — sslstrip — still works on any site that relies on HTTP→HTTPS redirects alone.
A redirect from http://yoursite.com to https://yoursite.com does not fix this. The redirect response itself travels over the unencrypted HTTP connection and can be intercepted and suppressed — the browser never gets told to switch.
Strict-Transport-Security fixes the problem at the browser level. The first time a user visits your HTTPS site with an HSTS header present, the browser records the directive and refuses to make HTTP connections to that domain for the duration of max-age. Every subsequent visit — even if the user manually types http:// — is automatically upgraded to HTTPS before any network packet leaves the device. There is no unencrypted request for an attacker to intercept.
The header and what each part means
The full production HSTS header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| Directive | What it does | Required? |
|---|---|---|
max-age=N |
How many seconds the browser remembers and enforces HSTS after the last time it received this header. 31,536,000 = one year. | Yes |
includeSubDomains |
Extends the policy to every subdomain of your domain (api., staging., etc.). Only safe when all subdomains serve HTTPS. See Step 3. |
No (recommended) |
preload |
Marks the domain for submission to the browser preload list — baking HSTS in before any first visit. Requires includeSubDomains and max-age ≥ 31536000. Very hard to undo. See Step 4. |
No |
Start with just max-age. Add the other directives only after you understand their implications.
Step 1: Test with a short max-age first
Before committing to a long HSTS window, deploy the header with a 5-minute max-age. This keeps any mistake quickly reversible:
Strict-Transport-Security: max-age=300
With this in place, verify over the next few days that:
- Every page, asset, font, and API endpoint loads over HTTPS
- HTTP→HTTPS redirects work correctly on your platform
- Your browser console shows no mixed-content warnings (resources loading over HTTP on an HTTPS page)
- Every subdomain you use is accessible over HTTPS (important before Step 3)
If something breaks, the 5-minute window means the fix doesn’t require clearing browser state across your entire user base. Once everything checks out, proceed to Step 2.
Step 2: Set max-age to one year
Once verified, raise max-age to 31,536,000 — one year. This is the value HardenCheck and the majority of security scanners require for a passing grade:
Strict-Transport-Security: max-age=31536000
The timer refreshes on every visit. A user who visits regularly has a rolling one-year window — the countdown only starts from their most recent visit. Users who visited once and never returned will stop enforcing HSTS after one year has passed.
Step 3: Decide about includeSubDomains
includeSubDomains extends HSTS to every subdomain under your domain. This closes a real gap: without it, an attacker can serve content on a subdomain over HTTP and potentially capture session cookies that were not scoped correctly to the root domain alone.
Before you add it, audit every subdomain you use:
api.yoursite.com— serving over HTTPS?staging.yoursite.com— serving over HTTPS?cdn.yoursite.comor any asset subdomain — HTTPS?- Any CI preview URLs built on your root domain?
- Any legacy subdomains still on HTTP?
If any subdomain is HTTP-only — even an internal or rarely-used one — adding includeSubDomains will lock browsers out of it. They will refuse the HTTP connection, the HTTPS version may not exist, and users get a hard error with no fallback or workaround.
Many teams solve this by running staging environments on a separate domain (e.g. yoursite-staging.com) or using the hosting platform’s own subdomain for deploy previews — Netlify’s *.netlify.app and Vercel’s *.vercel.app preview URLs are under those platforms’ domains, not yours, and are therefore unaffected by your HSTS policy.
Once all subdomains are HTTPS-ready:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Step 4: Understand preload before you use it
The HSTS preload list is a database maintained by Google and shipped inside Chrome, Firefox, Safari, and Edge. Domains on the list have HSTS enforced from the very first connection — even before the browser has ever visited the site and received a header. This closes the remaining gap: a brand-new visitor has no HSTS entry cached, and that first visit is still vulnerable to SSL stripping. The preload list eliminates that window entirely.
Requirements for preload list submission (verified at hstspreload.org):
- A valid TLS certificate on the HTTPS site
- HTTP→HTTPS redirect on port 80
- An HSTS header on the HTTPS response with
max-ageof at least 31,536,000 includeSubDomainsmust be presentpreloadmust be present
If you meet all requirements, submit the domain at hstspreload.org. Acceptance typically takes a few weeks; browser adoption varies by release cycle.
preload if you are committed to this domain being on HTTPS permanently.
The full preload-ready header:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Deploy it on your platform
nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache (.htaccess)
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>
Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
}
]
}
]
}
Next.js (next.config.js)
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains'
}
];
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
Netlify (_headers file)
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
Cloudflare Pages (_headers file)
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
max-age value GitHub is currently setting meets the minimum recommended threshold.
What HSTS doesn’t cover
HSTS forces the connection to use HTTPS; it does not make HTTPS itself stronger. If your TLS certificate has expired, uses a deprecated cipher suite, or is issued by an untrusted certificate authority, HSTS won’t help — and it correctly won’t fall back to HTTP either. The browser shows a hard certificate error instead. This is the right behaviour (falling back to HTTP is precisely what HSTS prevents), but it means you need to keep your certificate valid independently of HSTS being set. Most managed hosting platforms handle certificate renewal automatically; on a self-managed server, certbot renew in a daily cron job handles it.
HSTS also does not protect the content of your pages once the encrypted connection is established. Cross-site scripting, resource injection, and clickjacking are separate threats that a Content-Security-Policy header addresses. The two headers are complementary: HSTS secures the transport; CSP secures what executes inside it. See the CSP guide →
Verify with HardenCheck
Once you’ve deployed your HSTS header — or while you’re still testing with a short max-age — paste your raw response headers into HardenCheck. It checks whether Strict-Transport-Security is present, whether max-age meets the recommended minimum, and whether includeSubDomains is set. You can iterate on the value by editing it directly in the paste box and watching the grade update in real time, without touching your server.
Grade my headers with HardenCheck →
Frequently asked questions
What happens if my TLS certificate expires while HSTS is active?
The browser shows a hard certificate error — not a “proceed anyway” prompt, and not a fallback to HTTP. This is correct: HSTS exists specifically to prevent HTTP fallback. The answer is to renew your certificate, not to remove HSTS. Most hosting platforms (Vercel, Netlify, Cloudflare, GitHub Pages) renew Let’s Encrypt certificates automatically. On a self-managed server, certbot renew in a daily cron job handles this. The important takeaway: keep your certificate in good standing independently of whether HSTS is set.
Can I remove HSTS? What does max-age=0 do?
Serving Strict-Transport-Security: max-age=0 over HTTPS clears the cached directive in any browser that receives it. But this header must arrive over HTTPS — if you’ve lost HTTPS access entirely, you cannot serve the opt-out and users with a cached entry are stuck until the original max-age expires. This is why testing with max-age=300 first matters so much: a 5-minute window means any early mistake self-corrects quickly. If you are on the preload list, max-age=0 alone will not remove you — you must submit a removal request at hstspreload.org and wait for the browser release cycle to propagate it, typically 3–6 months.
Does HSTS protect against all man-in-the-middle attacks?
HSTS eliminates HTTP-downgrade attacks (SSL stripping) by ensuring all connections use HTTPS. But HTTPS itself relies on certificate infrastructure: if an attacker can obtain a valid certificate for your domain through a compromised certificate authority, they can still intercept HTTPS traffic. A CAA DNS record restricts which CAs are allowed to issue certificates for your domain, and Certificate Transparency logs make unauthorised certificates publicly detectable within hours. HSTS preloading reduces the first-visit window further. None of these measures replaces well-managed certificate and key infrastructure, but together they significantly narrow the remaining attack surface.
What’s the difference between HSTS and HTTPS?
HTTPS is the encrypted transport protocol (TLS) that protects data as it travels between browser and server — it’s what makes the connection private and tamper-evident. HSTS is a response header that tells the browser to always enforce HTTPS, removing the ability to make an HTTP connection to the domain even if the user manually types http://. You need both: HTTPS does the encryption; HSTS ensures the browser never accidentally bypasses it.
Does includeSubDomains affect my staging or API subdomains?
Yes — every subdomain of your domain, including staging.yoursite.com, api.yoursite.com, and any CI preview URLs built on your root domain, must serve valid HTTPS before you add includeSubDomains. Many teams handle this by running staging on a separate domain entirely (e.g. yoursite-staging.com) or using a platform subdomain for deploy previews — Netlify’s deploy-preview--xyz.netlify.app URLs and Vercel preview URLs on *.vercel.app are under those platforms’ own domains, not yours, so they are not affected by your HSTS policy.
Is there a downside to a very long max-age?
The tradeoff is recovery time. With a one-year max-age, any browser that visited your site in the past year will continue enforcing HTTPS for up to another year after you remove the header — you cannot reach those browsers to send a max-age=0 opt-out until they visit again. For production sites that will remain on HTTPS permanently (which is almost all of them) this is the right choice. For a site you are actively experimenting with, start with max-age=300, verify everything works over several days, then escalate to a year.
Also in the Copper Bay Labs ship-safety suite
HSTS secures your transport layer — these tools and guides cover the rest:
Write a Content-Security-Policy that blocks XSS without breaking your site — with report-only mode, nonces, hashes, and platform snippets.
Secret scanning LeakCheckPaste code or a config file and see exposed API keys and secrets flagged before they reach the repo.
Post-deploy ExposureCheckScan your live URL for exposed .env files, .git directories, source maps, and bundled secrets.
Paste your package.json and see vulnerable, abandoned, typosquatted, and risky-license packages flagged by severity.
Check your site for WCAG accessibility failures and privacy-risk patterns before they become liabilities.