EN DE

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.

Skip the trial and error: our scanner crawls your site and gives you the exact CSP you need, with all required origins already whitelisted. Copy-paste into Nginx, done.

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:

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.

Skip the guesswork — scan your site → Get the exact CSP for Nginx in 30 seconds, with every external resource your site loads already whitelisted