- Home
- HTTP Headers
- Strict-Transport-Security Header
Header
Strict-Transport-Security Header
Learn how Strict-Transport-Security (HSTS) forces browsers to only communicate over HTTPS, preventing protocol downgrade and man-in-the-middle attacks.
TL;DR: Forces browsers to only connect via HTTPS, preventing protocol downgrade attacks. Start with short max-age for testing, then use 1+ years for production.
What is Strict-Transport-Security?
The Strict-Transport-Security (HSTS) header tells browsers to only access your site using HTTPS, never HTTP. It’s like putting up a sign saying “HTTPS only - no insecure connections allowed!” This prevents protocol downgrade attacks and cookie hijacking.
Once a browser sees this header, it will automatically convert all HTTP requests to HTTPS for the specified duration, even if the user types http:// in the address bar.
How Strict-Transport-Security Works
First HTTPS visit:
GET / HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Type: text/html
<html>...</html>
```text
**Browser remembers HSTS policy for 1 year.**
**Future HTTP attempts (automatic upgrade):**
```text
User types: http://example.com
Browser converts to: https://example.com (before even making request)
```http
## Syntax
```http
Strict-Transport-Security: max-age=<seconds>
Strict-Transport-Security: max-age=<seconds>; includeSubDomains
Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preload
Directives
- max-age - How long (in seconds) to enforce HTTPS-only
- includeSubDomains - Apply policy to all subdomains
- preload - Eligible for browser HSTS preload list
Common Examples
Basic HSTS
Strict-Transport-Security: max-age=31536000
```text
Enforce HTTPS for 1 year (31536000 seconds).
### Include Subdomains
```http
Strict-Transport-Security: max-age=31536000; includeSubDomains
Apply to www.example.com, api.example.com, etc.
Preload Ready
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
```text
Eligible for browser preload list (2 years = 63072000 seconds).
### Short Duration (Testing)
```http
Strict-Transport-Security: max-age=300
5 minutes for testing before full deployment.
Real-World Scenarios
Production Website
GET / HTTP/1.1
Host: banking.example.com
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Type: text/html
<html>
<head><title>Secure Bank</title></head>
<body>Welcome to your secure banking portal</body>
</html>
```text
### API Server
```http
GET /api/data HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Type: application/json
{"data": "secure response"}
Disable HSTS (Emergency)
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=0
```text
Setting max-age to 0 tells browser to forget HSTS policy.
### Gradual Rollout
```http
# Week 1: Short duration
Strict-Transport-Security: max-age=86400
# Week 2: Longer duration
Strict-Transport-Security: max-age=604800
# Week 3: Include subdomains
Strict-Transport-Security: max-age=2592000; includeSubDomains
# Production: Full policy
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Server Implementation
Express.js (Node.js)
const express = require('express')
const helmet = require('helmet')
const app = express()
// Using helmet middleware (recommended)
app.use(
helmet.hsts({
maxAge: 31536000, // 1 year in seconds
includeSubDomains: true,
preload: true
})
)
// Manual implementation
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')
next()
})
// Only send HSTS over HTTPS
app.use((req, res, next) => {
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')
}
next()
})
// Different settings for different environments
const hstsMaxAge =
process.env.NODE_ENV === 'production'
? 31536000 // 1 year in production
: 300 // 5 minutes in development
app.use((req, res, next) => {
if (req.secure) {
res.setHeader('Strict-Transport-Security', `max-age=${hstsMaxAge}; includeSubDomains`)
}
next()
})
// Force HTTPS redirect
app.use((req, res, next) => {
if (!req.secure && req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(301, `https://${req.hostname}${req.url}`)
}
next()
})
```javascript
### FastAPI (Python)
```python
from fastapi import FastAPI, Request
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
@app.middleware("http")
async def add_hsts_header(request: Request, call_next):
response = await call_next(request)
# Only send HSTS over HTTPS
if request.url.scheme == "https":
response.headers["Strict-Transport-Security"] = \
"max-age=31536000; includeSubDomains; preload"
return response
# Force HTTPS redirect
@app.middleware("http")
async def force_https(request: Request, call_next):
if request.url.scheme != "https" and \
request.headers.get("x-forwarded-proto") != "https":
# Redirect to HTTPS
https_url = request.url.replace(scheme="https")
return RedirectResponse(url=str(https_url), status_code=301)
response = await call_next(request)
return response
Django
# settings.py
# Enable HSTS
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Force HTTPS
SECURE_SSL_REDIRECT = True
# For testing (use shorter duration)
if DEBUG:
SECURE_HSTS_SECONDS = 300 # 5 minutes
# Custom middleware
class HSTSMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.is_secure():
response['Strict-Transport-Security'] = \
'max-age=31536000; includeSubDomains; preload'
return response
MIDDLEWARE = [
# ... other middleware ...
'myapp.middleware.HSTSMiddleware',
]
```nginx
### Nginx
```nginx
server {
listen 443 ssl http2;
server_name example.com;
# SSL certificates
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# HSTS header
add_header Strict-Transport-Security \
"max-age=31536000; includeSubDomains; preload" always;
# ... rest of config ...
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
Apache
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
# HSTS header
Header always set Strict-Transport-Security \
"max-age=31536000; includeSubDomains; preload"
</VirtualHost>
# Redirect HTTP to HTTPS
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
```text
## Best Practices
### For Servers
**1. Start with short max-age**
```http
# ✅ Test first (5 minutes)
Strict-Transport-Security: max-age=300
# After testing, increase gradually
Strict-Transport-Security: max-age=86400 # 1 day
Strict-Transport-Security: max-age=2592000 # 30 days
Strict-Transport-Security: max-age=31536000 # 1 year
2. Only send over HTTPS
// ✅ Check if connection is secure
if (req.secure) {
res.setHeader('Strict-Transport-Security', 'max-age=31536000')
}
// ❌ Don't send over HTTP
res.setHeader('Strict-Transport-Security', 'max-age=31536000')
```text
**3. Be careful with includeSubDomains**
```http
# ⚠️ Make sure ALL subdomains support HTTPS first
Strict-Transport-Security: max-age=31536000; includeSubDomains
# ✅ Test subdomains separately first
# Check: api.example.com, www.example.com, etc.
4. Use preload cautiously
# ⚠️ Preload is hard to undo - be absolutely sure
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# Requirements for preload:
# - max-age >= 31536000 (1 year)
# - includeSubDomains directive
# - preload directive
# - Valid HTTPS certificate
# - All subdomains must support HTTPS
```text
**5. Have a rollback plan**
```http
# Emergency disable (removes HSTS policy)
Strict-Transport-Security: max-age=0
For Deployment
1. Ensure valid HTTPS setup first
# ✅ Check SSL certificate
openssl s_client -connect example.com:443
# ✅ Test all subdomains
curl -I https://www.example.com
curl -I https://api.example.com
```text
**2. Test with short max-age**
```nginx
# Testing configuration
add_header Strict-Transport-Security "max-age=300";
# Monitor for issues for a few days
# Then increase to production value
3. Monitor mixed content warnings
<!-- ❌ Mixed content (breaks HSTS) -->
<img src="http://cdn.example.com/image.jpg" />
<!-- ✅ Use HTTPS or protocol-relative URLs -->
<img src="https://cdn.example.com/image.jpg" />
<img src="//cdn.example.com/image.jpg" />
```text
**4. Submit to HSTS preload list**
```text
https://hstspreload.org/
Requirements:
1. Serve a valid certificate
2. Redirect from HTTP to HTTPS on the same host
3. Serve all subdomains over HTTPS
4. Serve HSTS header on base domain with:
- max-age >= 31536000
- includeSubDomains
- preload
```text
## HSTS Preload
### What is Preload?
Browsers ship with a hardcoded list of domains that require HTTPS. Sites on this list are HTTPS-only from the first visit.
### Submitting to Preload List
```http
# 1. Set proper HSTS header
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# 2. Ensure all requirements met
# - Valid HTTPS on all subdomains
# - HTTP redirects to HTTPS
# - No mixed content
# 3. Submit at https://hstspreload.org/
# 4. Wait for inclusion in Chrome, Firefox, Safari, etc.
Removing from Preload (Hard!)
# Visit https://hstspreload.org/
# Request removal
# Wait 6-12 months for browser updates
# Not recommended - very difficult to undo
Common Time Periods
Testing
Strict-Transport-Security: max-age=300 # 5 minutes
Strict-Transport-Security: max-age=3600 # 1 hour
Strict-Transport-Security: max-age=86400 # 1 day
```http
### Staging
```http
Strict-Transport-Security: max-age=604800 # 1 week
Strict-Transport-Security: max-age=2592000 # 30 days
Production
Strict-Transport-Security: max-age=31536000 # 1 year (recommended)
Strict-Transport-Security: max-age=63072000 # 2 years (for preload)
```text
## Security Benefits
### Prevents Protocol Downgrade Attacks
```text
Attacker tries: http://bank.com (intercept)
Browser automatically uses: https://bank.com (secure)
```text
### Prevents Cookie Hijacking
```text
Without HSTS:
http://bank.com → Cookie sent in cleartext → Attacker steals
With HSTS:
http://bank.com → Browser uses HTTPS → Cookie encrypted
```text
### Prevents Mixed Content
```text
Forces all resources to use HTTPS, preventing insecure content injection
```text
## Testing HSTS
### Using curl
```bash
# Check HSTS header
curl -I https://example.com
# Verify over HTTPS only
curl -I https://example.com | grep -i strict-transport
# Should not appear over HTTP
curl -I http://example.com | grep -i strict-transport
Using Browser DevTools
1. Open DevTools → Network tab
2. Visit https://example.com
3. Click on first request
4. Headers → Response Headers
5. Look for Strict-Transport-Security
Check HSTS Status
// Chrome: chrome://net-internals/#hsts
// Search for your domain to see HSTS status
// Or programmatically:
fetch('https://example.com').then((response) => {
const hsts = response.headers.get('Strict-Transport-Security')
console.log('HSTS:', hsts)
})
```text
### Test Preload Eligibility
```text
Visit: https://hstspreload.org/
Enter your domain
Check if it meets all requirements
Related Headers
- Content-Security-Policy - Broader security policy
- X-Frame-Options - Clickjacking protection
- X-Content-Type-Options - MIME sniffing protection
- Referrer-Policy - Control referrer information
Frequently Asked Questions
What is Strict-Transport-Security?
HSTS tells browsers to only connect via HTTPS. After receiving this header, browsers automatically upgrade HTTP requests to HTTPS and refuse insecure connections.
What is a good max-age for HSTS?
Start with max-age=86400 (1 day) for testing. Production sites should use max-age=31536000 (1 year) or longer. Preload requires at least 1 year.
What is HSTS preload?
Preload adds your domain to browser built-in HSTS lists. Include preload directive and submit to hstspreload.org. This protects first visits before receiving the header.
Should I include subdomains?
Use includeSubDomains if all subdomains support HTTPS. This is required for preload. Be careful: it affects all subdomains including ones you might not control.