CSP for Umami Analytics: Copy-Paste Policy Examples
Table of Contents
If you add Umami to a site with a locked-down Content Security Policy, it usually fails in one of two places:
- the script never loads
- the browser blocks the tracking requests
So the CSP work for Umami is usually pretty small: script-src and connect-src, sometimes img-src if you use fallback behavior or custom setups.
I like Umami because it’s simpler than the usual analytics stack. That also means the CSP is usually cleaner than the Google Tag Manager + Google Analytics + consent-manager soup you see in production.
For contrast, here’s a real-world CSP from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-OTk3MTdhNWItZGZhMC00MmJmLTk3NWQtMmFkNTU0OTlmNWZh' '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 policy is doing a lot. Umami usually doesn’t need anything close to that.
What Umami needs in CSP
At minimum, Umami usually needs:
script-srcto allow the Umami script URLconnect-srcto allow requests back to the Umami tracking endpoint
Depending on your setup, you may also need:
img-srcif you allow analytics-related image beacons or broad remote imagesworker-srcorstyle-src— uncommon for standard Umami installsframe-src— generally not needed
The two common Umami setups
There are really only two setups that matter for CSP:
- Self-hosted Umami on the same origin
- Umami hosted on another domain, including Umami Cloud or a dedicated analytics subdomain
The policy changes a lot depending on that split.
1) Same-origin Umami
If your site and Umami script are both served from the same origin, CSP is easy.
Example script:
<script defer src="/script.js" data-website-id="YOUR-WEBSITE-ID"></script>
Example CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self';
connect-src 'self';
img-src 'self' data:;
style-src 'self';
font-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Copy-paste version on one line:
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
Why this works:
- the Umami script loads from your own origin, so
script-src 'self'covers it - tracking requests also go back to your own origin, so
connect-src 'self'covers them
This is the cleanest option by far.
2) Umami on a separate analytics domain
A lot of teams put Umami on something like:
https://analytics.example.comhttps://stats.example.com
Then the app stays on https://www.example.com.
Example script:
<script
defer
src="https://analytics.example.com/script.js"
data-website-id="YOUR-WEBSITE-ID">
</script>
Minimal CSP:
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; img-src 'self' data:; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
That’s the version I’d start with.
If you also proxy events through a different endpoint, add that endpoint to connect-src, not script-src.
Example:
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com https://events.example.com; img-src 'self' data:; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
3) Umami Cloud or any third-party Umami host
If your script is loaded from a third-party Umami host, allow that exact origin.
Example:
<script
defer
src="https://cloud.umami-host.example/script.js"
data-website-id="YOUR-WEBSITE-ID">
</script>
CSP:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cloud.umami-host.example; connect-src 'self' https://cloud.umami-host.example; img-src 'self' data:; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
I strongly recommend using the exact origin instead of broad wildcards. Don’t do this unless you truly need it:
script-src 'self' https://*.example.com;
connect-src 'self' https://*.example.com;
Wildcards are easy to ship and annoying to audit later.
Copy-paste policies by scenario
Minimal policy for same-origin Umami
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
Minimal policy for Umami on analytics.example.com
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
Strict site policy with Umami allowed
This is a better baseline for production apps:
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; img-src 'self' data:; style-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none';
Report-Only policy for testing Umami
When I’m adding analytics to a hardened site, I usually ship it in report-only first.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; img-src 'self' data:; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
That lets you verify whether Umami is blocked without breaking production traffic.
If you use nonces or strict CSP
A lot of modern apps use nonce-based CSP:
script-src 'self' 'nonce-RANDOM123';
If Umami is loaded as an external script tag, you still need the Umami origin unless your policy relies on nonce + strict-dynamic behavior and the script is injected by a trusted nonced script.
Typical static script tag example:
<script
nonce="{{ .CSPNonce }}"
defer
src="https://analytics.example.com/script.js"
data-website-id="YOUR-WEBSITE-ID">
</script>
Corresponding CSP:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RANDOM123' https://analytics.example.com; connect-src 'self' https://analytics.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
If you use 'strict-dynamic', test carefully. It changes how host allowlists are interpreted in supporting browsers. The headertest.com policy above is a good example of a more advanced script policy:
script-src 'self' 'nonce-OTk3MTdhNWItZGZhMC00MmJmLTk3NWQtMmFkNTU0OTlmNWZh' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
That kind of policy is fine, but it’s easy to misunderstand. For Umami, I’d keep it boring unless the rest of your app already depends on nonce-based loading.
Nginx example
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; img-src 'self' data:; style-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always;
Apache example
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; img-src 'self' data:; style-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';"
Express / Node example
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' https://analytics.example.com; " +
"connect-src 'self' https://analytics.example.com; " +
"img-src 'self' data:; " +
"style-src 'self'; " +
"font-src 'self'; " +
"object-src 'none'; " +
"base-uri 'self'; " +
"frame-ancestors 'none';"
);
next();
});
Common CSP mistakes with Umami
1) Allowing the script but forgetting connect-src
This is the classic one.
You allow:
script-src 'self' https://analytics.example.com
but forget:
connect-src 'self' https://analytics.example.com
Result: the script loads, but event collection fails.
2) Adding Umami to default-src only
Some apps still rely too much on default-src. Don’t assume it’s enough.
Be explicit:
script-src 'self' https://analytics.example.com;
connect-src 'self' https://analytics.example.com;
3) Using a wildcard when one host would do
This:
script-src 'self' https://*.example.com;
connect-src 'self' https://*.example.com;
is usually lazy policy design. If Umami is only on analytics.example.com, say exactly that.
4) Forgetting custom script paths
Some Umami setups use a custom script path instead of /script.js, often to reduce ad blocker hits.
Example:
<script defer src="https://analytics.example.com/umami.js"></script>
CSP does not care about the path. It cares about the origin. So this is still enough:
script-src 'self' https://analytics.example.com;
connect-src 'self' https://analytics.example.com;
Debugging checklist
When Umami doesn’t work under CSP, I check these in order:
- Does the browser console show a blocked
script-srcviolation? - Does the script URL come from the origin you actually allowed?
- Does the console show a blocked
connect-srcviolation for event requests? - Are you using a nonce-based policy but forgot to nonce the script tag?
- Are you proxying analytics through another subdomain that also needs to be in
connect-src? - Did you deploy the header you think you deployed?
That last one gets people more often than they admit.
Recommended baseline
For most developer teams using Umami on a dedicated analytics subdomain, this is the policy I’d start with:
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://analytics.example.com; img-src 'self' data:; style-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none';
If Umami is same-origin, simplify it even further:
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none';
If you want more ready-to-use policy patterns, the official CSP reference is the MDN CSP documentation, and you can also use https://csp-examples.com for quick policy templates.
Keep the Umami allowlist tight, prefer exact origins, and don’t cargo-cult giant analytics CSPs from other stacks. Umami is one of the rare cases where the secure setup is also the simpler one.