Content Security Policies
Content Security Policy (CSP) is a browser security feature that restricts what resources a web page can load and from where. A well-configured CSP prevents cross-site scripting (XSS) attacks by refusing to execute scripts, load stylesheets, or display images that do not come from approved sources.
The source material covers CSP-Builder: a PHP/Composer tool for generating CSP headers from JSON files. This adds a dependency and a build step to what is essentially a single header line. This page writes CSP headers directly in nginx, which is simpler, more transparent, and does not require PHP or Composer on the web server.
Understanding CSP directives
A CSP header is a Content-Security-Policy response header containing a semicolon-separated list of directives. Each directive names a resource type and the sources permitted for that type.
The most important directives:
| Directive | Controls |
|---|---|
default-src | Fallback for all resource types not explicitly listed |
script-src | JavaScript sources |
style-src | CSS stylesheet sources |
img-src | Image sources |
font-src | Web font sources |
connect-src | Fetch, XHR, WebSocket, EventSource |
frame-src | Iframe sources |
frame-ancestors | Who can embed this page in a frame (replaces X-Frame-Options) |
form-action | Where forms can submit |
base-uri | Restricts <base> element URLs |
object-src | Plugin sources (Flash, Java applets) |
worker-src | Web Worker and Service Worker sources |
media-src | Audio and video sources |
manifest-src | Web app manifest sources |
The most important source values:
| Value | Meaning |
|---|---|
'none' | No sources permitted |
'self' | Same origin as the page |
'unsafe-inline' | Allow inline scripts/styles (weakens CSP significantly) |
'unsafe-eval' | Allow eval() and similar (weakens CSP significantly) |
https://example.com | Specific origin |
https: | Any HTTPS source |
data: | Data URIs |
blob: | Blob URLs |
'nonce-xyz' | Specific nonce value (per-request) |
'sha256-abc' | Specific hash of inline content |
Deprecated and removed directives
Several directives from the source material are no longer valid:
block-all-mixed-content: deprecated, useupgrade-insecure-requestsinsteadplugin-types: removed from the CSP specificationreflected-xss: was never a valid CSP directive (confused with X-XSS-Protection)referrer: obsolete, use theReferrer-Policyheader insteadreport-uri: deprecated in favour ofreport-to
Do not include these in new CSP headers.
The reporting approach
Before deploying a strict CSP, use report-only mode to detect violations without breaking anything:
# Add to a virtual server during CSP development
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report;" always;
The browser sends violation reports to /csp-report as JSON POST requests. Log them to a file for analysis:
location /csp-report {
access_log /var/log/nginx/csp-violations.log main;
return 204;
}
Review the violations before switching from Content-Security-Policy-Report-Only to Content-Security-Policy. Switch when violations are either fixed (legitimate content blocked) or confirmed as attacks (content that should be blocked).
Per-service policies
CSP is set per virtual server because each application has different requirements. A static site can have a very strict policy. Nextcloud needs a more permissive one because it loads resources dynamically.
Static website: strictest policy
For a site serving only static HTML with no external resources:
add_header Content-Security-Policy "
default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
upgrade-insecure-requests;
" always;
default-src 'none' denies everything not explicitly allowed. Each directive then opens exactly what the site needs. frame-ancestors 'none' prevents the page from being embedded in any frame. This is the most restrictive policy available and appropriate for simple content sites.
SnappyMail webmail
SnappyMail needs inline scripts and styles for its interface:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors 'self';
form-action 'self';
base-uri 'self';
upgrade-insecure-requests;
" always;
unsafe-inline and unsafe-eval are unfortunately necessary for most webmail clients. The risk is mitigated by the fact that SnappyMail is only accessible internally and serves a known audience.
Nextcloud
Nextcloud sets its own Content-Security-Policy header via PHP. nginx’s add_header would add a second CSP header alongside Nextcloud’s, which causes unpredictable browser behaviour. The correct approach is to use proxy_hide_header to remove Nextcloud’s header and replace it with a known-good one, or to let Nextcloud’s header stand and not set one in nginx.
The simplest approach for a self-hosted Nextcloud: let Nextcloud manage its own CSP and do not set one in nginx:
# For Nextcloud: let the application manage its own CSP
# Do not add Content-Security-Policy in the nginx server block
# Remove the application's header and replace with a controlled one (optional)
# proxy_hide_header Content-Security-Policy;
# add_header Content-Security-Policy "..." always;
If replacing Nextcloud’s CSP, the policy must permit everything Nextcloud needs:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' data:;
connect-src 'self' wss://nextcloud.yourdomain.net;
media-src 'self';
frame-src 'self';
frame-ancestors 'self';
form-action 'self';
base-uri 'self';
worker-src 'self' blob:;
upgrade-insecure-requests;
" always;
Kavita ebook server
Kavita is a modern SPA (Single Page Application) that loads resources dynamically:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' wss://books.yourdomain.net;
media-src 'self' blob:;
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
worker-src 'self' blob:;
upgrade-insecure-requests;
" always;
PostfixAdmin (internal admin interface)
Internal admin interfaces can have stricter policies since they serve a known audience with known requirements:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self';
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
upgrade-insecure-requests;
" always;
Mail autoconfig endpoint
A pure XML endpoint with no HTML or browser interaction:
add_header Content-Security-Policy "default-src 'none';" always;
Using CSP snippets
For sites with the same policy requirements, create a CSP snippet in /etc/nginx/snippets/:
Create /etc/nginx/snippets/csp-static.conf:
#
# CSP for static content sites
# /etc/nginx/snippets/csp-static.conf
#
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; upgrade-insecure-requests;" always;
Create /etc/nginx/snippets/csp-spa.conf:
#
# CSP for Single Page Applications
# /etc/nginx/snippets/csp-spa.conf
#
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; worker-src 'self' blob:; upgrade-insecure-requests;" always;
Include in virtual servers:
# Static site
include snippets/csp-static.conf;
# Single Page Application
include snippets/csp-spa.conf;
Testing CSP
Browser developer tools
The browser’s developer console reports CSP violations as errors. Open the developer tools (F12), navigate to the Console tab, and load the page. CSP violations appear in red with details about what was blocked and which directive was responsible.
Online CSP analyser
The Mozilla Observatory analyses CSP headers and other security headers:
https://observatory.mozilla.org/
Enter the hostname and run an analysis. The CSP section shows which directives are present, flags issues, and suggests improvements.
CSP Evaluator
Google’s CSP Evaluator checks a policy for weaknesses:
https://csp-evaluator.withgoogle.com/
Paste the policy value (without the add_header line) and it checks for common bypass vectors.
Checking headers from the command line
# Check the CSP header from the server
curl -I https://yourdomain.net | grep -i content-security-policy
# Check all security headers
curl -I https://yourdomain.net | grep -iE "content-security-policy|x-frame-options|strict-transport|referrer-policy|permissions-policy|x-content-type"
The add_header inheritance problem
nginx’s add_header directives do not inherit between contexts. If a location {} block has any add_header directive, it overrides all add_header directives from the enclosing server {} block for that location.
This is the most common CSP mistake in nginx configurations. A location block that adds one header silently removes all headers set at the server level, including the CSP.
The solution: either include all required headers in every location block that has add_header, or use the always flag and set headers only at the server level without any add_header in location blocks.
The snippet approach used in this series sets all security headers at the server level. Location blocks use proxy_pass and include snippets/proxy-headers.conf which does not use add_header, preserving the server-level headers.
CSP is the most impactful security header available but also the most likely to break things if set incorrectly. The report-only approach, monitoring violations before enforcing, is the right way to deploy a new policy. Start permissive, tighten gradually, and verify with the browser console before concluding a policy is correct.