← All guides

How to Enable HSTS on Nginx, Apache and Cloudflare

One header stops protocol-downgrade and cookie-hijacking attacks. Here's the exact line for each platform — and the one flag not to rush.

Do this now

If your site already serves everything over HTTPS (including subdomains), add the header. On nginx, inside the server block that terminates TLS:

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

Reload nginx, then point our security headers checker at your site — the HSTS row should flip green with the max-age it observed. Apache and Cloudflare equivalents are below, along with a safer ramp-up if you're not yet sure everything is HTTPS-clean.

What HSTS actually does

Strict-Transport-Security tells a browser: for the next max-age seconds, never talk to this host over plain HTTP — upgrade every request to HTTPS internally, and refuse to let the user click through certificate errors. That closes the gap your 301-redirect leaves open: with only a redirect, the first request a user types (example.com) still goes out over HTTP, and an attacker on the network can intercept it before your server ever answers — the classic SSL-stripping attack. With HSTS, after one successful HTTPS visit, that plaintext first request never happens again.

It's a browser-side promise, so two properties follow. First, it only protects visitors after their first HTTPS visit (the "trust on first use" gap — that's what preload addresses, below). Second, it's cached: once a browser has seen your header, it will hold you to it for the full max-age, even if you later remove the header. That's why the rollout order below starts small.

Nginx

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

Two nginx-specific traps. The always flag matters: without it, nginx omits the header on error responses (404s, 500s), and those responses need the protection too. And add_header directives are not inherited if a lower-level block (location, for example) declares any add_header of its own — the child's list replaces the parent's entirely. If some paths mysteriously lack the header, that inheritance rule is almost always why.

Apache

Requires mod_headers (a2enmod headers on Debian/Ubuntu, then restart). In the HTTPS virtual host:

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Same reasoning for always: it makes Apache set the header on non-2xx responses as well. Set it only in the TLS virtual host — the spec says browsers ignore HSTS received over plain HTTP, so sending it there does nothing but confuse audits.

Cloudflare

If Cloudflare fronts your site, you can enable HSTS at the edge without touching origin config: dashboard → SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS) → Enable. Cloudflare shows a deliberately scary confirmation first — read it, it's the same caveat this guide makes: if HTTPS ever breaks while the header is cached, visitors are locked out rather than downgraded. Pick your max-age and the include-subdomains toggle in the same panel. Setting it at the edge or at the origin is equivalent for browsers; just do it in one place so there's a single source of truth.

Roll out in stages

Because browsers cache the policy for the full max-age, a mistake with a one-year value is a one-year mistake. The boring, reliable sequence:

  1. Confirm everything is HTTPS. Every subdomain, every API endpoint, every internal tool on a subdomain — includeSubDomains covers them all, and an HTTP-only intranet.example.com becomes unreachable for anyone who visited the apex.
  2. Start with a short max-agemax-age=300 (five minutes) — and live with it for a few days. If something breaks, you're five minutes from recovery.
  3. Ramp up: a week (604800), then the value browsers and checkers expect: max-age=31536000 (one year) with includeSubDomains.

One thing HSTS does not replace: keep your HTTP→HTTPS 301 redirect. HSTS only applies to browsers that have already seen the header; the redirect still catches first-time visitors, curl scripts, and old clients. The two are complementary — the redirect gets everyone onto HTTPS once, HSTS makes sure browsers never leave.

The preload caveat

Adding preload to the header and submitting your domain at hstspreload.org gets you hardcoded into browsers' built-in HSTS lists — closing the first-visit gap entirely. It's the right end state for a domain that will be HTTPS forever, but understand what you're signing: removal from the preload list takes months to propagate through browser releases, and until it does, every subdomain must serve valid HTTPS or it's simply unreachable. Our recommendation: ship max-age=31536000; includeSubDomains without preload, run it cleanly for a few months, and only then decide whether preload's marginal gain is worth its permanence. The header alone already defeats the realistic attacks.

Verify it

Run your domain through the security headers checker after each change — it reports the exact HSTS value your server actually sends (not what your config intends), and grades the other six headers while it's there. If HSTS was your only miss, this is the cheapest grade bump you'll ever ship. If you're doing this as billable client work, the Vuln Assessment Report Toolkit turns findings like a missing HSTS header into a client-ready report.