Configure HSTS: Preloading, Risks, and Recovery | OpsBlu Docs

Configure HSTS: Preloading, Risks, and Recovery

How to configure HTTP Strict Transport Security correctly. Covers the HSTS preload list requirements, incremental rollout strategy, server...

What HSTS Does

HTTP Strict Transport Security tells browsers: "Never connect to this domain over HTTP. Always use HTTPS." After a browser receives the HSTS header, it automatically upgrades all future HTTP requests to HTTPS — even if the user types http:// or clicks an HTTP link. This happens client-side, before any network request is made.

Without HSTS, the first request to http://example.com goes over plaintext HTTP, and the server redirects to HTTPS. That plaintext request is vulnerable to interception:

Without HSTS:
  User → HTTP (plaintext, interceptable) → Server → 301 to HTTPS → User → HTTPS

With HSTS:
  User → Browser upgrades to HTTPS internally → HTTPS (never hits the wire as HTTP)

What HSTS prevents:

  • SSL stripping attacks — An attacker on the network intercepts the initial HTTP request and proxies a plaintext connection to the user while connecting to the server via HTTPS. The user sees HTTP in the address bar but doesn't notice.
  • Protocol downgrade attacks — Forcing a connection back to HTTP to intercept traffic.
  • Cookie hijacking — Cookies set without the Secure flag can be sent over HTTP. HSTS ensures no HTTP requests happen.

The HSTS Header

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Directive Meaning
max-age=31536000 Browser remembers HSTS for 31,536,000 seconds (1 year). After this time expires without a new header, HSTS is forgotten.
includeSubDomains Applies to all subdomains (www, api, cdn, mail, etc.). Without this, only the exact domain is protected.
preload Signals intent to be included in the browser's built-in HSTS preload list (see below).

Checking Your Current HSTS Configuration

# Check if HSTS header is present
curl -sI https://example.com | grep -i strict-transport-security

# Check from HTTP (should redirect to HTTPS, NOT serve content)
curl -sI http://example.com | head -5

# Check with verbose SSL details
curl -vI https://example.com 2>&1 | grep -i "strict-transport"

Online tools:

Common findings:

  • No HSTS header at all — Most common issue. The header simply isn't configured.
  • max-age too short — Values under 31536000 (1 year) don't qualify for preload and provide weaker protection.
  • Missing includeSubDomains — Subdomains remain vulnerable to SSL stripping.
  • HSTS on HTTP response — The header must only be sent over HTTPS. Browsers ignore HSTS headers received over HTTP (an attacker could inject a fake one).

Don't jump straight to max-age=31536000; includeSubDomains; preload. If something is wrong with your HTTPS configuration on any subdomain, HSTS will make that subdomain completely inaccessible. Roll out gradually:

Step 1: Short max-age, No Subdomains (1 week)

Strict-Transport-Security: max-age=300

Test for 1 week. If anything breaks, the 5-minute max-age means browsers forget HSTS quickly.

Verify: All pages load over HTTPS. No mixed content warnings. No certificate errors.

Step 2: Increase max-age (1 month)

Strict-Transport-Security: max-age=604800

One week max-age. Monitor for issues across all pages and subdomains.

Step 3: Add includeSubDomains (1 month)

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

Before this step, verify every subdomain has valid HTTPS:

  • www.example.com — valid cert, redirects HTTP → HTTPS
  • api.example.com — valid cert
  • cdn.example.com — valid cert
  • mail.example.com — valid cert (or doesn't serve web traffic)
  • Any other subdomain

If a subdomain doesn't support HTTPS, includeSubDomains will break it. Fix all subdomains before enabling this.

Step 4: Full Production + Preload

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

One year max-age. Ready for preload list submission.

HSTS Preload List

The preload list is a list of domains built into Chrome, Firefox, Safari, and Edge. Browsers enforce HSTS for preloaded domains from the very first visit — no initial HTTP request needed.

Requirements for Preload

From hstspreload.org:

  1. Valid HTTPS certificate (not self-signed).
  2. Redirect all HTTP traffic to HTTPS on the same host (http://example.comhttps://example.com, not to https://www.example.com).
  3. All subdomains must support HTTPS.
  4. HSTS header on the base domain (example.com, not just www.example.com) with:
    • max-age at least 31536000 (1 year)
    • includeSubDomains directive
    • preload directive

Submitting to the Preload List

  1. Meet all requirements above.
  2. Go to hstspreload.org and enter your domain.
  3. If all checks pass, submit your domain.
  4. Wait — the preload list is updated with each browser release (roughly every 6 weeks for Chrome). It can take 1–4 months for your domain to appear in production browser builds.

Removing from the Preload List

This is slow and difficult. Removal takes the same 1–4 month browser release cycle. If you preload a domain and then need to serve HTTP content, users will be locked out for months.

Only preload if you are committed to HTTPS on all subdomains permanently.

Server Configuration

nginx

server {
    listen 80;
    server_name example.com www.example.com;
    # Redirect all HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # HSTS header — 'always' ensures it's sent even on error responses
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # ... rest of config
}

nginx gotcha: add_header in a location block overrides add_header in the server block. If you have add_header in any location block, you must repeat the HSTS header there too. Alternatively, use the map directive or ngx_http_headers_more_module.

Apache

# In VirtualHost for port 80:
<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>

# In VirtualHost for port 443:
<VirtualHost *:443>
    ServerName example.com
    SSLEngine on

    # HSTS header
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>

Requires mod_headers enabled: a2enmod headers && systemctl reload apache2

Caddy

example.com {
    header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}

Caddy automatically handles HTTP → HTTPS redirection and TLS certificates. The HSTS header is the only thing you need to add explicitly.

IIS

<!-- web.config -->
<system.webServer>
    <httpProtocol>
        <customHeaders>
            <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
        </customHeaders>
    </httpProtocol>
    <rewrite>
        <rules>
            <rule name="HTTP to HTTPS" stopProcessing="true">
                <match url="(.*)" />
                <conditions>
                    <add input="{HTTPS}" pattern="off" />
                </conditions>
                <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
            </rule>
        </rules>
    </rewrite>
</system.webServer>

Cloudflare

DashboardSSL/TLS → Edge Certificates → HTTP Strict Transport Security → Enable.

Configure max-age (recommended: 12 months), enable Include subdomains, enable Preload. Cloudflare adds the header at the edge — you don't need to configure it on your origin server.

Recovering from HSTS Mistakes

Users Can't Access a Subdomain

Problem: You enabled includeSubDomains but staging.example.com doesn't have HTTPS.

Fix:

  1. Get a valid certificate for the subdomain (Let's Encrypt, or a wildcard cert).
  2. OR, if you can't add HTTPS to the subdomain, lower the max-age on the main domain to get browsers to forget faster.
  3. Users can manually clear HSTS for a domain in Chrome at chrome://net-internals/#hsts → "Delete domain security policies" → enter the domain.

Need to Temporarily Serve HTTP

Problem: You preloaded but need HTTP access for some reason.

Reality: You can't easily undo preloading. Browser-cached HSTS (non-preloaded) expires after max-age. Preloaded domains are hardcoded into browser builds. Submit a removal request at hstspreload.org and wait 1–4 months.

Prevention: This is why incremental rollout matters. Don't preload until you're certain all subdomains will stay HTTPS permanently.

Other Security Headers to Consider

HSTS is one piece of a complete security header configuration. Related headers:

Header Purpose
Content-Security-Policy Controls which resources the page can load (prevents XSS)
X-Content-Type-Options: nosniff Prevents MIME type sniffing
X-Frame-Options: DENY Prevents clickjacking via iframes
Referrer-Policy: strict-origin-when-cross-origin Controls referrer information in requests
Permissions-Policy Controls browser features (camera, microphone, geolocation)

Check all your security headers at securityheaders.com.