CSP for Ghost: Copy-Paste Policies That Actually Work

Table of Contents

Ghost is pretty clean from a CSP perspective, but the moment you add analytics, consent banners, custom themes, comments, or embeds, things get messy fast.

I’ve had the best results treating Ghost like this:

  • start with a tight baseline policy
  • add only what your theme and integrations actually need
  • avoid unsafe-inline for scripts if you can
  • accept that style-src 'unsafe-inline' is often the practical compromise on Ghost themes unless you control every template

This guide is a reference you can copy from and adjust.

What Ghost usually needs

A basic self-hosted Ghost install commonly needs:

  • same-origin assets: self
  • images from your own site and maybe https:
  • Ghost admin/API calls on the same origin
  • forms posting back to the same origin
  • no plugins like old-school WordPress, which helps a lot

If you run a plain Ghost blog with no third-party scripts, your CSP can be very small.

Minimal CSP for a plain Ghost site

This is the baseline I’d start with for a simple Ghost publication:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self';
  frame-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

Why this works

  • default-src 'self' keeps everything same-origin by default
  • script-src 'self' blocks random third-party JS
  • style-src 'unsafe-inline' is often needed because Ghost themes and injected markup can rely on inline styles
  • img-src 'self' data: https: avoids broken remote images in posts
  • object-src 'none' should be standard everywhere
  • frame-ancestors 'none' prevents clickjacking unless you intentionally embed your site elsewhere

If you want ready-made patterns for policy structure, https://csp-examples.com is useful.

Here’s a real CSP header from a production site using Google Tag Manager, Google Analytics, and Cookiebot:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MDI5MTljOWUtZDMxNC00ZmM2LWEzMmItMjRiNDQ4ZGVkNWVl' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

That’s a solid example of what “real CSP” looks like after a site grows up a bit.

A few things I like about it:

  • object-src 'none', base-uri 'self', form-action 'self', frame-ancestors 'none' are all there
  • script-src uses a nonce plus strict-dynamic
  • third-party domains are scoped to the directives that need them
  • connect-src includes websocket support where needed

A few things I’d always double-check:

  • whether default-src really needs third-party hosts at all
  • whether style-src 'unsafe-inline' can be reduced
  • whether every analytics domain is still in use

People tend to cargo-cult old CSP entries for years.

Copy-paste policies for common Ghost setups

1. Ghost with no third-party services

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'

2. Ghost with Google Analytics or Google Tag Manager

If you use GTM, you typically need both script loading and network connections:

Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; frame-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'

If your GTM container injects more scripts, you may need a nonce-based setup instead of a simple allowlist.

3. Ghost with Cookiebot

Content-Security-Policy: default-src 'self'; script-src 'self' https://*.cookiebot.com; style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'

4. Ghost with GTM, GA, and Cookiebot

This is the practical “marketing stack” version:

Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'

Nonce-based CSP for Ghost

If your Ghost theme injects inline scripts you trust, a nonce-based policy is cleaner than falling back to unsafe-inline for scripts.

Example policy:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{{NONCE}}' 'strict-dynamic' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; frame-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'

And your inline script in the theme:

<script nonce="{{NONCE}}">
  window.dataLayer = window.dataLayer || [];
</script>

The catch: Ghost does not natively hand you a CSP nonce variable in theme templates. So if you want proper per-request nonces, you usually generate and inject them at the reverse proxy or app layer.

That makes nonces possible, but not always pleasant.

Nginx config for Ghost

If you run Ghost behind Nginx, this is the simplest way to set the header.

Basic Ghost CSP

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'" always;

Ghost with GTM and Cookiebot

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'" always;

Use always. Without it, you’ll miss error responses, and that creates weird policy gaps.

Start with Report-Only first

If you’re tightening an existing Ghost site, don’t flip straight to blocking mode unless you enjoy breaking production on a Tuesday.

Use report-only first:

add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'" always;

If you want reporting, add a reporting endpoint you control and wire in report-uri or report-to based on your stack and browser support strategy. Check the official CSP docs on MDN or browser vendor docs before standardizing reporting behavior, because support is annoyingly uneven.

Common Ghost CSP breakages

Inline scripts in themes

A custom theme often includes stuff like this:

<script>
  document.documentElement.classList.add('has-js');
</script>

That will fail under script-src 'self'.

Your choices:

  • move it to an external JS file
  • use a nonce
  • use a hash
  • give up and use unsafe-inline for scripts, which I try hard to avoid

Best fix:

<script src="{{asset 'built/site.js'}}"></script>

Remote images in posts

Ghost editors paste images and embeds from all over the place. If you lock img-src to only self, old content will break.

Practical setting:

img-src 'self' data: https:;

That’s looser than ideal, but realistic for editorial sites.

Embeds and iframes

YouTube, Vimeo, newsletter forms, and comments usually fail because frame-src is too strict.

Example:

frame-src 'self' https://www.youtube.com https://player.vimeo.com;

Only add the providers you actually use.

Web fonts

If your Ghost theme pulls fonts from a CDN, you’ll need to allow both the font files and often the stylesheet origin.

Example:

style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com data:;

I prefer self-hosting fonts when possible. Less CSP noise, fewer third parties, better privacy story.

A hardened Ghost policy I’d actually ship

For a typical production Ghost blog with moderate integrations, this is a sane starting point:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  object-src 'none';

If I fully controlled theme code and injection points, I’d push further:

  • remove unnecessary hosts from default-src
  • replace inline scripts with external files or nonces
  • reduce img-src https: if editorial workflow allows it
  • self-host fonts and maybe analytics alternatives

Official docs worth checking

For exact directive behavior and browser quirks, the official references are:

That’s the combo I use: Ghost docs for how the platform renders things, MDN for how browsers will actually enforce the policy.

CSP on Ghost is manageable. The hard part isn’t Ghost itself. It’s every marketing, consent, analytics, and embed snippet someone pastes into the theme six months later. Keep the policy close to the code, review it whenever integrations change, and don’t let old third-party domains pile up forever.