Security IndexSecurity Headers

HTTP Strict Transport Security (HSTS)

What is HSTS?

HTTP Strict Transport Security is a policy mechanism that instructs browsers to only connect to your site over HTTPS for a specified duration. Once a browser receives a valid HSTS header, it:

  1. Refuses all future HTTP connections to the same host — with no network request made
  2. Automatically upgrades http:// links to https:// before sending them
  3. Ignores certificate errors — the browser will show a hard error page instead of the "proceed anyway" option

The header looks like this:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

The attack HSTS prevents: SSL stripping

Without HSTS, the first visit to a site exposes a window of vulnerability. Here's what happens without HSTS when a user on a coffee shop Wi-Fi visits your site:

  1. User types example.com into the browser
  2. Browser sends a plain HTTP request: GET http://example.com/
  3. An attacker on the same network intercepts this before it reaches your server
  4. Attacker proxies the connection — serving the user over HTTP, connecting to your HTTPS server themselves
  5. User sees your site. Their browser thinks it's talking to http://example.com — no padlock
  6. Every form submission, cookie, session token is visible to the attacker in plaintext

This is an SSL stripping attack. It works because the browser's initial request is HTTP — the HTTPS redirect never has a chance to fire.

HSTS eliminates this attack by ensuring the browser never sends the initial HTTP request at all.

Directive breakdown

max-age

How long (in seconds) the browser remembers the HSTS policy:

Strict-Transport-Security: max-age=31536000
# 31536000 seconds = 1 year

Minimum recommended: 6 months (15768000)
Recommended: 1 year (31536000)
HSTS preload requirement: 1 year minimum

Setting a short max-age (under a month) provides little protection — the policy may expire before a user's next visit, and the window of vulnerability reopens.

Setting max-age=0 instructs the browser to delete the cached HSTS policy. Use this only when intentionally removing HSTS.

includeSubDomains

Extends the HSTS policy to all subdomains:

Strict-Transport-Security: max-age=31536000; includeSubDomains

Without includeSubDomains:

  • secure.example.com → protected ✓
  • login.example.com → not protected ✗
  • api.example.com → not protected ✗

An attacker can still SSL-strip connections to any subdomain not covered by HSTS, then set cookies on the parent domain (.example.com) that are sent with subsequent requests to your protected site.

Before adding includeSubDomains, verify that every subdomain serves valid HTTPS. A subdomain that only serves HTTP will become unreachable — the browser will refuse to connect over HTTP, but HTTPS won't work either.

preload

The preload directive signals that you want your domain included in browser preload lists. Preloaded domains are hard-coded into browser source code as requiring HTTPS — HSTS protection applies from the very first visit, before any HSTS header has been received.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Adding the directive alone doesn't add you to the preload list — you must also submit at hstspreload.org.

How HSTS preload lists work

The browser HSTS preload list (hstspreload.org) is maintained by Google and shipped with Chrome, Firefox, Safari, and Edge. The list contains domains that have opted into HSTS preloading.

Why preload matters: HSTS only protects from the second visit onwards — the first visit has no prior HSTS record, so the browser might send an HTTP request. Preloading eliminates this initial window.

Preload eligibility requirements

  • Serve a valid HTTPS response on the bare domain and all subdomains
  • Redirect all HTTP traffic to HTTPS
  • Set HSTS on the HTTPS response with:
    • max-age of at least 31536000 (1 year)
    • includeSubDomains present
    • preload present
  • All subdomains accessible over HTTPS

Preload removal is slow

Removing a domain from the preload list takes months — browsers ship the list with each release, and users may be running older versions. Only submit to preload if you are committed to HTTPS-only for the foreseeable future.

If you need to move a subdomain back to HTTP, you cannot — includeSubDomains prevents the browser from even attempting an HTTP connection.

Implementation by stack

nginx

# In your HTTPS server block
server {
    listen 443 ssl;
    server_name example.com www.example.com;
 
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
 
    # Also redirect HTTP to HTTPS
    # ...
}
 
# HTTP redirect block
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

The always parameter ensures the header is set on all responses, including error pages.

Apache

# In your VirtualHost (HTTPS only)
<VirtualHost *:443>
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>
 
# HTTP redirect
<VirtualHost *:80>
    Redirect permanent / https://example.com/
</VirtualHost>

Next.js

// next.config.ts
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains; preload',
          },
        ],
      },
    ];
  },
};
 
export default nextConfig;

Express / Node.js

const helmet = require('helmet');
 
app.use(
  helmet.hsts({
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  })
);

Vercel

In vercel.json:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Strict-Transport-Security",
          "value": "max-age=31536000; includeSubDomains; preload"
        }
      ]
    }
  ]
}

Vercel also enforces HTTPS at the platform level, but setting the header explicitly ensures it's present in scan results and compliance checks.

Cloudflare

Cloudflare's "Always Use HTTPS" and "HSTS" settings (under SSL/TLS → Edge Certificates) configure HSTS at the CDN edge. Enable it there, but also set the header at your origin for defense in depth — if a request bypasses Cloudflare, HSTS should still be present.

Common mistakes

Sending HSTS over HTTP: the header is silently ignored on plain HTTP responses. HSTS only takes effect when received over a valid HTTPS connection.

Setting on a subdomain with short max-age: a subdomain with max-age=86400 (1 day) provides minimal protection. Users who visit infrequently won't be protected between visits.

Not testing before adding includeSubDomains: verify every subdomain responds over HTTPS before enabling this. Use curl -I https://sub.example.com on each subdomain.

Adding preload without submitting: the preload directive in the header is a declaration of intent, not an automatic submission. Visit hstspreload.org to actually submit.

HSTS and certificate errors

HSTS changes browser behavior on certificate errors. Without HSTS, a user can click "proceed anyway" when they see a certificate error. With HSTS, this option is removed — the browser shows a hard block with no bypass.

This is intentional: allowing bypass would let a network attacker present a self-signed certificate and have the user accept it, re-enabling SSL stripping.

Be aware that this means any certificate error on a preloaded domain is a complete outage for that browser until the cert is fixed.

How PatchVex detects HSTS issues

The PatchVex Web Scanner:

  1. Requests both the HTTP and HTTPS versions of the URL
  2. Checks for Strict-Transport-Security on the HTTPS response
  3. Flags if absent
  4. Parses max-age — flags if under 15768000 (6 months)
  5. Notes absence of includeSubDomains
  6. Notes absence of preload
  7. Flags if the header appears on an HTTP response (ineffective)
  8. Checks that HTTP redirects to HTTPS (a prerequisite for HSTS)