Your nginx `server` block sets `add_header Content-Security-Policy ...`, yet the header is missing from some responses. What two rules about nginx's `add_header` explain that?
answer
- inheritance is all-or-nothing per level
- one add_header wipes the inherited set
- default applies only to certain status codes
- always removes the status filter
- add appends, it does not replace
basics
~20 snginx's add_header is inherited only when the lower level defines no add_header of its own — one add_header in a location silently drops every inherited security header. And without the always parameter, add_header applies only to a fixed set of success and redirect status codes, so error responses carry nothing.
solid answer
~50 sTwo independent rules bite here. First, inheritance is all-or-nothing: `add_header` directives are inherited from an enclosing level **only if the current level defines none**. A `location` that adds one unrelated header — a cache tag, a CORS field — silently discards the entire set of security headers from the `server` block, and nginx logs nothing about it. Second, by default `add_header` only fires for a specific list of status codes (200, 201, 204, 206, 301, 302, 303, 304, 307, 308); the `always` parameter, available since 1.7.5, makes it apply regardless. So your CSP is present on a 200 and absent on the 500 that renders an error page. The remedy is to keep the security header set in an `include`d snippet, re-include it in every location that adds any header of its own, and mark them all `always`. Verify with `curl -I` against both a good URL and a deliberately failing one, not just the homepage.
code
nginx · 18 linesserver {
listen 443 ssl;
server_name www.example.com;
include snippets/security-headers.conf;
location / {
proxy_hide_header Content-Security-Policy;
include snippets/security-headers.conf;
proxy_pass http://app;
}
location /static/ {
include snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000";
root /srv/www;
}
}go deeper
Know that add_header attaches a response header and that nginx only inherits those directives when the inner level defines none of its own. Remember the always keyword exists.
State both rules precisely and show the fix: keep the security set in an included snippet, re-include it wherever a location adds a header, and mark every one always.
Diagnose it from response bytes rather than the config — compare a 200, a static path and a 404 — and handle the duplicate-header case where an upstream also emits a policy.
Set the ownership boundary: decide whether the edge or the application emits security headers, make that a platform rule, and back it with an automated check on real responses so the next location block cannot quietly undo it.
## Rule one: inheritance is all-or-nothing nginx's documentation is explicit: `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. This is not "later wins" and it is not merging. It is replacement of the whole set. ```nginx server { add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header Content-Security-Policy "default-src 'self'" always; location / { proxy_pass http://app; # inherits all three } location /static/ { add_header Cache-Control "public, max-age=31536000"; # inherits NOTHING - all three security headers are gone } } ``` One innocuous caching header in one location removed three security headers from an entire path prefix. Nothing warns you: `nginx -t` passes, the config is legal, and the header simply is not there. This is the single most common way a CSP or HSTS rollout ends up partially applied, and it is a favourite interview question precisely because it punishes people who have read the directive list but never diffed real responses. The same rule governs several other nginx directive families that take a list — `proxy_set_header` behaves the same way across contexts — so the habit generalises. ### Living with it Put the set in one file and include it wherever you break inheritance: ```nginx # /etc/nginx/snippets/security-headers.conf add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Content-Security-Policy "default-src 'self'" always; ``` ```nginx location /static/ { include snippets/security-headers.conf; add_header Cache-Control "public, max-age=31536000"; } ``` Repetition is the price of the inheritance rule; an include keeps the repetition from drifting. ## Rule two: the status-code filter By default `add_header` adds the field only when the response code is one of 200, 201, 204, 206, 301, 302, 303, 304, 307 or 308. Every other status — 4xx, 5xx, and anything else — gets no header. That is worse than it first sounds. Error pages are exactly where an injected-content or clickjacking protection is most likely to matter, because error paths often render user-supplied fragments, and a 500 page served without a CSP is unprotected in the moment your application is already misbehaving. The `always` parameter, added in nginx 1.7.5, removes the filter: ```nginx add_header Content-Security-Policy "default-src 'self'" always; ``` There is very little reason to omit `always` on a security header. The one place to think is `Strict-Transport-Security`, where you genuinely do not want to assert a long max-age from a response you are not confident about — but even there, the usual advice is to send it always on the HTTPS listener and never on the plaintext one. ## Rule three, for completeness: add is not set `add_header` appends. If the upstream already sent the field, the client receives it twice, and browser handling of duplicated security headers varies — for CSP, multiple policies are intersected, which can break a page in ways that look nothing like a header problem. When you proxy to an application that may set its own security headers, decide who owns them. To let the edge own them: ```nginx proxy_hide_header Content-Security-Policy; add_header Content-Security-Policy "default-src 'self'" always; ``` If you need true replace-or-add semantics without hiding first, that is the third-party headers-more module's `more_set_headers`, not core nginx. ## Verifying Never accept "it is in the config" as evidence. Check the actual bytes on several classes of response: ```bash curl -sI https://www.example.com/ # a 200 from the app curl -sI https://www.example.com/static/x.js # a location that adds its own header curl -sI https://www.example.com/nope # a 404 ``` If the header is present on the first and absent on either of the others, you have identified which of the two rules bit you: a missing header on the `/static/` path is the inheritance rule; a missing header on the 404 is the status-code filter.
- Which response codes does `add_header` apply to without the `always` parameter?200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 — a fixed list of success and redirect codes. Every 4xx and 5xx is excluded, so error pages go out without your security headers. `always`, available since nginx 1.7.5, makes the header apply regardless of status, which is what you want for essentially every security header.
- The upstream already sends a Content-Security-Policy and nginx adds one too. What does the client get?Both. `add_header` appends rather than replaces, so two policies arrive and browsers intersect multiple CSPs — the effective policy is the strictest combination, which frequently breaks a page in a way that does not look like a header issue. Decide on one owner: `proxy_hide_header Content-Security-Policy;` before adding yours at the edge, or let the application own it entirely.
- How would you keep the inheritance rule from silently dropping headers as the config grows?Keep the set in a single snippet file and `include` it in every context that defines any `add_header` of its own, so the repetition cannot drift. Then verify with automated checks that fetch a representative URL per location class, including a deliberately failing one, and assert on the response headers. Configuration review alone does not catch this; only response bytes do.
saying these in an interview costs you the question
- Assuming add_header directives merge across levels
- Thinking a later add_header just overrides the earlier one
- Expecting security headers on 4xx and 5xx without always
- Believing add_header replaces a header the upstream already sent
- Verifying only the homepage and declaring the rollout done