Strict-Transport-Security header 'not set' in nginx: the two traps

Two nginx behaviours make the HSTS header disappear from some responses, even though it's in your config.

1. The header

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

Send it from the HTTPS server block. Browsers ignore HSTS received over plain HTTP. max-age=31536000 is one year. Only add includeSubDomains if every subdomain works over HTTPS.

2. Trap 1: without always, error pages have no header

Config200 page404 page
add_header Strict-Transport-Security "…";Header sentMissing
add_header Strict-Transport-Security "…" always;Header sentHeader sent

Without always, nginx adds headers only to some success and redirect codes (200, 204, 206, 301, 302, 303, 304, 307, 308, per the nginx documentation). Scanners that request a missing page then report "HSTS not set".

3. Trap 2: a nested block with its own add_header drops it

location /app/ {
    add_header Strict-Transport-Security "max-age=31536000" always;
    location /app/x/ {
        add_header X-Frame-Options SAMEORIGIN;   # only this header is sent here
    }
}

In our test, /app/x/ returned X-Frame-Options and no Strict-Transport-Security. nginx inherits add_header lines only into blocks that have none of their own. Repeat the HSTS line in every block that has add_header, or keep all security headers in one included file.

4. Check every kind of response

curl -sI https://your-site/ | grep -i strict
curl -sI https://your-site/does-not-exist | grep -i strict

Both should print the header. Build the rest of your security headers with the free security headers generator.

Frequently asked questions

Why does my scan say Strict-Transport-Security is not set when it's in nginx.conf?

In our test, add_header without 'always' left it off the 404 page, and a nested location with its own add_header dropped it. Add 'always' and repeat the line in blocks that set other headers.

Should HSTS be sent over HTTP?

No. Browsers ignore it over HTTP; send it on HTTPS responses and redirect HTTP to HTTPS.

What max-age should I use?

Start lower (for example 300 seconds) while testing, then one year (31536000) once every page works over HTTPS.

Written with AI assistance. Measured on 2026-10-01 with nginx:alpine 1.31.6 over HTTPS (self-signed test certificate) and curl -I, on existing pages and on a 404. PlainHeaders is our extension; the other methods are free.