What is HSTS?
HTTP Strict Transport Security is a policy mechanism that instructs browsers to only connect to your site over HTTPS for a specified duration. Once a browser receives a valid HSTS header, it:
- Refuses all future HTTP connections to the same host — with no network request made
- Automatically upgrades
http://links tohttps://before sending them - Ignores certificate errors — the browser will show a hard error page instead of the "proceed anyway" option
The header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadThe attack HSTS prevents: SSL stripping
Without HSTS, the first visit to a site exposes a window of vulnerability. Here's what happens without HSTS when a user on a coffee shop Wi-Fi visits your site:
- User types
example.cominto the browser - Browser sends a plain HTTP request:
GET http://example.com/ - An attacker on the same network intercepts this before it reaches your server
- Attacker proxies the connection — serving the user over HTTP, connecting to your HTTPS server themselves
- User sees your site. Their browser thinks it's talking to
http://example.com— no padlock - Every form submission, cookie, session token is visible to the attacker in plaintext
This is an SSL stripping attack. It works because the browser's initial request is HTTP — the HTTPS redirect never has a chance to fire.
HSTS eliminates this attack by ensuring the browser never sends the initial HTTP request at all.
Directive breakdown
max-age
How long (in seconds) the browser remembers the HSTS policy:
Strict-Transport-Security: max-age=31536000
# 31536000 seconds = 1 yearMinimum recommended: 6 months (15768000)
Recommended: 1 year (31536000)
HSTS preload requirement: 1 year minimum
Setting a short max-age (under a month) provides little protection — the policy may expire before a user's next visit, and the window of vulnerability reopens.
Setting max-age=0 instructs the browser to delete the cached HSTS policy. Use this only when intentionally removing HSTS.
includeSubDomains
Extends the HSTS policy to all subdomains:
Strict-Transport-Security: max-age=31536000; includeSubDomainsWithout includeSubDomains:
secure.example.com→ protected ✓login.example.com→ not protected ✗api.example.com→ not protected ✗
An attacker can still SSL-strip connections to any subdomain not covered by HSTS, then set cookies on the parent domain (.example.com) that are sent with subsequent requests to your protected site.
Before adding includeSubDomains, verify that every subdomain serves valid HTTPS. A subdomain that only serves HTTP will become unreachable — the browser will refuse to connect over HTTP, but HTTPS won't work either.
preload
The preload directive signals that you want your domain included in browser preload lists. Preloaded domains are hard-coded into browser source code as requiring HTTPS — HSTS protection applies from the very first visit, before any HSTS header has been received.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadAdding the directive alone doesn't add you to the preload list — you must also submit at hstspreload.org.
How HSTS preload lists work
The browser HSTS preload list (hstspreload.org) is maintained by Google and shipped with Chrome, Firefox, Safari, and Edge. The list contains domains that have opted into HSTS preloading.
Why preload matters: HSTS only protects from the second visit onwards — the first visit has no prior HSTS record, so the browser might send an HTTP request. Preloading eliminates this initial window.
Preload eligibility requirements
- Serve a valid HTTPS response on the bare domain and all subdomains
- Redirect all HTTP traffic to HTTPS
- Set HSTS on the HTTPS response with:
max-ageof at least 31536000 (1 year)includeSubDomainspresentpreloadpresent
- All subdomains accessible over HTTPS
Preload removal is slow
Removing a domain from the preload list takes months — browsers ship the list with each release, and users may be running older versions. Only submit to preload if you are committed to HTTPS-only for the foreseeable future.
If you need to move a subdomain back to HTTP, you cannot — includeSubDomains prevents the browser from even attempting an HTTP connection.
Implementation by stack
nginx
# In your HTTPS server block
server {
listen 443 ssl;
server_name example.com www.example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Also redirect HTTP to HTTPS
# ...
}
# HTTP redirect block
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}The always parameter ensures the header is set on all responses, including error pages.
Apache
# In your VirtualHost (HTTPS only)
<VirtualHost *:443>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>
# HTTP redirect
<VirtualHost *:80>
Redirect permanent / https://example.com/
</VirtualHost>Next.js
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
],
},
];
},
};
export default nextConfig;Express / Node.js
const helmet = require('helmet');
app.use(
helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true,
})
);Vercel
In vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
}
]
}
]
}Vercel also enforces HTTPS at the platform level, but setting the header explicitly ensures it's present in scan results and compliance checks.
Cloudflare
Cloudflare's "Always Use HTTPS" and "HSTS" settings (under SSL/TLS → Edge Certificates) configure HSTS at the CDN edge. Enable it there, but also set the header at your origin for defense in depth — if a request bypasses Cloudflare, HSTS should still be present.
Common mistakes
Sending HSTS over HTTP: the header is silently ignored on plain HTTP responses. HSTS only takes effect when received over a valid HTTPS connection.
Setting on a subdomain with short max-age: a subdomain with max-age=86400 (1 day) provides minimal protection. Users who visit infrequently won't be protected between visits.
Not testing before adding includeSubDomains: verify every subdomain responds over HTTPS before enabling this. Use curl -I https://sub.example.com on each subdomain.
Adding preload without submitting: the preload directive in the header is a declaration of intent, not an automatic submission. Visit hstspreload.org to actually submit.
HSTS and certificate errors
HSTS changes browser behavior on certificate errors. Without HSTS, a user can click "proceed anyway" when they see a certificate error. With HSTS, this option is removed — the browser shows a hard block with no bypass.
This is intentional: allowing bypass would let a network attacker present a self-signed certificate and have the user accept it, re-enabling SSL stripping.
Be aware that this means any certificate error on a preloaded domain is a complete outage for that browser until the cert is fixed.
How PatchVex detects HSTS issues
The PatchVex Web Scanner:
- Requests both the HTTP and HTTPS versions of the URL
- Checks for
Strict-Transport-Securityon the HTTPS response - Flags if absent
- Parses
max-age— flags if under 15768000 (6 months) - Notes absence of
includeSubDomains - Notes absence of
preload - Flags if the header appears on an HTTP response (ineffective)
- Checks that HTTP redirects to HTTPS (a prerequisite for HSTS)