How to Set Up HSTS (HTTP Strict Transport Security)
Force HTTPS connections and prevent downgrade attacks
TL;DR
TL;DR (15 minutes)
HSTS tells browsers to skip HTTP entirely. Add Strict-Transport-Security: max-age=31536000; includeSubDomains once you've confirmed HTTPS works on your whole site. Start with max-age=300 for testing so you can increase it safely. Don't use includeSubDomains unless every subdomain is already on HTTPS, and don't submit to the preload list until you're sure you'll never need HTTP again.
Prerequisites
- Valid SSL/TLS certificate installed (Let's Encrypt, commercial CA, etc.)
- Your site accessible via HTTPS without certificate errors
- All resources (images, scripts, styles) loaded over HTTPS
- HTTP to HTTPS redirect already configured
- If using includeSubDomains: all subdomains must support HTTPS
What is HSTS?
HSTS (HTTP Strict Transport Security) tells browsers to only connect to your site using HTTPS, even if a user types http:// or clicks an HTTP link.
Why HSTS Matters
Without HSTS, an attacker can intercept the initial HTTP request before your redirect happens. That's SSL stripping: your server sends a 301 to HTTPS, but the plaintext request is already exposed. HSTS stops this. Browsers refuse HTTP before sending a single byte, with no round trip to your server.
HSTS Header Syntax
Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preload
| Directive | Required | Description |
|---|---|---|
| max-age | Yes | How long (in seconds) browsers should remember to use HTTPS |
| includeSubDomains | No | Apply HSTS to all subdomains (*.example.com) |
| preload | No | Consent to being added to browser preload lists |
Common max-age Values
| Value | Duration | Use Case |
|---|---|---|
| 300 | 5 minutes | Initial testing |
| 86400 | 1 day | Extended testing |
| 604800 | 1 week | Staging/verification |
| 31536000 | 1 year | Production (recommended) |
| 63072000 | 2 years | Maximum security |
Step-by-Step Implementation
Verify HTTPS is Working Properly
Before enabling HSTS, ensure your site is fully HTTPS-ready:
# Check SSL certificate
curl -vI https://yoursite.com 2>&1 | grep -A 6 "Server certificate"
# Check for mixed content (HTTP resources on HTTPS page)
# Open Chrome DevTools > Console > Look for "Mixed Content" warnings
# Verify redirect is in place
curl -I http://yoursite.com
# Should return 301/302 redirect to https://
Pre-flight Checklist
- SSL certificate is valid and not expiring soon
- All pages accessible via HTTPS
- No mixed content warnings in browser console
- HTTP to HTTPS redirect working
- All subdomains support HTTPS (if using includeSubDomains)
Start with a Short max-age for Testing
Add HSTS with a 5-minute max-age to test safely:
nginx:
server {
listen 443 ssl http2;
server_name yoursite.com;
# HSTS - Start with short max-age for testing
add_header Strict-Transport-Security "max-age=300" always;
# ... rest of config
}
Apache:
<VirtualHost *:443>
ServerName yoursite.com
# HSTS - Start with short max-age for testing
Header always set Strict-Transport-Security "max-age=300"
# ... rest of config
</VirtualHost>
Express.js:
const helmet = require('helmet');
// HSTS with short max-age for testing
app.use(helmet.hsts({
maxAge: 300, // 5 minutes
includeSubDomains: false,
preload: false
}));
Vercel (vercel.json):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=300"
}
]
}
]
}
Netlify (_headers):
/*
Strict-Transport-Security: max-age=300
Verify the Header is Being Sent
# Check HSTS header with curl
curl -I https://yoursite.com | grep -i strict-transport-security
# Expected output:
# strict-transport-security: max-age=300
Or check in browser DevTools:
- Open DevTools (F12)
- Go to Network tab
- Reload the page
- Click on the document request
- Find
strict-transport-securityin Response Headers
Test HSTS Behavior
Verify browsers redirect HTTP to HTTPS without contacting your server:
- Visit your site via HTTPS to receive the HSTS header
- Type
http://yoursite.comin the address bar - The browser should redirect to HTTPS immediately (internal redirect)
- Check Network tab - you should see a "307 Internal Redirect"
Check HSTS status in Chrome:
# Visit this URL in Chrome
chrome://net-internals/#hsts
# Query your domain to see HSTS status
# Delete the entry if you need to reset during testing
Increase max-age for Production
Once verified, increase to 1 year (required for preload list):
nginx:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Express.js:
app.use(helmet.hsts({
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: false // Don't add preload yet
}));
Vercel:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
}
]
}
]
}
Netlify:
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
Submit to HSTS Preload List (Optional)
For maximum security, submit your domain to the browser preload list:
Requirements for preload:
- Serve a valid SSL certificate
- Redirect all HTTP to HTTPS on the same host
- All subdomains must support HTTPS
- HSTS header with: max-age of at least 1 year (31536000)
- HSTS header with: includeSubDomains directive
- HSTS header with: preload directive
Update your HSTS header for preload:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Submit at: hstspreload.org
Preload Warning
Once you're on the preload list, getting off it takes months. Removal goes through a formal process and then has to ship in browser releases. Don't submit unless you're genuinely certain your domain and every subdomain will support HTTPS permanently.
HSTS Security Checklist
- SSL certificate is valid and set to auto-renew
- Tested with short max-age first before production values
- All subdomains support HTTPS (if using includeSubDomains)
- No planned migration to HTTP-only subdomains
- Have a process for SSL certificate renewal
- Understand preload list implications before submitting
How to Verify It Worked
Method 1: Command Line
# Check HSTS header
curl -sI https://yoursite.com | grep -i strict-transport-security
# Expected output:
strict-transport-security: max-age=31536000; includeSubDomains
Method 2: Browser DevTools
- Open DevTools (F12) > Network tab
- Reload your HTTPS site
- Click on the document request
- Check Response Headers for
strict-transport-security
Method 3: Check Internal Redirect
- First, visit your HTTPS site to cache the HSTS policy
- Open DevTools > Network tab
- Type
http://yoursite.comin address bar - You should see "307 Internal Redirect" (not a server redirect)
Method 4: Chrome HSTS Inspector
# Open in Chrome
chrome://net-internals/#hsts
# In "Query HSTS/PKP domain" section:
# Enter your domain and click "Query"
# Should show "Found" with your max-age
Method 5: Online Scanner
Use securityheaders.com to verify HSTS is correctly configured.
Common Errors and Troubleshooting
HSTS header not appearing
- Not on HTTPS: HSTS header is only sent over HTTPS, not HTTP
- CDN stripping headers: Check CDN configuration for header passthrough
- Server config not reloaded: Restart nginx/Apache after changes
Can't access site after enabling HSTS
- Certificate expired: Renew your SSL certificate immediately
- Clear HSTS cache: In Chrome, go to
chrome://net-internals/#hstsand delete the entry - Wait for max-age: If using a long max-age, you may need to wait or clear browser data
Subdomain not accessible
- includeSubDomains issue: If you used includeSubDomains but a subdomain doesn't support HTTPS
- Solution: Add HTTPS to the subdomain, or remove includeSubDomains from the main domain
- Clear HSTS cache: Users who visited before need to clear their HSTS cache
Can't remove from preload list
- Long process: Submit removal request at hstspreload.org
- Takes months: Changes propagate with browser releases
- Send max-age=0: While waiting, send
max-age=0to clear client caches
Pro Tip: Gradual HSTS Rollout
Increase max-age gradually: 5 min -> 1 day -> 1 week -> 1 month -> 1 year. This gives you time to identify issues before browsers cache a long-lived policy. Monitor for certificate renewal issues and subdomain problems at each stage.
Frequently Asked Questions
What is HSTS and why do I need it?
HSTS tells browsers to only connect via HTTPS. Without it, an attacker can intercept the very first HTTP request before your redirect fires. That initial request is the hole. HSTS closes it by making browsers refuse HTTP before a connection is even attempted.
What max-age should I use for HSTS?
Start with max-age=300 (5 minutes) for testing, then ramp up to max-age=31536000 (1 year) for production. The preload list requires at least 1 year. Don't use max-age=0 unless you're intentionally disabling HSTS.
What happens if I enable HSTS but my HTTPS breaks?
Browsers won't make HTTP connections until max-age expires or you explicitly send max-age=0. That's why you start with a short max-age. If you're on the preload list, it's worse: the browser has your domain hardcoded and you can't override it from a server header.
Should I use includeSubDomains?
Only if all subdomains support HTTPS, including ones you haven't created yet. That's the catch: includeSubDomains covers every *.yourdomain.com, so spinning up an HTTP-only staging subdomain months later will break it for anyone with the HSTS policy cached.
What is the HSTS preload list?
A list of domains hardcoded directly into Chrome, Firefox, Safari, and Edge to always use HTTPS. Unlike a normal HSTS header, the protection kicks in on the very first visit before your server has ever responded. Requirements: max-age of at least 1 year, includeSubDomains, and the preload directive.
Check Your HSTS Configuration
Scan your site to verify HSTS is properly configured and check for other security headers.