How to Set Up HSTS (HTTP Strict Transport Security)
Force HTTPS connections and prevent downgrade attacks
TL;DR
TL;DR (15 minutes)
HSTS forces browsers to use HTTPS only. Add the header Strict-Transport-Security: max-age=31536000; includeSubDomains after verifying your site works over HTTPS. Start with a short max-age (300 seconds) for testing. Only use includeSubDomains if all subdomains support HTTPS. Consider preload list submission for maximum security.
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. This is called a "protocol downgrade attack" or "SSL stripping." HSTS prevents this by making browsers refuse HTTP connections entirely.
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 on the preload list, removal is difficult and takes months. Your domain will be hardcoded in browsers to always use HTTPS. Only submit if you're certain your domain and ALL subdomains 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. It prevents downgrade attacks where attackers force HTTP connections to intercept traffic. Without HSTS, even with HTTP->HTTPS redirects, the first request can be intercepted.
What max-age should I use for HSTS?
Start with max-age=300 (5 minutes) for testing. Use max-age=31536000 (1 year) for production. For preload list submission, you need at least 1 year. Never use max-age=0 unless intentionally disabling HSTS.
What happens if I enable HSTS but my HTTPS breaks?
Browsers refuse HTTP connections until max-age expires or you send max-age=0. Start with a short max-age for testing. If on the preload list, HSTS is hardcoded in browsers and cannot be easily disabled.
Should I use includeSubDomains?
Only if ALL subdomains support HTTPS. This applies HSTS to every subdomain, including future ones. If any subdomain doesn't support HTTPS, users can't access it.
What is the HSTS preload list?
A list of domains hardcoded into browsers to always use HTTPS. Browsers never attempt HTTP for preloaded domains, even on first visit. Requires 1-year max-age, includeSubDomains, and preload directive.
Check Your HSTS Configuration
Scan your site to verify HSTS is properly configured and check for other security headers.
Start Free Scan