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
| Config | 200 page | 404 page |
|---|---|---|
add_header Strict-Transport-Security "…"; | Header sent | Missing |
add_header Strict-Transport-Security "…" always; | Header sent | Header 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.