Loading content...
You've migrated your entire site to HTTPS, obtained proper certificates, eliminated mixed content, and configured HTTP-to-HTTPS redirects. Your site is now fully secure, right? Almost.
There's a critical vulnerability window that remains: the first connection. When a user types example.com in their browser (without https://), the browser's default behavior is to try HTTP first. That initial HTTP request—before the redirect to HTTPS—is vulnerable to interception.
HTTP Strict Transport Security (HSTS) closes this gap. It's a security mechanism that tells browsers: "Never connect to this domain over HTTP. Always use HTTPS, from the very first request."
In this comprehensive exploration, we'll dissect HSTS completely—how it works, why it's essential, configuration best practices, common pitfalls, and the browser preload list that eliminates even the first-request vulnerability.
By the end of this page, you will understand: (1) Why HTTP-to-HTTPS redirects aren't enough, (2) How HSTS works and its security properties, (3) The HSTS header syntax and directives, (4) The HSTS preload list, (5) Deployment strategies and common mistakes, and (6) HSTS interaction with other security mechanisms.
To understand HSTS, we must first understand why HTTP redirects alone are insufficient.
The SSL Stripping Attack:
In 2009, security researcher Moxie Marlinspike demonstrated the "SSL stripping" attack:
bank.example.com (no https://)Why Redirects Don't Help:
Even with HTTP→HTTPS redirects:
User Request: GET http://bank.example.com
↓
[INTERCEPTABLE WINDOW]
↓
Server Response: 301 Moved Permanently
Location: https://bank.example.com
The attacker can intercept the initial request before the redirect and simply not pass along the redirect. The user never sees HTTPS.
What HSTS Provides:
With HSTS, browsers remember that a domain must use HTTPS:
1. First visit: Browser receives HSTS header
2. Browser stores: "bank.example.com = HTTPS only for 1 year"
3. Future visits: Browser uses HTTPS automatically
- User types: bank.example.com
- Browser requests: https://bank.example.com
- NO HTTP request is ever made
The critical difference: the browser upgrades to HTTPS internally, before making any network request.
HSTS has a limitation: the browser must successfully receive the HSTS header at least once over a secure connection. If the attacker intercepts the very first visit, they can prevent HSTS from being set. The HSTS preload list (covered later) solves this.
HSTS is implemented via a single HTTP response header, sent only over HTTPS connections.
Basic Syntax:
Strict-Transport-Security: max-age=<seconds>
Full Syntax with All Directives:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
The Three Directives:
| Directive | Required | Description | Example |
|---|---|---|---|
max-age | Yes | Time in seconds the browser should remember HSTS policy | max-age=31536000 (1 year) |
includeSubDomains | No | Apply HSTS to all subdomains | includeSubDomains |
preload | No | Request inclusion in browser preload list | preload |
Understanding max-age:
The max-age directive specifies how long (in seconds) the browser should enforce HTTPS:
max-age=0 → Disable HSTS immediately
max-age=300 → 5 minutes (for testing)
max-age=86400 → 1 day
max-age=604800 → 1 week
max-age=2592000 → 30 days
max-age=31536000 → 1 year (recommended minimum for production)
max-age=63072000 → 2 years (required for preload)
Timer Behavior:
Every time the browser receives the HSTS header, the timer resets. If you visit a site monthly and it has max-age=604800 (1 week), the HSTS policy is continuously renewed.
Warning: If you stop sending the header, the policy will eventually expire. Users won't be protected after max-age seconds pass without visiting.
Understanding includeSubDomains:
# Only example.com is protected
Strict-Transport-Security: max-age=31536000
# example.com AND all subdomains are protected
Strict-Transport-Security: max-age=31536000; includeSubDomains
With includeSubDomains, HSTS applies to:
example.comwww.example.comapi.example.comsecure.api.example.comIf ANY subdomain cannot serve HTTPS (e.g., a legacy internal app, a third-party service), includeSubDomains breaks it entirely. Browsers will refuse to connect over HTTP. Audit ALL subdomains before enabling this directive.
Where to Send the Header:
The HSTS header must be sent:
✅ Only over HTTPS — Browsers ignore HSTS headers received over HTTP (this prevents attackers from injecting a malicious policy)
✅ On successful responses — Include on 200, 301, 302, etc. Browsers should process it on any response.
✅ Consistently — Every HTTPS response should include it to refresh the timer.
# Nginx configuration
server {
listen 443 ssl;
server_name example.com;
# Add to all HTTPS responses
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
server {
listen 80;
server_name example.com;
# HTTP: redirect only, NO HSTS header
return 301 https://$server_name$request_uri;
}
Understanding browser HSTS behavior is essential for debugging and testing.
Internal Redirect (307):
When a user navigates to an HTTP URL for an HSTS-enabled site, the browser performs an internal redirect:
User types: http://example.com
Browser check: Is example.com in HSTS cache?
YES → Internal redirect to https://example.com
Network tab: 307 Internal Redirect
Location: https://example.com
The 307 Internal Redirect is local to the browser—no HTTP request is made.
HSTS Cache Inspection:
| Browser | How to View HSTS Cache |
|---|---|
| Chrome | chrome://net-internals/#hsts — Query and delete HSTS entries |
| Firefox | about:config → Search strictTransportSecurity (no direct cache view) |
| Safari | ~/Library/Cookies/HSTS.plist (macOS, requires parsing) |
| Edge | edge://net-internals/#hsts — Same as Chrome |
Chrome HSTS Tools (chrome://net-internals/#hsts):
Query HSTS/PKP domain:
Domain: example.com
[Query]
Result:
static_sts_domain: example.com
static_upgrade_mode: FORCE_HTTPS
static_sts_include_subdomains: true
static_sts_observed: (unix timestamp)
dynamic_sts_domain: example.com
dynamic_upgrade_mode: FORCE_HTTPS
dynamic_sts_include_subdomains: true
dynamic_sts_expiry: (unix timestamp)
Delete domain security policies:
Domain: example.com
[Delete]
Static vs Dynamic Entries:
HSTS and Private/Incognito Mode:
Behavior varies:
Certificate Errors with HSTS:
When HSTS is active, browsers refuse to show the "proceed anyway" option for certificate errors:
Without HSTS:
"Your connection is not private"
[Advanced] → "Proceed to example.com (unsafe)"
With HSTS:
"Your connection is not private"
[No bypass option]
"You cannot visit example.com because HSTS is enabled"
This is a security feature—HSTS sites have explicitly stated they should only be accessed securely.
The removal of bypass options for HSTS sites is intentional. It prevents users from being socially engineered into accepting fraudulent certificates. For legitimate development needs, you must clear the HSTS cache or use a different domain.
The HSTS preload list solves the "trust on first use" problem by including HSTS policies directly in browser source code.
The Problem It Solves:
Standard HSTS requires at least one successful HTTPS visit to set the policy. An attacker who intercepts the very first connection can prevent HSTS from ever being set.
The Solution:
Browsers ship with a preloaded list of domains that have committed to HTTPS-only. For these domains, browsers enforce HTTPS from installation—no initial visit required.
The Preload List:
Preload Requirements:
To be eligible for preloading, your site must meet:
max-age ≥ 31536000 (1 year)includeSubDomains directivepreload directiveExample Compliant Configuration:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Submission Process:
1. Configure HSTS correctly on your domain
2. Visit https://hstspreload.org
3. Enter your domain
4. Site checks all requirements
5. If passed, submit for inclusion
6. Wait for next browser release cycle (weeks to months)
7. Policy is now enforced for all users of updated browsers
Removing a domain from the preload list takes months—you must submit a removal request and wait for browser updates to propagate. During this time, users with older browsers are still locked into HTTPS. Never preload domains you're not 100% committed to maintaining HTTPS on.
Checking Preload Status:
https://hstspreload.org/?domain=example.com
Status:
✅ example.com is currently preloaded.
Status: preloaded
First submitted: 2018-06-15
Or:
❌ example.com is not preloaded.
Issues:
- HSTS header must contain the `preload` directive
- max-age must be at least 31536000 seconds (1 year)
Preloading an Entire TLD:
Some TLDs are preloaded in their entirety:
.dev — All .dev domains require HTTPS.app — All .app domains require HTTPS.page — All .page domains require HTTPS.google — Google's branded TLDFor these TLDs, browsers enforce HTTPS for every domain automatically.
Removal Process:
1. Set max-age to 0: Strict-Transport-Security: max-age=0
2. Submit removal request at hstspreload.org
3. Wait for approval and browser release cycles
4. Users gradually lose preloaded HSTS as browsers update
5. Full removal takes 6-12 months
Deploying HSTS carelessly can lock users out of your site. A gradual rollout is essential.
Phase 1: Prepare (No HSTS Yet)
□ Migrate entire site to HTTPS
□ Configure HTTP→HTTPS redirects (301)
□ Fix all mixed content issues
□ Verify ALL subdomains support HTTPS
□ Test thoroughly in all browsers
□ Monitor for certificate issues
Phase 2: Conservative HSTS
# Start with very short max-age
Strict-Transport-Security: max-age=300
Phase 3: Extended Testing
# Gradually increase max-age
Strict-Transport-Security: max-age=86400 # 1 day
# Then:
Strict-Transport-Security: max-age=604800 # 1 week
# Then:
Strict-Transport-Security: max-age=2592000 # 30 days
Phase 4: Production HSTS
# Full production deployment
Strict-Transport-Security: max-age=31536000
Phase 5: includeSubDomains (If Appropriate)
# After verifying ALL subdomains
Strict-Transport-Security: max-age=31536000; includeSubDomains
⚠️ Only after comprehensive subdomain audit:
Phase 6: Preloading (Optional, Permanent)
# Commit to permanent HTTPS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Document when you increased each max-age value. If issues are reported weeks later, you'll know the timeline for when HSTS policies were cached by various users.
# WRONG
server {
listen 80;
add_header Strict-Transport-Security ...; # Ignored!
}
includeSubDomains, the header can be on the apex only. But for defense-in-depth, send it everywhere.Mistake Deep Dive: Certificate Issues with HSTS
Without HSTS:
Expired certificate → "Proceed anyway" option → User can access site
With HSTS:
Expired certificate → NO bypass option → Site inaccessible
Mitigation:
Mistake Deep Dive: Mixed HTTP/HTTPS Environments
Scenario: Your site uses includeSubDomains but a legacy internal app runs on internal.example.com over HTTP.
User visits example.com → Receives HSTS with includeSubDomains
Later visits internal.example.com
Browser: "Nope, HSTS says HTTPS only"
Internal app: Broken
Solutions:
includeSubDomains (weakens security)With includeSubDomains, wildcards apply recursively. HSTS for example.com covers not just *.example.com but *.foo.bar.example.com. Think about ALL possible subdomains, including ones that don't exist yet.
HSTS is one component of a comprehensive security header strategy. Let's see how it interacts with other headers.
Complete Security Headers Example:
# HTTPS enforcement
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Content Security Policy (mixed content, XSS)
Content-Security-Policy: default-src https:; upgrade-insecure-requests;
# Prevent clickjacking
X-Frame-Options: DENY
# Prevent MIME sniffing
X-Content-Type-Options: nosniff
# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin
# Control browser features
Permissions-Policy: geolocation=(), microphone=(), camera=()
| Header | Primary Purpose | Interaction with HSTS |
|---|---|---|
| HSTS | Force HTTPS connections | Core; enables secure transport |
| CSP | Control resource loading | Complements with upgrade-insecure-requests |
| X-Frame-Options | Prevent framing | HSTS ensures frame source is secure |
| X-Content-Type-Options | Prevent MIME sniffing | Independent but both are defense-in-depth |
| Referrer-Policy | Control referrer data | Protects HTTPS URLs from leaking |
| Permissions-Policy | Control APIs/features | Independent security layer |
HSTS + CSP Synergy:
Both headers protect against insecure connections:
# HSTS: Browser converts http:// to https:// for this domain
Strict-Transport-Security: max-age=31536000
# CSP: Block ANY non-HTTPS resources (including third-party)
Content-Security-Policy: default-src https:
# CSP: Auto-upgrade http:// to https:// for resources
Content-Security-Policy: upgrade-insecure-requests
Key Difference:
Production Configuration (Nginx):
server {
listen 443 ssl http2;
server_name example.com;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src https:; script-src 'self' https://cdn.example.com;" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
# ... rest of config
}
Use https://securityheaders.com to audit your site's security headers. It rates your headers and provides specific recommendations for improvement.
Before deploying HSTS to production, thorough testing is essential.
Method 1: curl
# Check if HSTS header is present
curl -sI https://example.com | grep -i strict-transport-security
# Output:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Verify it's NOT sent over HTTP (should be empty)
curl -sI http://example.com | grep -i strict-transport-security
Method 2: Browser Developer Tools
1. Open DevTools → Network tab
2. Visit https://example.com
3. Click on the main document request
4. Check Response Headers for Strict-Transport-Security
Method 3: Chrome HSTS Diagnostics
1. Navigate to chrome://net-internals/#hsts
2. In "Query HSTS/PKP domain": Enter example.com
3. Click Query
4. Check results for:
- dynamic_upgrade_mode: FORCE_HTTPS
- dynamic_sts_include_subdomains: true/false
- dynamic_sts_expiry: (timestamp)
Method 4: SSL Labs Test
https://www.ssllabs.com/ssltest/analyze.html?d=example.com
Results include:
- HSTS: Yes (max-age=31536000; includeSubDomains; preload)
- HSTS Preloaded: Yes/No
- Certificate information
- Protocol/cipher analysis
Method 5: Online HSTS Checker
https://gf.dev/hsts-test
https://hstspreload.org (for preload status)
https://securityheaders.com (comprehensive header check)
Testing HSTS Behavior:
To verify HSTS is working:
1. Visit https://example.com (receive HSTS header)
2. Clear browser cache (but NOT cookies/site data)
3. Navigate to http://example.com
4. Watch Network tab: Should show 307 Internal Redirect
5. No HTTP request should appear
Alternatively:
1. Go to chrome://net-internals/#hsts
2. Add example.com to HSTS cache manually
3. Try http://example.com
4. Verify redirect
We've comprehensively explored HTTP Strict Transport Security—the mechanism that ensures HTTPS from the first connection. Let's consolidate the essential concepts:
Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preloadWhat's Next:
With HSTS understood, the final page covers Web Security Best Practices—a comprehensive guide to securing web applications beyond HTTPS, including common vulnerabilities and defensive patterns.
You now understand HSTS thoroughly—from the SSL stripping attacks it prevents through header syntax, browser behavior, the preload list, deployment strategies, and common pitfalls. Apply this knowledge to ensure your HTTPS deployments are truly secure from the first connection.