Edge platform
Content Security Policy on Cloudflare
Cloudflare gives you two places to set a header, a Transform Rule in the dashboard or a Worker in code, and several features that change your pages on the way through. The first decides what your policy can be. The features decide what it reports.
A complete report-only config
A Worker on the zone's route that adds the policy to HTML responses and passes everything else through untouched. Deploy it with npx wrangler deploy after changing the route to your own domain.
const ENDPOINT = "https://your-ingest-code.ingest.us.headerhawk.com/r";
const POLICY = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"img-src 'self' data:",
"font-src 'self'",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'self'",
`report-uri ${ENDPOINT}`,
"report-to csp-endpoint",
].join("; ");
export default {
async fetch(request): Promise<Response> {
const response = await fetch(request);
// A policy governs documents. Scripts, images and API responses pass
// through untouched.
const type = response.headers.get("content-type") ?? "";
if (!type.startsWith("text/html")) return response;
// headers.set replaces any policy the origin sent. If your origin already
// generates one - with a nonce, say - this Worker should not be setting it.
const withPolicy = new Response(response.body, response);
withPolicy.headers.set("Content-Security-Policy-Report-Only", POLICY);
withPolicy.headers.set("Reporting-Endpoints", `csp-endpoint="${ENDPOINT}"`);
return withPolicy;
},
} satisfies ExportedHandler;{
"name": "csp-report-only",
"main": "src/index.ts",
"compatibility_date": "2026-05-01",
// Run the Worker on every request to the zone, in front of the origin.
"routes": [{ "pattern": "example.com/*", "zone_name": "example.com" }],
}How this was checked: type-checked against the Workers runtime types and run with wrangler 4.86.0 in local mode in front of an nginx origin. HTML responses carry both headers, a JavaScript file passes through untouched, a policy sent by the origin is replaced, and the browser applies the policy and posts a report. It was not deployed to Cloudflare's network, so the features below were not exercised.
Gotcha
Transform Rules or a Worker: set, add, and where a nonce can come from
A response header Transform Rule needs no code, and offers two operations that sound alike. Set replaces a header the origin already sent. Add sends yours without removing the origin's. If your origin already has a policy, Set discards it and Add gives the browser two policies to apply.
A Transform Rule's value is fixed text or an expression over the request, so it cannot carry a nonce that matches the page. A policy built from hosts and hashes does not need one. A Worker can generate a nonce, but the same value has to be on the scripts, which means rewriting the origin's HTML. A Worker that stamps a nonce onto every <script> it finds also stamps it onto an injected one, the very script a nonce exists to stop. A nonce belongs wherever the HTML is generated.
The Worker in the config uses headers.set, so it behaves like Set. If your origin already generates a policy, keep it there and do not deploy this Worker in front of it.
Gotcha
Cloudflare features add scripts your policy never allowed
Several Cloudflare features change the page between your origin and the browser. Rocket Loader takes over how your scripts load. Web Analytics adds a beacon from static.cloudflareinsights.com that posts to cloudflareinsights.com. Bot detection adds a script under /cdn-cgi/challenge-platform/. None of them is in your repository, so a policy written from it misses all of them.
# What Cloudflare's CSP reference lists for two common features:
script-src 'self' ajax.cloudflare.com static.cloudflareinsights.com
connect-src 'self' cloudflareinsights.comBot detection's script is served from your own origin, so 'self' covers it, and if the policy in your response header uses a nonce, Cloudflare reads it and adds it to the scripts it injects. A nonce set in a <meta> tag is not supported for that. To keep a particular script out of Rocket Loader's hands, mark it:
<!-- Rocket Loader leaves this script alone -->
<script data-cfasync="false" src="/app.js"></script>In report-only mode these are often a new policy's first reports, and easy to misread as a script nobody on the team wrote, on every page. Check which Cloudflare features the zone has on before treating one as an injection.
Now see what it's blocking
A report-only policy blocks nothing. When a browser loads a page and finds something the policy would have refused, it posts a report to the endpoint in report-uri and report-to. Until something listens there, those reports go nowhere.
HeaderHawk is one place to send them. It accepts both report formats on the same URL, groups them by directive and blocked host so a thousand reports about one script read as one issue, and keeps extension and bot reports visible but out of alerts. Replace your-ingest-code in the config above with the code from your site's setup panel, or paste the URL the panel shows.
The Cloudflare integration guide covers that step: where the endpoint goes on Cloudflare, and how to confirm the headers are being served.
A policy is the first of several controls browsers enforce on the code your pages load, including Integrity-Policy, Permissions-Policy, COOP and COEP, and they all report to the same endpoint. What client-side security is covers the rest of that layer.
Start reading the reports
Free for 1 site. Leave the policy in report-only until the reports go quiet, then enforce it.