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:

  1. the script never loads
  2. 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-src to allow the Umami script URL
  • connect-src to allow requests back to the Umami tracking endpoint

Depending on your setup, you may also need:

  • img-src if you allow analytics-related image beacons or broad remote images
  • worker-src or style-src — uncommon for standard Umami installs
  • frame-src — generally not needed

The two common Umami setups

There are really only two setups that matter for CSP:

  1. Self-hosted Umami on the same origin
  2. 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.com
  • https://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:

  1. Does the browser console show a blocked script-src violation?
  2. Does the script URL come from the origin you actually allowed?
  3. Does the console show a blocked connect-src violation for event requests?
  4. Are you using a nonce-based policy but forgot to nonce the script tag?
  5. Are you proxying analytics through another subdomain that also needs to be in connect-src?
  6. Did you deploy the header you think you deployed?

That last one gets people more often than they admit.


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.