HTTPS and Web Security
HSTS: Enforcing HTTPS from the First Connection
Read aloud is unavailable in this browser.
The First-Connection Problem
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.
The Problem HSTS Solves
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:
- Victim visits
bank.example.com(nohttps://) - Browser makes initial request over HTTP
- Attacker intercepts this request (man-in-the-middle)
- Attacker relays request to real bank over HTTPS
- Attacker receives secure response, strips encryption
- Attacker serves HTTP version to victim
- Victim sees normal page, no padlock (but many users don't notice)
- Attacker captures all credentials entered
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.
The HSTS Header: Syntax and Directives
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.com- Any depth of subdomain
If 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;
}
How Browsers Implement HSTS
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:
- Static: From the preload list, compiled into the browser
- Dynamic: Learned from HSTS headers during browsing
HSTS and Private/Incognito Mode:
Behavior varies:
- Chrome: Inherits HSTS from regular session, but doesn't persist new entries
- Firefox: Separate HSTS cache for private browsing
- Safari: Similar to Chrome
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
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:
- Maintained by Chrome project, used by all major browsers
- Contains ~150,000+ domains as of 2024
- Submitted via https://hstspreload.org
- Compiled into browser binaries
Preload Requirements:
To be eligible for preloading, your site must meet:
- Valid HTTPS certificate on the main domain
- Redirect HTTP to HTTPS on the same host
- HSTS header on the HTTPS site with:
max-age≥ 31536000 (1 year)includeSubDomainsdirectivepreloaddirective
- All subdomains must support HTTPS
- HSTS header served on base domain (not just www)
Example 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 TLD
For 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
HSTS Deployment: A Safe Rollout Strategy
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
- If something breaks, HSTS expires in 5 minutes
- Test all functionality
- Monitor for support issues
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:
- Enumerate ALL subdomains (DNS zone, certificates)
- Verify each serves HTTPS correctly
- Check for any HTTP-only services
- Coordinate with teams owning subdomains
Phase 6: Preloading (Optional, Permanent)
# Commit to permanent HTTPS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
- Submit to hstspreload.org
- Understand there's no quick reversal
- Monitor submission status
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.
Common HSTS Mistakes and How to Avoid Them
- •Sending HSTS over HTTP
Browsers ignore HSTS headers on HTTP responses (security). Only configure HSTS on HTTPS virtual hosts.
# WRONG server { listen 80; add_header Strict-Transport-Security ...; # Ignored! } - •includeSubDomains without subdomain audit Breaking a single subdomain breaks user access to ALL subdomains. Internal tools, staging environments, third-party CNAMEs—all must support HTTPS.
- •Preloading without understanding permanence Preload removal takes 6-12 months. If your business needs HTTP access (legacy clients, embedded systems), do not preload.
- •Setting max-age=0 in production This disables HSTS. Useful for removal, but dangerous if accidentally deployed. Audit your configs.
- •Forgetting to send header on subdomains
If using
includeSubDomains, the header can be on the apex only. But for defense-in-depth, send it everywhere. - •Not handling certificate renewal HSTS prevents bypassing certificate errors. If renewal fails and HSTS is active, your site is completely inaccessible until you fix the certificate.
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:
- Automate certificate renewal (Let's Encrypt, Certbot)
- Monitor certificate expiration dates
- Set up alerts well before expiration (30+ days)
- Test renewal process works in staging
- Use multiple domains if business-critical (fallback)
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:
- Make internal app HTTPS (preferred)
- Move internal app to different domain (internal-app.companytools.com)
- Don't use
includeSubDomains(weakens security) - Use split DNS so internal app resolves differently
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 in the Security Header Ecosystem
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:
- HSTS affects navigation to YOUR domain
- CSP affects resources loaded BY your pages (including third-party)
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.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.