HTTP

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.

6 min read intermediate Try in Playground

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

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.

Keep Learning