Nginx Security Headers Configuration for Web Applications
Quick answer
Nginx can add important security headers to every response: Strict-Transport-Security to enforce HTTPS, X-Content-Type-Options to stop MIME sniffing, a frame...
Nginx can add important security headers to every response: Strict-Transport-Security to enforce HTTPS, X-Content-Type-Options to stop MIME sniffing, a frame protection policy, a Referrer-Policy, and a Content-Security-Policy. Use the always parameter so headers appear on error responses too, and watch out for the way add_header directives are inherited, which can silently remove headers.
Which Security Headers Matter Most?
Security headers instruct browsers to enable protections that are off by default. They are not a replacement for secure application code, but they reduce the impact of common problems like cross-site scripting, clickjacking, and protocol downgrades.
- Strict-Transport-Security: tells browsers to use HTTPS only for your domain for a given period.
- X-Content-Type-Options: with the value nosniff, prevents browsers from guessing content types.
- X-Frame-Options or frame-ancestors: controls whether other sites can embed your pages in frames, which defends against clickjacking.
- Referrer-Policy: limits how much URL information is sent to other sites.
- Content-Security-Policy: restricts where scripts, styles, and other resources can load from.
- Permissions-Policy: disables browser features your application does not use.
Browser support and recommended values change over time. Older headers such as the XSS filter header are deprecated in modern browsers, so check current guidance before copying configurations from older articles.
How Do You Configure Them in Nginx?
Use the add_header directive, ideally with the always parameter, which adds the header regardless of the response status. Without it, Nginx adds headers only to successful and redirect responses.
server {
listen 443 ssl;
server_name example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
}
Be careful with HSTS. Once a browser sees it, it will refuse plain HTTP for the whole period, and includeSubDomains extends that to every subdomain. Start with a short max-age while testing, make sure every subdomain supports HTTPS, and only then increase it. The preload option has long-term consequences, so read the preload requirements before using it.
What Is the add_header Inheritance Trap?
Nginx inherits add_header directives from the enclosing level only if the current level defines none of its own. If you define a header in the server block and then add any add_header inside a location block, the server-level headers disappear for that location.
This causes subtle bugs. A location that sets a Cache-Control header suddenly loses your security headers, and nobody notices until a scanner or an audit flags it. The usual remedies are to repeat all the headers in every location that adds its own, or to put the shared headers into a file and include it everywhere.
location /assets/ {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable";
}
Some distributions and builds include extra modules for managing headers more flexibly. If you use one, consult its documentation to understand how it interacts with the standard directive.
How Should You Roll Out and Verify Headers?
Add headers gradually and test each one, since a strict policy can break working functionality. Content-Security-Policy is the riskiest, and it benefits from a report-only phase before enforcement.
- Check responses with a command line client, including error pages and static files.
- Test in a staging environment with real pages, third-party scripts, and embedded content.
- Avoid setting the same header in both Nginx and the application, which can create duplicates or conflicts.
- Re-test after configuration changes, especially new location blocks.
- Use online or local scanners to audit your configuration periodically.
Also hide version information where practical, keep TLS settings modern, and remember that headers complement, not replace, input validation and output encoding in your application.
Make header checks part of your deployment pipeline. A small automated test that requests a few representative URLs and asserts the presence of the expected headers catches the inheritance problem the moment someone adds a new location block, long before an external audit finds it.
Frequently Asked Questions
Why are my headers missing on some responses?
Either you did not use the always parameter, so error responses lack them, or a location block with its own add_header overrode the inherited ones.
Is X-Frame-Options still needed?
The frame-ancestors directive in CSP is the modern way, though some setups keep both for older browsers.
Is HSTS safe to enable?
Yes, once your site works fully over HTTPS. Start with a short duration and expand gradually.
Should I set headers in Nginx or in the application?
Either works. Nginx suits site-wide defaults, while the application suits per-response policies. Avoid duplicating them.
Summary
Add core security headers with the always parameter, understand add_header inheritance, roll out HSTS and CSP carefully, and verify results regularly. Check current browser guidance and the Nginx documentation.