Two nginx rules silently remove security headers. A location that defines any add_header discards every add_header from the server level. And without the always parameter, headers are sent only for a short list of status codes, so 404 and 500 pages go out bare. Both are documented, and both are easy to miss.
Rule one: inheritance is all or nothing
The nginx docs say add_header directives are inherited from the previous configuration level if and only if there are no add_header directives defined on the current level.
So this config looks protective but is not:
server {
add_header X-Content-Type-Options nosniff always;
location /api/ {
add_header Cache-Control no-store;
}
}
Responses from /api/ carry Cache-Control but not X-Content-Type-Options. The location defined its own add_header, so the server level one no longer applies.
Newer nginx (1.29.3 and later) adds add_header_inherit. Set to merge, it appends values from the previous level. Set to off, it cancels inheritance. The default on keeps the old behavior. If you run an older build, repeat the headers in every location that sets its own, or use an include file.
Rule two: the response code list
Without always, the header is added only when the response code is 200, 201, 204, 206, 301, 302, 303, 304, 307 or 308. Everything else, including 400, 404, 403 and 500, goes out without it unless you add the always parameter.
This matters because error pages are exactly where attackers probe. A missing X-Content-Type-Options or Content-Security-Policy on an error page that reflects part of the URL is a real gap.
add_header Strict-Transport-Security "max-age=31536000" always;
A short audit method
- List every server and location block that contains add_header.
- For each location, confirm the headers you expect are present, either repeated or merged.
- Request one URL per status class (success, redirect, 404, 500) and read the response headers.
- Repeat for proxied paths, since an upstream may add or overwrite its own.
curl -sI https://example.com/ | grep -i -E 'strict-transport|x-content-type|content-security'
curl -sI https://example.com/does-not-exist | grep -i -E 'strict-transport|x-content-type|content-security'
If the second command prints less than the first, always is missing somewhere.
Header choices still need thought
Presence is not correctness. HSTS has a first visit gap, covered in HSTS explained. A Content Security Policy needs a rollout plan, see CSP report only rollout steps. Fixing inheritance only makes sure the headers you chose reach the client.
Try it yourself
Open the demo, run the web app scan, and look at which response header findings appear and which paths they come from.
Click through it at your own pace, or open it full screen.
Quick check
A location block has one add_header line. What happens to server level add_header values?
They are not inherited for that location, unless add_header_inherit merge is set on a version that supports it.
Does a 404 response get an add_header header without always?
No. Only the listed codes such as 200, 301 and 304 do. Add always to cover errors.
Sources
Last reviewed: 19 October 2026.