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-inlinefor 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 defaultscript-src 'self'blocks random third-party JSstyle-src 'unsafe-inline'is often needed because Ghost themes and injected markup can rely on inline stylesimg-src 'self' data: https:avoids broken remote images in postsobject-src 'none'should be standard everywhereframe-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.
Real-world Ghost CSP with analytics and consent tools
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 therescript-srcuses a nonce plusstrict-dynamic- third-party domains are scoped to the directives that need them
connect-srcincludes websocket support where needed
A few things I’d always double-check:
- whether
default-srcreally 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-inlinefor 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.