Content Security Policy in Nginx
Deploying a CSP in Nginx is usually a one-line change in your server block. This guide covers the correct placement, the always flag that many tutorials skip, and the caveats around header inheritance in nested locations.
The minimal snippet
Open your site's configuration file (on Debian/Ubuntu usually /etc/nginx/sites-enabled/your-site, on RHEL/CentOS often /etc/nginx/conf.d/your-site.conf) and add this inside the server { } block:
server {
listen 443 ssl http2;
server_name example.com;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'" always;
# ... rest of your config ...
}
Then validate and reload:
sudo nginx -t
sudo systemctl reload nginx
Reload (not restart) is sufficient for header changes and doesn't drop existing connections.
Why the always flag matters
This is the single most commonly omitted detail in Nginx CSP tutorials, and it's a real security issue.
Without always, Nginx only applies add_header directives on 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses. That means your 404 Not Found, 403 Forbidden and 500 Internal Server Error pages have no CSP header at all. Those error pages often render user-controlled content (think: reflecting a bad URL back to the user), which is exactly where XSS bugs hide.
The always flag forces Nginx to apply the header to every response regardless of status code. Always use it for security headers.
Report-Only rollout
Deploy the header as Content-Security-Policy-Report-Only first. The browser logs violations to the console without blocking anything, so you can monitor for 1–2 weeks and fix the warnings before enforcing. Open your site, hit F12, and look for lines like:
[Report Only] Refused to load the script 'https://cdn.example.com/widget.js' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'".
Each line is a real resource your site loads that the policy would block. Either whitelist the origin or remove the dependency. Once the console is quiet, switch the header name from Content-Security-Policy-Report-Only to Content-Security-Policy and reload Nginx.
Common pitfalls
Nested add_header directives erase each other
This one catches everyone at least once. If you have add_header in a server block AND in a nested location block, the location block's headers completely replace the parent's — they're not merged. So adding a single header inside a location silently drops all the headers from the parent server context.
server {
add_header X-Frame-Options DENY always;
add_header Content-Security-Policy "default-src 'self'" always;
location /api/ {
add_header X-API-Version 1 always;
# WRONG: X-Frame-Options and CSP are NOT applied to /api/ requests.
# You have to repeat them here.
}
}
Fix: repeat all security headers in every location block that adds any header at all. Or use a map variable + single add_header at the server level to avoid duplication.
The header doesn't show up at all
Verify with curl -I https://your-site.example. If CSP is missing from the output, check:
- Is
nginx -tclean? Syntax errors silently skip directives. - Is your request hitting the right server block? Check
server_namematching. - Is there a
locationblock catching the request first and overriding headers? (See above.) - Is a proxy or CDN in front of Nginx stripping headers? Cloudflare, for example, doesn't strip CSP by default, but some misconfigured reverse proxies do.
Multiple CSP headers sent
If you have multiple add_header Content-Security-Policy ... directives (typically from included files), Nginx sends all of them. Browsers treat multiple CSP headers as the intersection, which is more restrictive than any individual policy. Result: things get blocked that you didn't expect. Consolidate into a single header.
HTTP/2 and header case sensitivity
HTTP/2 technically requires lowercase header names. Nginx already sends content-security-policy in lowercase on the wire regardless of how you write it in the config, so this usually isn't a problem. But if you're debugging with tools that show headers verbatim, don't be confused by the case difference.
Advanced: using map for conditional policies
If you want different CSPs for different parts of your site — say, strict on your marketing pages, looser on an admin panel — use an Nginx map block in the http { } context:
map $request_uri $csp_policy {
default "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
~^/admin/ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'";
}
server {
add_header Content-Security-Policy $csp_policy always;
}
This way you maintain a single add_header and route the right policy to the right paths.