Skip to main content
All fixes

SSL, TLS, security headers

Add HSTS header to enforce HTTPS and prevent SSL stripping

Without Strict-Transport-Security, attackers can SSL-strip the first request. Set max-age=63072000; includeSubDomains; preload and submit to hstspreload.org.

What's happening

HSTS (HTTP Strict Transport Security) is an HTTP response header that tells browsers to use HTTPS-only for a given hostname for a specified duration. Without it, the very first request a browser makes to a hostname can be plain HTTP — and a network attacker (rogue WiFi hotspot, ISP injecting ads, nation-state) can intercept that request and serve a downgraded page that never gets upgraded to HTTPS.

The attack is called SSL stripping, popularized by sslstrip in 2009 and still effective today on any site without HSTS. The user's address bar shows http://example.com and they never see a TLS warning because the attacker is talking HTTPS to the origin and HTTP to the victim.

The fix is to add a Strict-Transport-Security header on every HTTPS response with a long max-age, includeSubDomains if you control all subdomains, and ideally the preload directive plus submission to hstspreload.org to be hardcoded into browser binaries.

Why it matters

Users on hostile networks (airports, hotels, conferences) are exposed to SSL stripping for the lifetime of their first visit. Once HSTS is set, subsequent visits are protected, but the first visit is the dangerous one and only preloading closes that gap.

Modern security frameworks (OWASP ASVS, NIST 800-218, CIS Benchmarks) all require HSTS on production HTTPS sites. Bug-bounty programs and penetration tests file findings against any production hostname missing the header.

Search engines do not directly rank for HSTS but Mozilla Observatory, SecurityHeaders.com, and SSL Labs penalize the missing header in their public grades, which feed into trust signals for security-conscious users.

Common causes

  • Web server configuration was migrated to HTTPS without ever adding the header.
  • CDN passes through origin headers but origin never set HSTS.
  • Header is set only on a non-canonical hostname (apex but not www, or vice versa).
  • Header is set with a tiny max-age like 60 seconds for testing and never increased.
  • Cloudflare HSTS toggle is disabled in the SSL/TLS panel.

Detect this on your site

Run a quick scan with the Full Site Audit. The tool surfaces this exact issue with the records and context needed to apply the fix below.

Open Full Site Audit

How to fix it

  1. 1

    Confirm the header is missing

    Run curl -sI https://example.com | grep -i strict-transport-security. If nothing prints, HSTS is not set on this hostname. Repeat for each hostname you serve — apex, www, and any subdomains.

  2. 2

    Decide on max-age and scope

    Use max-age=63072000 (2 years) for production. Use includeSubDomains only if every subdomain is HTTPS-ready — once set, http://internal.example.com will fail. Use preload only when you are committed long-term, because removal from the preload list takes weeks.

  3. 3

    Set the header in nginx

    Add add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; inside the HTTPS server block. The always parameter sends the header on error responses too, which the spec requires.

  4. 4

    Set the header in Apache

    Inside the VirtualHost on port 443, add Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload". Make sure mod_headers is enabled with a2enmod headers.

  5. 5

    Set the header on Cloudflare

    In the SSL/TLS panel under Edge Certificates, enable HSTS. Set max-age to 12 months minimum, enable Apply HSTS to subdomains, and enable Preload. Cloudflare overrides any origin HSTS values, so coordinate with origin config.

  6. 6

    Submit to the preload list

    Visit hstspreload.org and submit your domain. Requirements: serve a valid HTTPS cert on the apex, redirect HTTP to HTTPS on the apex, serve HSTS on the apex with max-age >= 31536000, includeSubDomains, and preload. Submission is reviewed by Chrome's team and rolls out to other browsers.

  7. 7

    Verify with browsers

    Open chrome://net-internals/#hsts, query the hostname. You should see static_sts (after preload) or dynamic_sts (immediately after first visit). Mozilla Observatory and SecurityHeaders.com will both grade the header.

Example

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

    # Required: 2-year max-age, all subdomains, preloadable
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}

Production-grade HSTS header in nginx

Frequently asked

Initial review by the Chrome team typically takes 4 to 8 weeks. Inclusion ships in the next Chrome release and propagates to Firefox Safari and Edge over the following months. Removal can take longer — months — so do not preload a domain you might decommission.

Browsers ignore HSTS on plain HTTP responses by spec. Set it only on HTTPS responses. Setting it on HTTP wastes bytes and creates confusion in scanner reports.

Every subdomain — internal-staging.example.com dev.example.com anything matching the suffix — must serve HTTPS or browsers will refuse to load it. Inventory subdomains before enabling. The preload directive makes this irreversible without a multi-week removal cycle.

Related fixes