What HSTS Does
HTTP Strict Transport Security tells browsers: "Never connect to this domain over HTTP. Always use HTTPS." After a browser receives the HSTS header, it automatically upgrades all future HTTP requests to HTTPS — even if the user types http:// or clicks an HTTP link. This happens client-side, before any network request is made.
Without HSTS, the first request to http://example.com goes over plaintext HTTP, and the server redirects to HTTPS. That plaintext request is vulnerable to interception:
Without HSTS:
User → HTTP (plaintext, interceptable) → Server → 301 to HTTPS → User → HTTPS
With HSTS:
User → Browser upgrades to HTTPS internally → HTTPS (never hits the wire as HTTP)
What HSTS prevents:
- SSL stripping attacks — An attacker on the network intercepts the initial HTTP request and proxies a plaintext connection to the user while connecting to the server via HTTPS. The user sees HTTP in the address bar but doesn't notice.
- Protocol downgrade attacks — Forcing a connection back to HTTP to intercept traffic.
- Cookie hijacking — Cookies set without the
Secureflag can be sent over HTTP. HSTS ensures no HTTP requests happen.
The HSTS Header
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| Directive | Meaning |
|---|---|
max-age=31536000 |
Browser remembers HSTS for 31,536,000 seconds (1 year). After this time expires without a new header, HSTS is forgotten. |
includeSubDomains |
Applies to all subdomains (www, api, cdn, mail, etc.). Without this, only the exact domain is protected. |
preload |
Signals intent to be included in the browser's built-in HSTS preload list (see below). |
Checking Your Current HSTS Configuration
# Check if HSTS header is present
curl -sI https://example.com | grep -i strict-transport-security
# Check from HTTP (should redirect to HTTPS, NOT serve content)
curl -sI http://example.com | head -5
# Check with verbose SSL details
curl -vI https://example.com 2>&1 | grep -i "strict-transport"
Online tools:
- SSL Labs Server Test — Grades your HTTPS setup including HSTS.
- Security Headers — Checks all security headers including HSTS.
- hstspreload.org — Checks if your domain meets preload list requirements.
Common findings:
- No HSTS header at all — Most common issue. The header simply isn't configured.
- max-age too short — Values under 31536000 (1 year) don't qualify for preload and provide weaker protection.
- Missing includeSubDomains — Subdomains remain vulnerable to SSL stripping.
- HSTS on HTTP response — The header must only be sent over HTTPS. Browsers ignore HSTS headers received over HTTP (an attacker could inject a fake one).
Incremental Rollout (Recommended)
Don't jump straight to max-age=31536000; includeSubDomains; preload. If something is wrong with your HTTPS configuration on any subdomain, HSTS will make that subdomain completely inaccessible. Roll out gradually:
Step 1: Short max-age, No Subdomains (1 week)
Strict-Transport-Security: max-age=300
Test for 1 week. If anything breaks, the 5-minute max-age means browsers forget HSTS quickly.
Verify: All pages load over HTTPS. No mixed content warnings. No certificate errors.
Step 2: Increase max-age (1 month)
Strict-Transport-Security: max-age=604800
One week max-age. Monitor for issues across all pages and subdomains.
Step 3: Add includeSubDomains (1 month)
Strict-Transport-Security: max-age=604800; includeSubDomains
Before this step, verify every subdomain has valid HTTPS:
www.example.com— valid cert, redirects HTTP → HTTPSapi.example.com— valid certcdn.example.com— valid certmail.example.com— valid cert (or doesn't serve web traffic)- Any other subdomain
If a subdomain doesn't support HTTPS, includeSubDomains will break it. Fix all subdomains before enabling this.
Step 4: Full Production + Preload
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
One year max-age. Ready for preload list submission.
HSTS Preload List
The preload list is a list of domains built into Chrome, Firefox, Safari, and Edge. Browsers enforce HSTS for preloaded domains from the very first visit — no initial HTTP request needed.
Requirements for Preload
From hstspreload.org:
- Valid HTTPS certificate (not self-signed).
- Redirect all HTTP traffic to HTTPS on the same host (
http://example.com→https://example.com, not tohttps://www.example.com). - All subdomains must support HTTPS.
- HSTS header on the base domain (
example.com, not justwww.example.com) with:max-ageat least 31536000 (1 year)includeSubDomainsdirectivepreloaddirective
Submitting to the Preload List
- Meet all requirements above.
- Go to hstspreload.org and enter your domain.
- If all checks pass, submit your domain.
- Wait — the preload list is updated with each browser release (roughly every 6 weeks for Chrome). It can take 1–4 months for your domain to appear in production browser builds.
Removing from the Preload List
This is slow and difficult. Removal takes the same 1–4 month browser release cycle. If you preload a domain and then need to serve HTTP content, users will be locked out for months.
Only preload if you are committed to HTTPS on all subdomains permanently.
Server Configuration
nginx
server {
listen 80;
server_name example.com www.example.com;
# Redirect all HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
# HSTS header — 'always' ensures it's sent even on error responses
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# ... rest of config
}
nginx gotcha: add_header in a location block overrides add_header in the server block. If you have add_header in any location block, you must repeat the HSTS header there too. Alternatively, use the map directive or ngx_http_headers_more_module.
Apache
# In VirtualHost for port 80:
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
# In VirtualHost for port 443:
<VirtualHost *:443>
ServerName example.com
SSLEngine on
# HSTS header
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>
Requires mod_headers enabled: a2enmod headers && systemctl reload apache2
Caddy
example.com {
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
Caddy automatically handles HTTP → HTTPS redirection and TLS certificates. The HSTS header is the only thing you need to add explicitly.
IIS
<!-- web.config -->
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
</customHeaders>
</httpProtocol>
<rewrite>
<rules>
<rule name="HTTP to HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
</rules>
</rewrite>
</system.webServer>
Cloudflare
Dashboard → SSL/TLS → Edge Certificates → HTTP Strict Transport Security → Enable.
Configure max-age (recommended: 12 months), enable Include subdomains, enable Preload. Cloudflare adds the header at the edge — you don't need to configure it on your origin server.
Recovering from HSTS Mistakes
Users Can't Access a Subdomain
Problem: You enabled includeSubDomains but staging.example.com doesn't have HTTPS.
Fix:
- Get a valid certificate for the subdomain (Let's Encrypt, or a wildcard cert).
- OR, if you can't add HTTPS to the subdomain, lower the max-age on the main domain to get browsers to forget faster.
- Users can manually clear HSTS for a domain in Chrome at
chrome://net-internals/#hsts→ "Delete domain security policies" → enter the domain.
Need to Temporarily Serve HTTP
Problem: You preloaded but need HTTP access for some reason.
Reality: You can't easily undo preloading. Browser-cached HSTS (non-preloaded) expires after max-age. Preloaded domains are hardcoded into browser builds. Submit a removal request at hstspreload.org and wait 1–4 months.
Prevention: This is why incremental rollout matters. Don't preload until you're certain all subdomains will stay HTTPS permanently.
Other Security Headers to Consider
HSTS is one piece of a complete security header configuration. Related headers:
| Header | Purpose |
|---|---|
Content-Security-Policy |
Controls which resources the page can load (prevents XSS) |
X-Content-Type-Options: nosniff |
Prevents MIME type sniffing |
X-Frame-Options: DENY |
Prevents clickjacking via iframes |
Referrer-Policy: strict-origin-when-cross-origin |
Controls referrer information in requests |
Permissions-Policy |
Controls browser features (camera, microphone, geolocation) |
Check all your security headers at securityheaders.com.