#
HSTS: HTTP Strict Transport Security
ESSENTIAL SECURITY RFC 6797 MANDATORY FOR HTTPS
Critical HTTPS Protection
HSTS is a mandatory security header for all HTTPS websites. It protects against protocol downgrade attacks and SSL stripping, vulnerabilities that affect even sites with valid HTTPS certificates.
#
What is HSTS?
SECURITY POLICY
Simple Definition: HSTS (HTTP Strict Transport Security) is a security policy mechanism that forces browsers to only interact with a website using HTTPS, preventing protocol downgrade attacks and cookie hijacking.
What HSTS Does:
- Forces HTTPS connections only (never HTTP)
- Blocks protocol downgrade attacks (SSL stripping)
- Protects cookies from being sent over HTTP
- Works even for first-time visitors (with preload)
Real-World Analogy
Think of HSTS like a "members only" club that stamps your hand:
- First visit: Club stamps your hand (browser saves HSTS policy)
- Future visits: Bouncer (browser) won't let you use the back door (HTTP)
- You MUST always use the secure front entrance (HTTPS)
- The stamp lasts for a specified time (max-age parameter)
- No exceptions - even if you try to use HTTP, browser upgrades to HTTPS
RFC 6797 STANDARD
HSTS is an IETF standard (RFC 6797) adopted in 2012, now supported by all major browsers.
#
The Problem HSTS Solves
SSL STRIPPING ATTACK
Critical Vulnerability Without HSTS
Even websites that fully support HTTPS are vulnerable to SSL stripping attacks. An attacker on the network can intercept the initial HTTP request before it's upgraded to HTTPS, stealing credentials and session cookies.
#
The Vulnerability: Why HTTPS Alone Isn't Enough
Even when a website supports HTTPS, users can still be vulnerable to these attacks:
SSL Stripping - Downgrade HTTPS to HTTP Cookie Hijacking - Steal session cookies sent over HTTP Man-in-the-Middle - Intercept initial connection Protocol Downgrade - Force use of insecure HTTP
Step 1: User Types URL
User types: http://bank.com
(Forgot the 's' in https)
Step 2: Initial HTTP Request
Browser → Server: GET / HTTP/1.1
Host: bank.com
Step 3: Attacker Intercepts
┌─────────┐ HTTP ┌──────────┐ HTTPS ┌────────┐
│ Browser │ ──────────> │ Attacker │ ──────────> │ Server │
└─────────┘ (Plaintext) └──────────┘ (Encrypted) └────────┘
↑
Steals data!
What Attacker Sees:
- Cookies (session tokens)
- Passwords (if submitted)
- Sensitive data
#
Common Vulnerable Scenarios
User Types HTTP
http://bank.com → 301 Redirect → https://bank.comFirst request is vulnerable!
Old Bookmarks
Bookmark from 2015: http://example.com Still goes to HTTP firstMixed Content Links
<a href="http://bank.com/login">Login</a>Clicking link uses HTTP
DNS Hijacking
Attacker intercepts DNS → Points to attacker's server
#
How HSTS Works
#
The HSTS Header
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Parameters:
#
HSTS Flow
User types: http://bank.com
┌─────────┐ ┌────────┐
│ Browser │ │ Server │
└────┬────┘ └────┬───┘
│ │
│ 1. GET / HTTP/1.1 │
│────────────────────────────────────>│
│ │
│ 2. HTTP 301 Moved Permanently │
│ Location: https://bank.com │
│<────────────────────────────────────│
│ │
│ 3. GET / HTTP/1.1 (HTTPS) │
│────────────────────────────────────>│
│ │
│ 4. HTTP 200 OK │
│ Strict-Transport-Security: │
│ max-age=31536000 │
│<────────────────────────────────────│
│ │
│ Browser remembers: │
│ "Always use HTTPS for bank.com" │
└─────────────────────────────────────┘
User types: http://bank.com
┌─────────┐ ┌────────┐
│ Browser │ │ Server │
└────┬────┘ └────┬───┘
│ │
│ Browser checks HSTS cache: │
│ "bank.com → Always HTTPS" │
│ │
│ Browser internally upgrades: │
│ http:// → https:// │
│ │
│ 1. GET / HTTP/1.1 (HTTPS) │
│────────────────────────────────────>│
│ │
│ NO HTTP request sent! │
│ Attacker can't intercept! │
│ │
│ 2. HTTP 200 OK │
│<────────────────────────────────────│
└─────────────────────────────────────┘
Key Benefit: NO HTTP traffic, NO interception possible!
#
Implementation
#
Basic Configuration
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Add HSTS header
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
# Your application
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
# Add HSTS header
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>
# Redirect HTTP to HTTPS
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
const express = require('express');
const helmet = require('helmet');
const app = express();
// Use helmet to set HSTS
app.use(helmet.hsts({
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: false // Set true only when ready
}));
// Or manually
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
next();
});
// Force HTTPS redirect
app.use((req, res, next) => {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(301, 'https://' + req.get('host') + req.url);
}
next();
});
app.listen(3000);
from flask import Flask, redirect, request
app = Flask(__name__)
@app.after_request
def set_hsts(response):
response.headers['Strict-Transport-Security'] = \
'max-age=31536000; includeSubDomains'
return response
# Force HTTPS redirect
@app.before_request
def before_request():
if not request.is_secure:
url = request.url.replace('http://', 'https://', 1)
return redirect(url, code=301)
if __name__ == '__main__':
app.run(ssl_context=('cert.pem', 'key.pem'))
package main
import (
"net/http"
)
func hstsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
)
next.ServeHTTP(w, r)
})
}
func redirectToHTTPS(w http.ResponseWriter, r *http.Request) {
target := "https://" + r.Host + r.URL.Path
if len(r.URL.RawQuery) > 0 {
target += "?" + r.URL.RawQuery
}
http.Redirect(w, r, target, http.StatusMovedPermanently)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello HTTPS!"))
})
// HTTPS server with HSTS
go http.ListenAndServeTLS(":443", "cert.pem", "key.pem",
hstsMiddleware(mux))
// HTTP to HTTPS redirect
http.ListenAndServe(":80", http.HandlerFunc(redirectToHTTPS))
}
#
HSTS Parameters Explained
#
max-age
Purpose: How long browsers should remember HSTS policy
Values:
max-age=0- Disable HSTS (removes from cache)max-age=300- 5 minutes (testing)max-age=86400- 1 daymax-age=2592000- 30 daysmax-age=31536000- 1 year (recommended for production)max-age=63072000- 2 years (maximum recommended)
Best Practice:
# Start with short max-age for testing
add_header Strict-Transport-Security "max-age=300";
# After testing, increase gradually
add_header Strict-Transport-Security "max-age=86400";
# Production: 1 year
add_header Strict-Transport-Security "max-age=31536000";
#
includeSubDomains
Purpose: Apply HSTS to all subdomains
Example:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Applies to:
example.com✓www.example.com✓api.example.com✓blog.example.com✓*.example.com✓
Warning: ALL subdomains must support HTTPS!
# If you set includeSubDomains:
Strict-Transport-Security: max-age=31536000; includeSubDomains
# But dev.example.com doesn't have HTTPS:
https://dev.example.com → Browser error! ❌
# Users can't access that subdomain anymore!
Solution: Only use includeSubDomains when ALL subdomains have HTTPS.
#
preload
Purpose: Include domain in browser's built-in HSTS list
Requirement for preload:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
All three required:
max-age≥ 31536000 (1 year)includeSubDomainspresentpreloaddirective
#
HSTS Preload List
MAXIMUM SECURITY
#
What is HSTS Preload?
Problem: First visit still uses HTTP (vulnerable!)
Solution: Hardcode HTTPS domains into browsers
The List:
- Maintained by Chromium project
- Built into Chrome, Firefox, Safari, Edge, IE 11
- Domains are ALWAYS accessed via HTTPS
- Protects even the very first visit
#
How to Submit
1. Visit: https://hstspreload.org
2. Requirements:
- ✅ Serve valid HTTPS certificate
- ✅ Redirect all HTTP to HTTPS (port 80 → 443)
- ✅ Serve HSTS header on all subdomains
- ✅ HSTS header on base domain with:
max-age≥ 31536000 (1 year)includeSubDomainsdirectivepreloaddirective
3. Configuration Example:
# ALL requirements met:
server {
listen 443 ssl http2;
server_name example.com *.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
add_header Strict-Transport-Security \
"max-age=31536000; includeSubDomains; preload" always;
}
server {
listen 80;
server_name example.com *.example.com;
return 301 https://$host$request_uri;
}
4. Submit:
1. Go to https://hstspreload.org
2. Enter your domain: example.com
3. Click "Check HSTS preload status and eligibility"
4. If eligible, click "Submit"
5. Wait for review (can take weeks/months)
#
Preload List Benefits
First Visit Protected - No HTTP request ever sent Maximum Security - Built into browser Long-term Protection - Can't be removed easily
#
Preload List Warnings
WARNING
Removal is SLOW - Takes months to remove from browsers Permanent Decision - Hard to reverse All Subdomains - Must work for ALL subdomains forever Breaking Changes - Can break HTTP-only subdomains
Only submit when:
- You're 100% committed to HTTPS
- All subdomains support HTTPS
- You understand the implications
#
Testing HSTS
#
Browser Developer Tools
Chrome DevTools:
1. Open DevTools (F12)
2. Network tab
3. Look for response headers:
Strict-Transport-Security: max-age=31536000
Check HSTS Status:
Chrome: chrome://net-internals/#hsts
Firefox: about:config → search "hsts"
Query HSTS Status:
1. Go to chrome://net-internals/#hsts
2. Enter domain in "Query HSTS/PKP domain"
3. Check if domain is in HSTS cache
Delete HSTS Entry (for testing):
1. Go to chrome://net-internals/#hsts
2. Enter domain in "Delete domain security policies"
3. Click "Delete"
#
Command Line Testing
# Check HSTS header
curl -I https://example.com | grep -i strict
# Output:
# strict-transport-security: max-age=31536000; includeSubDomains
# Check preload status
curl https://hstspreload.org/api/v2/status?domain=example.com
# Test redirect
curl -I http://example.com
# Should return:
# HTTP/1.1 301 Moved Permanently
# Location: https://example.com/
#
Online Testing Tools
SSL Labs:
https://www.ssllabs.com/ssltest/analyze.html?d=example.com
- Checks HSTS configuration
- Verifies preload status
- Grade scoring
Security Headers:
https://securityheaders.com/?q=example.com
- Analyzes HSTS header
- Checks max-age value
- Verifies directives
HSTS Preload:
https://hstspreload.org/
- Check eligibility
- View current status
- Submit for preload
#
Common HSTS Mistakes
#
Mistake #1: Setting HSTS on HTTP
# ❌ WRONG: HSTS on HTTP server
server {
listen 80;
server_name example.com;
# This won't work! Browsers ignore HSTS over HTTP
add_header Strict-Transport-Security "max-age=31536000";
return 301 https://$host$request_uri;
}
Why Wrong: HSTS must be served over HTTPS. Browsers ignore it on HTTP.
Fix:
# ✅ CORRECT: HSTS only on HTTPS
server {
listen 443 ssl http2;
add_header Strict-Transport-Security "max-age=31536000" always;
}
server {
listen 80;
return 301 https://$host$request_uri;
}
#
Mistake #2: Too Short max-age
# ❌ BAD: Too short
Strict-Transport-Security: max-age=300
Problem: 5 minutes = no real protection
Fix:
# ✅ GOOD: At least 1 year
Strict-Transport-Security: max-age=31536000
#
Mistake #3: Using includeSubDomains Without Planning
# ❌ DANGEROUS: If ANY subdomain lacks HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains
Breaking scenario:
https://example.com → Works ✓
https://www.example.com → Works ✓
https://dev.example.com → HTTP only → BROKEN! ❌
Fix: Only use when ALL subdomains have HTTPS.
#
Mistake #4: Jumping Straight to Preload
# ❌ RISKY: Immediately going to preload
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Problem: Hard to reverse!
Better approach:
Week 1: max-age=300 (5 minutes) - Test
Week 2: max-age=86400 (1 day) - Monitor
Week 3: max-age=2592000 (30 days) - Ensure stable
Month 2: max-age=31536000; includeSubDomains
Month 3: Add preload directive and submit
#
Mistake #5: Not Using 'always' Flag (Nginx)
# ❌ WRONG: Missing 'always'
add_header Strict-Transport-Security "max-age=31536000";
# Header only sent for 200 responses!
# Not sent for 301, 404, 500, etc.
Fix:
# ✅ CORRECT: Add 'always'
add_header Strict-Transport-Security "max-age=31536000" always;
# Header sent for ALL responses
#
Removing HSTS
#
Emergency: Remove HSTS
If you need to disable HSTS:
# Set max-age=0
Strict-Transport-Security: max-age=0
Process:
- Update server configuration to send
max-age=0 - Users visit site over HTTPS
- Browser removes HSTS entry
- HTTP access possible again
Clearing from Browser:
Chrome: chrome://net-internals/#hsts
Firefox: Delete browsing history > Clear site preferences
#
Removing from Preload List
Much harder!
- Update server: Set
max-age=0 - Submit removal: https://hstspreload.org/removal/
- Wait: Can take 3-12 months for browser updates
- Users with old browsers: Still see domain as preloaded
Lesson: Only preload when absolutely certain!
#
HSTS Best Practices
#
Deployment Checklist
- Valid HTTPS certificate installed
- All subdomains support HTTPS (if using includeSubDomains)
- HTTP → HTTPS redirect configured
- Start with short max-age (300 seconds)
- Test thoroughly on staging
- Monitor logs for issues
- Gradually increase max-age
- Use 1 year in production (31536000)
- Add 'always' flag (Nginx)
- Consider includeSubDomains carefully
- Only add preload when ready for permanent commitment
#
Production Configuration
# Production-ready HSTS configuration
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
# HSTS header
add_header Strict-Transport-Security \
"max-age=31536000; includeSubDomains" always;
# Other security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
# Your application
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
#
Security Benefits
#
Protections Provided
SSL Stripping Attacks - Prevents attacker from downgrading to HTTP Cookie Hijacking - Cookies can't be stolen over HTTP Man-in-the-Middle - Eliminates HTTP interception opportunity Protocol Downgrade - Forces HTTPS at browser level Mixed Content - Helps prevent insecure resources
#
Real-World Impact
Without HSTS:
Attacker on WiFi → Intercepts HTTP → Steals session cookie → Account compromised
With HSTS:
Browser → Enforces HTTPS → No HTTP traffic → Attacker gets nothing
#
Next Steps
#
Related Topics
TLS/SSL Basics - Understanding HTTPS Certificate Management - Managing certificates Cipher Suites - Encryption configuration
#
Tools & Resources
HSTS Preload - Submit to preload list SSL Labs - Test HSTS configuration Security Headers - Scan security headers
#
Protected by Layerd AI
Layerd AI Guardian Proxy automatically manages HSTS:
HSTS Injection - Automatically adds HSTS headers Configuration Validation - Ensures proper HSTS setup Preload Management - Assists with preload submission Monitoring - Tracks HSTS policy compliance
Learn more about Layerd AI Protection →
Last updated: November 2025