---
title: "Content Security Policy Examples by Stack"
description: "Content Security Policy examples for nginx, Cloudflare, and Next.js — plus the add_header trap that silently drops your header. Grade yours free."
publishedAt: 2026-09-17
tags: ["csp", "content-security-policy", "web-security", "nginx", "cloudflare", "nextjs", "setup"]
faq:
  - question: "How do I add a Content-Security-Policy header in nginx?"
    answer: "Put add_header Content-Security-Policy \"…\" always; in the server block and reload. The always flag makes it apply to error responses too. If any location block declares an add_header of its own, repeat the full header set there — nginx drops every inherited header in that block."
  - question: "Why does my CSP header disappear on some pages?"
    answer: "Usually nginx inheritance. A location or if block that declares any add_header of its own discards all headers inherited from the parent level. Without always, the header is also omitted entirely on 4xx and 5xx responses. Re-declare the set, or on nginx 1.29.3+ use add_header_inherit merge."
  - question: "Should I use a Cloudflare Transform Rule or a Worker for CSP?"
    answer: "A Transform Rule for a static policy, a Worker for a per-request nonce. Transform Rules act on response headers, so they cannot stamp a matching nonce onto your script tags — that means rewriting the HTML body. Cloudflare's documented nonce pattern uses a Worker with HTMLRewriter."
  - question: "Why do I have two Content-Security-Policy headers?"
    answer: "Your origin and your CDN are both emitting one. Browsers enforce each policy independently and apply the intersection, so two non-overlapping allowlists block almost everything. Emit exactly one — on Cloudflare use the \"Set\" operation, not \"Add\" — and confirm with curl -sI."
  - question: "Can I use a CSP nonce with Next.js static pages?"
    answer: "No. Next.js applies nonces during server rendering from the incoming request's CSP header, so the docs require dynamic rendering. Static optimization and Incremental Static Regeneration are disabled, CDN caching needs extra configuration, and Partial Prerendering is incompatible. Use the experimental SRI hashes if you need static pages."
---
# Content Security Policy Examples for nginx, Cloudflare, and Next.js

<TLDR>
  A Content Security Policy is one HTTP response header, but every stack sets it
  differently and every stack has its own way of losing it. On nginx, any
  `location` block that declares its own `add_header` drops every header
  inherited from the server block, and without `always` the header never appears
  on a 4xx or 5xx page at all. On Cloudflare, a Transform Rule sets a static
  policy but cannot inject a per-request nonce — that needs a Worker. On
  Next.js, a nonce works, but it forces dynamic rendering, which disables static
  optimization and ISR.
</TLDR>

Most Content Security Policy examples are a bare header string with no server
attached. That is fine until you paste one into a config file and something
downstream quietly eats it — a policy that never arrives looks exactly like a
site with no policy at all.

This post carries one policy and deploys it three ways. Here it is, once:

```http

```

What each directive does, and which ones have no `default-src` fallback, is
covered in our [Content Security Policy guide](/learn/csp/). This post picks up
after that: three stacks, the header, the nonce, and the ways both go missing.

## Which mechanism does your stack use?

All three stacks set the same `Content-Security-Policy` response header. They
differ in one place that matters: whether the mechanism can produce a fresh
nonce per request. Stock nginx and Cloudflare Transform Rules cannot on their
own; a Cloudflare Worker and a Next.js proxy can.

<VerdictTable
  columns={[
    "Stack",
    "Where the header goes",
    "Per-request nonce?",
    "The one gotcha",
  ]}
  rows={[
    [
      "nginx",
      "add_header … always; in the server block",
      "Not in stock nginx",
      "A child location with its own add_header drops every inherited header",
    ],
    [
      "Cloudflare",
      "Transform Rule → Modify Response Header",
      "Only via a Worker",
      "“Add” appends a second policy; “Set” overwrites — appending gives you the intersection",
    ],
    [
      "Next.js",
      "headers() in next.config.js; nonce in proxy.ts",
      "Yes, in proxy.ts",
      "A nonce forces dynamic rendering — static optimization and ISR are disabled",
    ],
  ]}
  verdict="Static policy → any of the three, and the header is one line. Per-request nonce → nginx needs njs, Lua, or the upstream application; Cloudflare needs a Worker; Next.js needs proxy.ts and gives up static rendering in exchange."
  caption="Mechanism, nonce support, and the failure mode each stack is known for."
/>

Apache and Caddy follow the same shape, and
[the CSP validator](/tools/csp-validator/) renders both from a policy you paste
in — this post covers the three stacks where the deployment itself has teeth.

## nginx: the `add_header` line, and the three ways it disappears

In nginx, set the policy with
`add_header Content-Security-Policy "…" always;` inside the `server` block. The
`always` flag is not optional: without it, nginx adds the header only when the
response code is 200, 201, 204, 206, 301, 302, 303, 304, 307 or 308 — so your
404 and 502 pages ship with no policy at all. The parameter has been available
since nginx 1.7.5.

```nginx

```

**Trap 1: inheritance is all-or-nothing.** The `ngx_http_headers_module` docs
put it plainly — "These directives are inherited from the previous
configuration level if and only if there are no `add_header` directives defined
on the current level"
([nginx.org](https://nginx.org/en/docs/http/ngx_http_headers_module.html)). One
unrelated `add_header Cache-Control …` in a `location` block discards the
entire inherited set, including your CSP. This is a common way an nginx CSP
ends up present on the homepage and absent on `/api/`.
Three fixes: re-declare the full set in every child block that adds anything
(shown above); switch inheritance with `add_header_inherit merge`, which appends
parent values to the current level and **appeared in nginx 1.29.3**; or stop
branching entirely with the `map` pattern below.

**Trap 2: `always` and error paths.** With `error_page`, `add_header` has to be
declared in the location that actually _serves_ the error body, not the one that
generates the code. nginx trac #1324 (2017) was filed as `always` failing on a
`return 451` and closed `invalid` for exactly that: the `add_header` sat in the
location doing the `return`, while `error_page` redirected internally to the
location that served the response. Verify with `curl`, not by reading the
config.

**Trap 3: `if` blocks are a new context.** An `if` block behaves like a nested
`location` and drops inherited `add_header`s the same way. Rather than fight it,
compute the value in a
[`map`](https://nginx.org/en/docs/http/ngx_http_map_module.html) and emit a
single `add_header` outside any conditional:

```nginx

```

That snippet also names the alternative:
[`more_set_headers`](https://github.com/openresty/headers-more-nginx-module)
from the third-party headers-more module, whose inheritance runs the other way
(parent values survive into child blocks) and which covers all status codes by
default. Its current tag is **v0.40 (29 May 2026)**.

One trap we hit ourselves: nginx expands `$` inside `add_header` with no in-band
escape, so a literal `$` in a policy value stops nginx from **starting**, not
from serving. We found this building the validator's deploy matrix, which now
warns when a policy contains one; the workaround is `set $DOLLAR "$";` and then
`${DOLLAR}`. That same expansion is what makes an nginx nonce possible at all.

**The nonce, honestly.** Stock nginx has no cryptographically secure random
value to offer. The pattern people reach for is `$request_id` plus `sub_filter`,
and it works mechanically — but when Sergey A. Osokin suggested `$request_id` on
the
[nginx mailing list](https://mailman.nginx.org/pipermail/nginx/2021-October/061127.html),
nginx core maintainer Maxim Dounin replied the same day (19 October 2021) that
the request identifier "might not be cryptographically safe." CSP Level 3 §7.1
says a nonce SHOULD be generated by a cryptographically secure random number
generator. Ship it knowing that, or source the value from njs, OpenResty/Lua,
`set_secure_random_alphanum` in set-misc-nginx-module, or the upstream
application.

```nginx

```

Two details in that snippet make or break it: `sub_filter_once off`, so every
placeholder is replaced, and `proxy_set_header Accept-Encoding "";`, so
`sub_filter` sees uncompressed HTML rather than a gzip stream it cannot match.
Scott Helme's
[2017 write-up](https://scotthelme.co.uk/csp-nonce-support-in-nginx/) uses the
same `sub_filter` mechanics but takes the nonce from
`set_secure_random_alphanum` from the third-party set-misc module. It is not
the only in-nginx route to a CSPRNG — njs exposes `crypto.getRandomValues` and
OpenResty's Lua bindings reach one too — but it is the shortest to write.

<Figure
  src="/images/blog/csp-header-examples/csp-header-examples_nginx-header-drop_flowchart.svg"
  alt="Flowchart of one request through nginx: the server block sets Content-Security-Policy, then two checks decide whether it survives — does the matched location or if block declare any add_header of its own, and is the response 4xx or 5xx with no always parameter. Either yes means no policy on the wire."
  caption="The two checks between an add_header line and a policy the browser actually enforces. Either branch produces a response indistinguishable from a site with no CSP at all."
/>

## Cloudflare: Transform Rule or Worker?

Use a Response Header Transform Rule for a static policy and a Worker for a
per-request nonce. Transform Rules act on response headers; stamping a matching
nonce onto your `<script>` tags means rewriting the response body, which is a
different job.

```text

```

The operation matters more than the value.
[Cloudflare's own wording](https://developers.cloudflare.com/rules/transform/response-header-modification/)
is that **Set** overwrites "its previous value or adding a new header," while
**Add** works "without removing any existing headers with the same name." Pick
Add while your origin is already sending a policy and you now have two — see
the intersection section below. Usefully, these rules also apply to default
Cloudflare error pages and Custom Errors, so they cover responses your origin
never gets to serve.

For a nonce, Cloudflare's
[documented path is a Worker using `HTMLRewriter`](https://developers.cloudflare.com/workers/examples/spa-shell/),
which streams the response and sets the attribute without buffering the body:

```javascript

```

**Cloudflare's own scripts need entries in your policy**, and this is a common
self-inflicted breakage. Per
[Cloudflare's CSP guidance](https://developers.cloudflare.com/fundamentals/reference/policies-compliances/content-security-policies/):
Rocket Loader needs `script-src 'self' ajax.cloudflare.com`; Web Analytics needs
`script-src static.cloudflareinsights.com` plus
`connect-src cloudflareinsights.com`; and
[Turnstile and Managed Challenge](https://developers.cloudflare.com/turnstile/reference/content-security-policy/)
need `challenges.cloudflare.com` in `script-src` **and** `frame-src`. Zaraz is
the exception — Cloudflare modifies the CSP itself to keep it running. Otherwise
Cloudflare states it does not modify an origin policy.

Two constraints before you pick a surface.
[Content security rules](https://developers.cloudflare.com/client-side-security/faq/)
— Page Shield in older docs, Client-side security in the current dashboard —
only **add** headers, leaving any origin policy intact, and they do not support
nonce directives, so use a hash. Cloudflare Pages `_headers` is
static only, capped at
[100 header rules and 2,000 characters per header](https://developers.cloudflare.com/pages/platform/limits/).
Transform Rules counts are published inside a shared per-plan Rules bucket, so
confirm your own limit in the dashboard.

## Next.js: `next.config.js` for a static policy, `proxy.ts` for a nonce

Set a static policy with `headers()` in `next.config.js`. For a per-request
nonce you need `proxy.ts` — the file convention Next.js 16 renamed from
`middleware.ts` — which generates the nonce, sets it on both the `x-nonce`
request header and the CSP header, and lets Next.js stamp it onto framework
scripts for you.

```javascript

```

Because those values are fixed at build time, this route means `'unsafe-inline'`
or the experimental SRI hashes. The nonce path is a different file:

```typescript

```

Once that header is set, Next.js
[parses it, extracts the `'nonce-{value}'`](https://nextjs.org/docs/app/guides/content-security-policy)
and applies it automatically to framework scripts, page bundles, its own inline
scripts and styles, and any `<Script>` with the `nonce` prop. Read it in a
Server Component with `(await headers()).get('x-nonce')`.

**The naming change dates every competing tutorial.** Next.js 16 renamed
`middleware.ts` to `proxy.ts` and the export from `middleware` to `proxy`;
`middleware.ts` still works but warns, and the codemod is
`npx @next/codemod@canary middleware-to-proxy .`
([docs](https://nextjs.org/docs/messages/middleware-to-proxy)). If an example
you found still says `middleware.ts`, it predates this.

**The nonce has a documented price.** The docs state you "must use dynamic
rendering to add nonces," and list the consequences: static optimization and
Incremental Static Regeneration are disabled, pages cannot be cached by CDNs
without additional configuration, and Partial Prerendering is incompatible. Opt
an individual page in with `await connection()`. Separately, `'unsafe-eval'` is
a development-only requirement — React uses `eval` for debugging output, and
neither React nor Next.js use it in production by default. All of this follows
docs version 16.3.2 (last updated 2026-03-20), which recommends
v13.4.20+ for nonce handling.

Two real failure modes are worth recognising by name. Under Turbopack with
`output: 'standalone'`, no script in the response is stamped with the nonce —
the reproduction counts 12 `<script>` tags and zero `nonce` attributes — so a
nonce-present policy makes the browser ignore
`'unsafe-inline'` and block everything, and the page renders but never hydrates
([vercel/next.js #96063](https://github.com/vercel/next.js/issues/96063), 2026).
And a strict-CSP proxy runs on every link prefetch: one team reported
"~20-30 middleware execution per route just for a user scrolling down the whole
page. We reached our middleware execution month limit in just 3 days"
([#53928](https://github.com/vercel/next.js/issues/53928), 2023). The `matcher`
in the snippet above exists to prevent exactly that.

<Figure
  src="/images/blog/csp-header-examples/csp-header-examples_nonce-path-per-stack_diagram.svg"
  alt="Three lanes comparing where a per-request CSP nonce comes from: nginx uses $request_id with sub_filter, which is not a CSPRNG, so a real one needs njs, OpenResty Lua, set_secure_random_alphanum or the upstream application; Cloudflare cannot do it with a Transform Rule and needs a Worker using HTMLRewriter; Next.js generates it in proxy.ts and stamps it during server rendering, at the cost of dynamic rendering."
  caption="Where the nonce value is born on each stack, and what each path costs. The policy string is the same in all three lanes — only the source of the nonce differs."
/>

## Two `Content-Security-Policy` headers? The strictest one wins

When more than one `Content-Security-Policy` header arrives, the browser
enforces each policy independently and the effective result is the intersection
— the most restrictive combination — not the last one to arrive. Two policies
whose allowlists do not overlap collapse toward `'none'`.

The concrete case makes it click: `img-src img.example.com` in one policy and
`img-src more-images.example.com` in the other means **no images load at all**.
Neither host satisfies both policies, and both must pass
([content-security-policy.com](https://content-security-policy.com/examples/multiple-csp-headers/)).

In practice the duplicate comes from one of three places: the origin
application emits a policy and the CDN adds a second; a Cloudflare Transform
Rule set to "Add" instead of "Set"; or Page Shield content security rules, which
add a header by design. All three are invisible in any single config file, which
is why the check is on the wire and not in the repo:

```bash

```

## What breaks on the day you enforce

Most first-day CSP breakage is not your own code. It is a third-party script
that injects itself after the page is built, or a vendor that quietly moved an
asset to a new host.

**Rocket Loader** injects its own inline `<script>` after the page is built, so
a nonce or hash policy has nothing to match it against. Teleport hit this
exactly: `rocket-loader.min.js` refused to execute under
`script-src 'self' 'nonce-…'`
([gravitational/teleport #38376](https://github.com/gravitational/teleport/issues/38376),
February 2024). Allowlisting `ajax.cloudflare.com` does not fix it — a host
source never matches an inline script. Disable Rocket Loader for the zone, or
accept `'unsafe-inline'` and lose the nonce's value.

**A vendor SDK bump moves assets.** Mozilla's own tracker records a OneTrust SDK
bump
([mozilla/bedrock #14118](https://github.com/mozilla/bedrock/issues/14118),
January 2024) that both broke the banner's JavaScript and started pulling icons
from a host missing from `img-src` — the console named the exact blocked URL and
directive. The fix is allowlisting the new host; the lesson is that a third
party can break your policy without you deploying anything.

**`frame-ancestors` in a `<meta>` tag is zero protection.** CSP Level 3 ignores
`frame-ancestors`, `report-uri`, `report-to` and `sandbox` inside a
`<meta http-equiv>` policy
([mozilla/http-observatory #387](https://github.com/mozilla/http-observatory/issues/387)
is one scanner's 2019 encounter with it). Our CSP guide states that as an
authoring rule; the deployment consequence is that a `<meta>`-only rollout ships
with no clickjacking defence at all, however well the policy grades. Deliver it
as a response header.

> **Two nginx failures that surface as a 502, not a CSP error**
>
> A CSP long enough to exceed nginx's `proxy_buffer_size` — documented as
> `4k|8k`, one memory page, so platform-dependent — or a multi-line policy value
> joined incorrectly in the config, both surface as an opaque upstream error
> rather than anything naming Content Security Policy. If a policy edit and a
> new 502 arrive together, raise `proxy_buffer_size` and confirm the value is a
> single line before you look anywhere else.

None of this argues against enforcing. It argues for running the policy in
`Content-Security-Policy-Report-Only` first and reading what comes back.

## Directives to delete from an old CSP example

If you copied a policy from a guide written before 2023, five of its directives
now do nothing. Deleting them changes no behaviour and removes dead weight from
a header you are already trying to keep under a buffer limit.

```text

```

[`block-all-mixed-content`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/block-all-mixed-content)
is a no-op in Chromium and obsolete in Firefox and Safari: passive mixed content
is auto-upgraded and active mixed content is hard-blocked by default.
[`plugin-types`](https://chromestatus.com/feature/5742693948850176) was removed
from CSP Level 3 and from Chromium, and never implemented in Firefox. `referrer`
is obsolete in all engines, and
[`prefetch-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/prefetch-src)
is not defined in any specification. `navigate-to` never shipped anywhere.

One piece of old advice flipped the other way.
[`require-trusted-types-for`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for)
was Chromium-only for years, and every guide that told you to skip it is now
stale: Chrome has supported it since 83, Safari shipped it in Safari 26
(September 2025) and Firefox in 148 (February 2026), which is when it became
Baseline newly available. It is ignored rather than fatal where unsupported, so
it is safe to ship as a progressive enhancement.

## FAQ

### How do I add a Content-Security-Policy header in nginx?

Put `add_header Content-Security-Policy "…" always;` in the `server` block and
reload. The `always` flag makes it apply to error responses too. If any
`location` block declares an `add_header` of its own, repeat the full header set
there — nginx drops every inherited header in that block.

### Why does my CSP header disappear on some pages?

Usually nginx inheritance. A `location` or `if` block that declares any
`add_header` of its own discards all headers inherited from the parent level.
Without `always`, the header is also omitted entirely on 4xx and 5xx responses.
Re-declare the set, or on nginx 1.29.3+ use `add_header_inherit merge`.

### Should I use a Cloudflare Transform Rule or a Worker for CSP?

A Transform Rule for a static policy, a Worker for a per-request nonce.
Transform Rules act on response headers, so they cannot stamp a matching nonce
onto your script tags — that means rewriting the HTML body. Cloudflare's
documented nonce pattern uses a Worker with `HTMLRewriter`.

### Why do I have two Content-Security-Policy headers?

Your origin and your CDN are both emitting one. Browsers enforce each policy
independently and apply the intersection, so two non-overlapping allowlists
block almost everything. Emit exactly one — on Cloudflare use the "Set"
operation, not "Add" — and confirm with `curl -sI`.

### Can I use a CSP nonce with Next.js static pages?

No. Next.js applies nonces during server rendering from the incoming request's
CSP header, so the docs require dynamic rendering. Static optimization and
Incremental Static Regeneration are disabled, CDN caching needs extra
configuration, and Partial Prerendering is incompatible. Use the experimental
SRI hashes if you need static pages.

## Conclusion

Content Security Policy examples are the quick part; getting the header to
arrive on every response is the work. Four things carry across all three stacks:

- **One policy string, three deployments.** The same value carries unchanged
  onto nginx and Cloudflare; the Next.js static route is the exception — with no
  per-request value available it needs `'unsafe-inline'` or SRI hashes.
- **nginx**: `always` on every `add_header`, and re-declare the full header set
  in any child block that adds anything of its own.
- **Cloudflare**: a Transform Rule with the "Set" operation for a static policy,
  a Worker with `HTMLRewriter` when you need a nonce.
- **Next.js**: `next.config.js` for static, `proxy.ts` for a nonce — and accept
  that the nonce costs you static rendering.

<CTA
  title="Grade your policy before you deploy it"
  description="Paste the policy you are about to ship into the CSP validator. It parses the directives, grades them against CSP Level 3, and renders the config for your stack — including the Apache and Caddy targets this post does not cover."
  label="Open the CSP validator"
  href="/tools/csp-validator/#generate"
/>

Then check it on the wire. A policy that grades well in a text box and never
reaches the browser protects nobody.