# 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.

Updated 2026-09-25 · Security · HTML version: https://getreport.app/guides/hsts-safely-and-the-preload-list

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](https://getreport.app/tools/security-headers).

## 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](https://getreport.app/learn/hsts) 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

> **Free tool:** [Security headers checker](https://getreport.app/tools/security-headers): Check HSTS, CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy and cookie flags on any site. Free, no signup, with a fix for every missing header.

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](https://getreport.app/tools/ssl-check) shows those two HTTPS findings next to the HSTS ones. All four matter here:

> **Check: Strict-Transport-Security header is set.** HSTS tells browsers to always use HTTPS for your site, so after the first visit a typed address or an old http:// link never starts on an unencrypted connection. Only the preload list covers the very first visit.
>
> 1. Send the header: Strict-Transport-Security: max-age=31536000; includeSubDomains.
> 2. Start with a shorter max-age (e.g. 86400) if you are not sure every subdomain supports HTTPS.

> **Check: HSTS is ready for the browser preload list.** Browsers ship a built-in list of HTTPS-only sites; being on it protects even a visitor's very first request. To qualify, the header needs max-age of at least 31536000 s (1 year), includeSubDomains and preload.
>
> 1. Send the header: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload.
> 2. Submit the domain at hstspreload.org once every subdomain works over HTTPS.

> **Check: The site is served over HTTPS.** Browsers label HTTP pages "Not secure" and Google uses HTTPS as a small ranking signal. Anyone on the network can read or alter what visitors see.
>
> 1. Get a certificate (Let's Encrypt is free) and enable HTTPS on your host.
> 2. Redirect every http:// URL to https:// with a 301.

> **Check: http:// redirects to https://.** Visitors who type your address without https, or follow an old link, land on the unencrypted page. Every one of those visits can be read or altered on the network.
>
> 1. Redirect every http:// URL to the same path on https:// with a 301.
> 2. nginx: return 301 https://$host$request_uri; Apache: RewriteRule in .htaccess; most hosts and Cloudflare have a "Always use HTTPS" switch.

![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](https://getreport.app/guides/img/hsts-safely-and-the-preload-list/finding.webp "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](https://getreport.app/guides/mixed-content-after-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:

```caddy
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](https://getreport.app/guides/security-headers-from-zero). The server or CDN is better, because cached pages served without PHP will not carry it.

### 3. Raise it in stages

| Stage | Header | How long to wait |
| --- | --- | --- |
| Test | `max-age=300` | A day of normal traffic |
| Settle | `max-age=86400` | A week |
| Commit | `max-age=31536000` | Permanent 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](https://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:

```bash
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](https://getreport.app/tools/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.
