tl;dr; When you use add_header
in a location
block in Nginx, it undoes all "parent" add_header
directives. Dangerous!
Gist of the problem is this:
There could be several
add_header
directives. These directives are inherited from the previous level if and only if there are noadd_header
directives defined on the current level.
From the documentation on add_header
The grand but subtle mistake
Basically, I had this:
server { server_name example.com; ...gzip... ...ssl... ...root... # Great security headers... add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection "1; mode=block"; ...more security headers... location / { try_files $uri /index.html; } }
And when you curl it, you can see that it works:
$ curl -I https://example.com [snip] X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Strict-Transport-Security: max-age=63072000; includeSubdomains; preload
The mistake I had, was that I added a new add_header
inside a relevant location
block. If you do that, all the other "global" add_headers
are dropped.
E.g.
server { server_name example.com; ...gzip... ...ssl... ...root... # Great security headers... add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection "1; mode=block"; ...more security headers... location / { try_files $uri /index.html; # NOTE! Adding some more headers here + add_header X-debug-whats-going-on on; } }
Now, same curl
command:
$ curl -I https://example.com [snip] X-debug-whats-going-on: on
Yikes! Now those other useful security headers are gone!
Here are your options:
- Don't add headers like that inside
location
blocks. Yeah, that's not always a choice. - Copy-n-paste all the general security
add_header
blocks into thelocation
blocks where you have to have "custom"add_header
entries. - Use an include file, see below.
How to include files
First create a new file, like /etc/nginx/snippets/general-security-headers.conf
then put this into it:
# Great security headers... add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection "1; mode=block"; ...more security headers... # More realistically, see https://gist.github.com/plentz/6737338
Now, instead of saying these add_header
lines in your /etc/nginx/sites-enabled/example.conf
change that to:
server { server_name example.com; ...gzip... ...ssl... ...root... include /etc/nginx/snippets/general-security-headers.conf; location / { try_files $uri /index.html; # Note! This gets included *again* because # this location block needs its own custom add_header # directives. include /etc/nginx/snippets/general-security-headers.conf; # NOTE! Adding some more headers here add_header X-debug-whats-going-on on; } }
(You need to use your imagination that a real Nginx config site probably has many different more complex location
directives)
It's arguably a bit clunky but it works and it's the best of both worlds. The right security headers for all locations and ability to set custom add_header
directives for specific locations.
Discussion
I'm most disappointed in myself for not noticing. Not for not noticing this in the Nginx documentation, but that I didn't check my security headers on more than one path. But I'm also quite disappointed in Nginx for this rather odd behaviour. To quote my security engineer at Mozilla, April King:
"add" doesn't usually mean "subtract everything else"
She agreed with me that the way it works is counter-intuitive and showed me this snippet which uses include files the same way.