Security headers can disappear in Nginx even when they are correctly configured.
Some endpoints may silently drop important headers like:
- Content-Security-Policy
- Strict-Transport-Security
- X-Frame-Options
This behavior is subtle and has existed in Nginx for a long time.
How the Problem Appears
Everything looks correct at first.
curl -I https://example.com/api/health
Response headers include security policies as expected.
But then another endpoint is checked:
curl -I https://example.com/
And suddenly:
❌ Content-Security-Policy missing
❌ Strict-Transport-Security missing
❌ X-Frame-Options missing
Tools like securityheaders.com will immediately detect this.
The confusing part is that the headers are clearly defined in the configuration.
A Common Configuration
A typical setup may look like this:
server {
add_header Content-Security-Policy "...";
add_header Strict-Transport-Security "max-age=31536000";
location ~* \.html$ {
add_header Cache-Control "no-store";
}
}
The intention is simple:
- disable caching for HTML responses
- apply security headers globally
However, this configuration does not behave as expected.
What Actually Happens
Responses served from:
location ~* \.html$
will only include:
Cache-Control: no-store
All security headers defined in the server block disappear.
There are no warnings and no errors in the logs.
The headers are simply missing.
The Reason
This happens because of how Nginx handles header inheritance.
If a location block defines any add_header directive, it does not inherit add_header directives from parent blocks.
Even adding a single header like:
Cache-Control
causes Nginx to drop previously defined headers such as:
- Content-Security-Policy
- Strict-Transport-Security
- X-Frame-Options
- Referrer-Policy
- Permissions-Policy
This behavior has existed for many years and has caused confusion in many configurations.
The Previous Workarounds
Before recently, the usual approaches were:
1. Duplicating headers
Define all security headers again in every location block.
2. Using includes
Create a file like:
include security_headers.conf;
and include it everywhere.
Both approaches work but are difficult to maintain and easy to forget.
The Fix in Nginx 1.29+
Recent Nginx versions introduced a better solution:
add_header_inherit merge;
This directive changes how headers are inherited.
Instead of replacing headers defined in parent blocks, child blocks merge them.
This feature was introduced in newer Nginx releases and is documented in the official release notes:
https://blog.nginx.org/blog/nginx-open-source-1-29-3-and-1-29-4
A Safer Configuration
With the new directive, the configuration can be written like this:
server {
add_header_inherit merge;
add_header Content-Security-Policy "...security policy..." always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location ~* \.html$ {
add_header Cache-Control "no-store, no-cache, private, max-age=0" always;
}
}
Now the behavior is predictable:
- HTML responses are not cached
- Security headers are applied consistently
- Headers do not need to be duplicated
Final Takeaway
When using add_header inside location blocks in Nginx 1.29+, enabling the following directive is recommended:
add_header_inherit merge;
This prevents accidental loss of security headers and makes configurations easier to maintain.
Top comments (0)