Documentation

# HSTS: HTTP Strict Transport Security

ESSENTIAL SECURITY RFC 6797 MANDATORY FOR HTTPS

# 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)

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

# 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

  1. User Types HTTP

    http://bank.com → 301 Redirect → https://bank.com

    First request is vulnerable!

  2. Old Bookmarks

    Bookmark from 2015: http://example.com
    Still goes to HTTP first
  3. Mixed Content Links

    <a href="http://bank.com/login">Login</a>

    Clicking link uses HTTP

  4. 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:

Parameter Value Purpose
max-age Seconds How long browser remembers (31536000 = 1 year)
includeSubDomains Optional Apply to all subdomains
preload Optional Submit to browser's built-in list

# 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

[Nginx]
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;
}
[Apache]
<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>
[Node.js/Express]
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);
[Python/Flask]
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'))
[Go]
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 day
  • max-age=2592000 - 30 days
  • max-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)
  • includeSubDomains present
  • preload directive

# 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)
    • includeSubDomains directive
    • preload directive

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:

  1. Update server configuration to send max-age=0
  2. Users visit site over HTTPS
  3. Browser removes HSTS entry
  4. 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!

  1. Update server: Set max-age=0
  2. Submit removal: https://hstspreload.org/removal/
  3. Wait: Can take 3-12 months for browser updates
  4. 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