Content Security Policy on Cloudflare Pages: Stop Guessing, Start With This
Content Security Policies are one of those things that look simple in theory and become quietly frustrating in practice. You add the headers, check a box in your mental security checklist, and then discover weeks later that your analytics isn’t tracking, your fonts aren’t loading, or a third-party script is silently blocked — none of which produces a visible error for your users.
On Cloudflare Pages specifically, there are a few non-obvious requirements. Here’s what actually works for a typical self-hosted stack.
What CSP does and why it’s worth the setup
A Content Security Policy tells browsers which sources are allowed to load scripts, styles, fonts, images, and make network requests from your page. Anything not on the allowlist gets blocked. It’s one of the most effective defenses against cross-site scripting (XSS) attacks, and it costs nothing beyond a header.
For GDPR purposes, a well-configured CSP also means you can demonstrate control over what’s loaded on your site — useful if you’re ever asked to justify your data processing.
Cloudflare injects a script you didn’t add
This is the one that catches almost everyone. When you deploy to Cloudflare Pages, Cloudflare automatically injects its own analytics beacon into your pages:
https://static.cloudflareinsights.com/beacon.min.js
You didn’t put it there. It’s not in your code. But it will be blocked by your CSP unless you explicitly allow it. The result: your CSP appears to work, but Cloudflare’s beacon fails silently.
Add both of these to your _headers file:
script-src ... https://static.cloudflareinsights.com;
connect-src ... https://cloudflareinsights.com;
Note the two different domains — the script is served from cloudflareinsights.com with a static. prefix, but the beacon sends data to the root domain.
Every external service needs both script-src and connect-src
This is the single most common CSP mistake. Developers add a domain to script-src, the script loads successfully, and they move on — not realizing the script’s network requests are being blocked by connect-src.
The rule: for any analytics, tracking, or third-party script, you need two entries:
- The domain where the script file is hosted →
script-src - The domain the script sends data to →
connect-src
These are often the same domain. But not always — Google Tag Manager loads from googletagmanager.com but fires events to google-analytics.com. Missing either entry causes silent failure.
A working example for a self-hosted stack
Here’s what a _headers file looks like for a site using self-hosted Plausible, Cloudflare insights, and no other third-party scripts:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com https://plausible.yourdomain.com; connect-src 'self' https://cloudflareinsights.com https://plausible.yourdomain.com; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
For each service you add, extend script-src with the script’s host and connect-src with where it sends data.
How to debug a CSP violation
Open browser DevTools → Console. Every blocked resource produces a specific error:
Refused to load script from 'https://...' because it violates the following Content Security Policy directive: "script-src ..."
The error names the exact domain and the exact directive it violated. Fix the entry for that directive, redeploy, re-check. Repeat until the console is clean.
One practical tip: deploy CSP in report-only mode first by using Content-Security-Policy-Report-Only instead of Content-Security-Policy. Violations are logged but not blocked, letting you audit what would break before enforcing anything.
unsafe-inline — when to use it and when not to
You’ll often see 'unsafe-inline' in script-src examples. This allows inline scripts (e.g. <script> tags directly in HTML), which are common in most Astro, Next.js, and similar setups. It weakens the XSS protection but is generally acceptable for sites that don’t accept user-generated content.
For stricter setups: use nonces or hashes instead of unsafe-inline. Astro supports nonce-based CSP with some configuration. It’s more work but gives you full XSS protection.
If you’re deploying on Cloudflare Pages and want to get CSP right from the start without spending a day debugging it, we cover this as part of our web development service.
Want to learn more?
See how we set up and operate GDPR-compliant self-hosted infrastructure at a fraction of SaaS costs.
Learn more