Most nginx configs are one add_header away from leaking more than they should. Here's a 2026 security-header checklist you can actually verify — plus the inheritance gotcha that silently disables half of them.
The headers that matter
-
Strict-Transport-Security — force HTTPS for future visits.
max-age=63072000; includeSubDomains; preload. - X-Content-Type-Options: nosniff — stop MIME sniffing.
-
Content-Security-Policy — the big one. Even a minimal
frame-ancestors 'self'(clickjacking) and a scopeddefault-srcbeats nothing. Build it per app. - Referrer-Policy: strict-origin-when-cross-origin — stop leaking full URLs.
-
X-Frame-Options: SAMEORIGIN — legacy clickjacking guard (CSP
frame-ancestorssupersedes it, but harmless to keep). - Drop
server_tokens offto stop advertising your exact nginx version.
The gotcha that breaks everything
add_header does not inherit into a block that defines its own add_header. If you set security headers at server level and then add a single add_header inside a location (say, a cache header), every security header vanishes for that location — silently, no error.
Fix: define headers once at the broadest scope and don't re-declare add_header in child blocks, or repeat them (an include snippet of headers is the clean pattern).
Don't serve your dotfiles
A classic exposure — .git/, .env, .htpasswd reachable over HTTP:
location ~ /\.(?!well-known) { deny all; return 404; }
Verify, don't assume
Check what you actually send:
curl -sI https://example.com | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy'
If a header you configured isn't there, you probably hit the add_header inheritance trap.
I put these rules (plus TLS protocol/cipher and server_name checks) into nginx-doctor — point it at your config and it prints each finding with the fix. Zero-config Go binary, read-only.
Top comments (0)