Skip to content

Security

HSTS: enabling it safely and joining the preload list

Turn on Strict-Transport-Security without locking anyone out, raise max-age in three stages, and decide whether the browser preload list is worth a commitment that is hard to undo.

getReport teamUpdated 25 Sept 202611 min read

HSTS is one line of server configuration that tells browsers "this site is HTTPS only, do not even try HTTP". Done in the right order it takes a week of waiting and ten minutes of work. Done in the wrong order it can lock visitors out of a subdomain for a year. This guide walks through the safe order, the configs for nginx, Apache, Caddy and Cloudflare, and the extra step of joining the browser preload list.

Quick answer

  • Fix HTTPS first: a valid certificate on every host, and every http:// URL redirecting to https:// in one hop.
  • Send Strict-Transport-Security: max-age=300 for a day, then max-age=86400 for a week, then max-age=31536000.
  • Add includeSubDomains only after checking every subdomain, internal ones included, over HTTPS.
  • For the preload list: max-age=31536000; includeSubDomains; preload on the bare domain, then submit at hstspreload.org. Treat it as permanent.
  • Check each stage with the security headers checker.

Why HSTS matters

Most people type example.com, not https://example.com, and many old links, bookmarks and printed flyers still say http://. Each of those visits starts with one unencrypted request that your server answers with a redirect to HTTPS. On a café or hotel network, that first request is the moment someone in the middle can answer instead of you: keep the visitor on plain HTTP and read or change everything they send, including the login form.

HSTS closes that gap after the first visit (the HSTS learn page has the short definition). Once a browser has seen the header over HTTPS, it rewrites every http:// request for that host to https:// itself, before anything leaves the device, for the next max-age seconds. It also removes the "continue anyway" button on certificate warnings for that host, so a visitor cannot be talked into clicking past a fake certificate.

Two limits are worth knowing:

  • The first visit is still exposed. The browser has never seen the header, so it has nothing to remember. The preload list fixes that: browsers ship with a built-in list of HTTPS-only domains.
  • The header is ignored over HTTP. Browsers only accept HSTS from an HTTPS response, otherwise an attacker could inject it. Sending it from port 80 does nothing.

How getReport checks it

The tool requests the page as a browser would, follows redirects and reads the headers of the final HTTPS response. It shows the two HSTS findings. Every getReport report also requests the http:// version of the same address to see whether it redirects, and the SSL / TLS checker shows those two HTTPS findings next to the HSTS ones. All four matter here:

The HSTS finding on a site without a Strict-Transport-Security header: the warning title, the reason, the suggested header value and the evidence line saying no such header was sent
When the header is missing, the finding names the value to send and the evidence line confirms the response had no strict-transport-security.

What the checks accept, precisely:

  • hsts passes whenever the header is present with any max-age above zero. A header with max-age=0 is flagged, because it tells browsers to forget the policy.
  • hsts-preload-ready only runs when the header is present. It is "ready" when max-age is at least 31,536,000 seconds and both includeSubDomains and preload appear. It is informational and costs no points: joining the list is a choice, not a requirement.
  • It reads the header on the page you tested. The preload list reads it on the bare domain (example.com, not www.example.com), so test that address too.
  • It does not probe your subdomains. Whether intranet.example.com works over HTTPS is something only you can confirm.

Step by step

1. Get HTTPS right on every host

HSTS enforces whatever state HTTPS is in. If a certificate expires while HSTS is active, visitors see a warning they cannot click through. Before adding the header:

  • The https-enforced and http-to-https-redirect findings pass on the main host.
  • The certificate is valid and renews automatically (the SSL / TLS checker shows days to expiry and whether the chain validates).
  • No page loads http:// resources; see mixed content after moving to HTTPS.

2. Send a short max-age

Start with five minutes. If something breaks, browsers forget the policy almost immediately.

nginx, in the server block that listens on 443:

nginx
add_header Strict-Transport-Security "max-age=300" always;

always makes nginx send it on error pages too. One nginx trap: a location block with its own add_header does not inherit the ones from the server block. If you set headers in a location (a cache header for images, for example), repeat the HSTS line there.

Apache, inside the <VirtualHost *:443> block, with mod_headers enabled:

Apache
Header always set Strict-Transport-Security "max-age=300"

Caddy redirects HTTP to HTTPS automatically but does not add HSTS by itself. In the Caddyfile:

Caddyfile
example.com {
    header Strict-Transport-Security "max-age=300"
    reverse_proxy app:3000
}

Cloudflare: SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS). The dashboard only offers long max-age values (months, not minutes), so run the short test stages from the origin (or a Modify Response Header rule) and switch to the dashboard setting for the final value, with "Apply HSTS policy to subdomains" and "Preload" off until steps 4 and 5. Keep a single source: if Cloudflare sends the header, remove it from the origin.

WordPress without server access can send it from a small plugin or functions.php on the send_headers hook, as shown in security headers from zero to A. The server or CDN is better, because cached pages served without PHP will not carry it.

3. Raise it in stages

StageHeaderHow long to wait
Testmax-age=300A day of normal traffic
Settlemax-age=86400A week
Commitmax-age=31536000Permanent setting

Every raise is a promise to returning visitors: a browser that saw max-age=31536000 today will insist on HTTPS until this date next year, even if you remove the header tomorrow. The waiting time is there so you find problems while the promise is short.

4. Decide about includeSubDomains

includeSubDomains extends the policy to every subdomain of the host that sends it. On example.com, that means shop., mail., staging., intranet., printer. and anything else under it, including hosts that are not public.

Before adding it, list your subdomains. Your DNS zone is the most complete source. Check each one in a browser over https://. Typical casualties:

  • An internal tool or router admin page with a self-signed certificate, reached on the office network as nas.example.com.
  • A mailing or marketing platform with a custom tracking domain (click.example.com) that only works over HTTP.
  • An old subdomain pointing at a service that was never given a certificate.

If any of them cannot do HTTPS, fix it or move it to a different domain first. Once a visitor's browser has the policy, that subdomain is unreachable for them until the policy expires.

nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

5. Join the preload list, if you mean it

The preload list is compiled into Chrome, and Firefox, Safari and Edge use lists based on it. A preloaded domain is HTTPS-only from the very first visit, on every device, with no header seen yet.

The requirements published at hstspreload.org:

  1. A valid certificate.
  2. Redirect from HTTP to HTTPS on the same host: http://example.com must go to https://example.com first, not straight to https://www.example.com.
  3. All subdomains served over HTTPS, in particular www. if it has a DNS record.
  4. The HSTS header on the base domain's HTTPS response, with max-age of at least 31,536,000 seconds, includeSubDomains and preload.
  5. If the HTTPS base domain redirects (to www., say), the redirect response itself carries the header.
nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Then enter the domain at hstspreload.org. The site checks the requirements live and lists anything missing. After acceptance it takes weeks to months before the entry ships in browser releases.

Watch out

Preloading is effectively permanent. Removal is possible through the same site, but the removal has to ship in new browser versions, and people update slowly. Plan for every subdomain you will ever create under that domain to need HTTPS from day one, for years. If that is not realistic, stop at step 4.

For reference, getreport.app sends max-age=31536000; includeSubDomains; preload from Caddy, and .app is a top-level domain that is preloaded as a whole, so every .app site is HTTPS-only by design.

Platform notes

WordPress

Hosts that manage the web server often have an HSTS switch in their control panel; use it rather than a plugin when it exists. Security plugins can also send the header. Whichever you use, confirm with curl that only one Strict-Transport-Security line comes back, and that cached pages carry it.

Shopify

Shopify serves every store over HTTPS and controls the response headers, so there is no HSTS setting to change. Run the checker to see what your store sends; preloading needs control of the header on the bare domain, which the platform keeps.

Static sites / custom

On Netlify and Cloudflare Pages, add the header in a _headers file in the publish directory; on Vercel, in headers in vercel.json; or set it once at the CDN. Check the result with the tool, since some hosts already send their own value.

Verify

  • Run the security headers checker on the bare domain and on www.. The hsts finding passes, and at the final stage hsts-preload-ready reads "HSTS is ready for the browser preload list" if you chose to preload.
  • From a terminal, check the HTTPS response and the HTTP redirect:
Shell
curl -sI https://example.com/ | grep -i strict-transport
curl -sI http://example.com/ | grep -i -E "^HTTP|^location"

The first prints the header once. The second shows a 301 or 308 to https://example.com/, the same host.

  • In Chrome, chrome://net-internals/#hsts → "Query HSTS/PKP domain" shows whether your browser has stored the policy and its expiry. The same page can delete a dynamic entry while testing.
  • For the preload list, hstspreload.org shows the status of the domain: pending, preloaded, or the list of unmet requirements.

Common mistakes

  • Going straight to a year. A subdomain you forgot about breaks for a year for everyone who visited. Use the 300 → 86400 → 31536000 ladder.
  • includeSubDomains on a domain with internal hosts. The office NAS or a vendor's tracking subdomain stops working in browsers. Inventory the DNS zone before adding it.
  • Header only on www., preload requested for the bare domain. hstspreload.org rejects it. Send the header on https://example.com, including on its redirect to www..
  • Redirecting http://example.com straight to https://www.example.com. Visitors get there, but the preload check fails because the bare domain never set HSTS over HTTPS. Redirect to https://example.com first, then to www.; the redirect checker shows each hop.
  • Turning HSTS off by deleting the header. Browsers keep the old policy until it expires. To retract it, send max-age=0 over HTTPS for a while (the hsts check flags that value on purpose, so it is visible), then remove the header.
Check your site before and after Check