Water.css is the kind of CSS framework I like for security work: tiny, boring, and mostly predictable. That matters for CSP because every extra build step, inline style hack, or third-party asset is another thing you need to allow.

If you’re using Water.css, your CSP can usually stay tight. Most setups only need to allow your own origin for styles, or a single CDN if you’re loading it remotely.

What Water.css changes in CSP

Water.css is just a stylesheet. In the normal case, CSP impact is limited to:

  • style-src for the stylesheet itself
  • font-src if your page also loads custom fonts
  • img-src if your content includes remote images
  • style-src 'unsafe-inline' only if you add inline styles elsewhere

Water.css itself does not require:

  • inline scripts
  • eval
  • remote script execution
  • frames
  • object/embed
  • broad wildcard sources

That makes it easy to lock down.

Minimal CSP for self-hosted Water.css

If you downloaded water.min.css and serve it from your own site, start here:

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

This is a good baseline for a simple static site.

HTML example

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Water.css with CSP</title>
  <meta http-equiv="Content-Security-Policy"
        content="default-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'">
  <link rel="stylesheet" href="/css/water.min.css">
</head>
<body>
  <main>
    <h1>Hello</h1>
    <p>This page uses self-hosted Water.css.</p>
  </main>
</body>
</html>

For production, send CSP as an HTTP response header, not a meta tag. Meta is okay for local testing, but I wouldn’t rely on it if I care about consistency.

CSP for Water.css from a CDN

If you load Water.css from a CDN, add that host to style-src.

Example:

Content-Security-Policy: default-src 'self'; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'

HTML example

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.min.css">

That’s enough if Water.css is the only third-party asset.

If your site uses no JavaScript at all, I still like keeping script-src 'self' instead of omitting it. Explicit beats implicit.

Dark and light theme variants

Water.css often gets loaded as one of these:

<link rel="stylesheet" href="/css/water.min.css">
<link rel="stylesheet" href="/css/light.min.css">
<link rel="stylesheet" href="/css/dark.min.css">

CSP doesn’t care which file name you use. If they’re self-hosted, this stays the same:

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

If they come from a CDN, keep the CDN host in style-src.

If you use inline styles on top of Water.css

This is where people accidentally weaken CSP and then blame the CSS framework.

Water.css doesn’t force inline styles. But your app might have things like:

<div style="margin-top: 2rem;">Hello</div>

That requires 'unsafe-inline' in style-src, unless you move that CSS into a stylesheet or use a hash/nonce approach where supported.

Example policy that permits inline styles:

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

I try hard to avoid 'unsafe-inline' for styles, but in real projects it’s common. If this is temporary, make it temporary.

For directive details, the official CSP docs are still the source of truth: MDN CSP introduction. If you want short directive explanations, csp-guide.com is handy.

For a static marketing site or docs site using self-hosted Water.css and no third-party junk, I’d use something like this:

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

Single-line version:

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

This is boring, and boring is good.

Nginx example

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

Apache example

Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"

Express.js example

app.use((req, res, next) => {
  res.setHeader(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"
  );
  next();
});

This is where a clean Water.css setup turns into a normal modern website mess.

You gave a real header from headertest.com:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MGFlMWNmZjMtMzY1OC00ZjgwLTkzMGUtN2M2YTg0OGU1MTM4' '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 not “for Water.css”. It’s a policy for Water.css plus tag manager, analytics, Cookiebot, API calls, and a websocket.

That distinction matters. Don’t cargo-cult a huge CSP if all you need is one stylesheet.

When your Water.css site starts adding third parties

Use this pattern:

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

If you don’t need inline styles, remove 'unsafe-inline'. If you don’t use a CDN for Water.css, remove https://cdn.jsdelivr.net.

Common mistakes

1. Allowing https: everywhere

I see this a lot:

Content-Security-Policy: default-src 'self' https:;

This is lazy and defeats a lot of the value of CSP. Water.css does not justify broad allowlists.

2. Putting CDN hosts in default-src only

If your stylesheet is remote, make sure it’s in style-src.

Bad:

Content-Security-Policy: default-src 'self' https://cdn.jsdelivr.net

Better:

Content-Security-Policy: default-src 'self'; style-src 'self' https://cdn.jsdelivr.net

3. Adding 'unsafe-inline' because “CSS is blocked”

Usually the problem is one inline style="" attribute or a <style> block you forgot about. Fix the source if you can.

4. Forgetting fonts

Water.css itself doesn’t need special font hosts if you stick to system fonts. But if your page loads webfonts, add the source under font-src.

Example:

Content-Security-Policy: default-src 'self'; style-src 'self'; font-src 'self' https://fonts.gstatic.com; script-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Only add that if you actually load fonts from there.

Good copy-paste policies

Self-hosted Water.css, no JS

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

Self-hosted Water.css, with local JS

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

CDN-hosted Water.css

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'

Water.css with inline styles allowed

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

My default recommendation

If you can, self-host Water.css and keep the policy small:

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

That’s the sweet spot: strict enough to matter, simple enough to maintain, and not polluted by random vendors that have nothing to do with your CSS framework.