Configuring your Content-Security-Policy on your development environment in 11ty

This is just a short post to discuss how I improved my Content-Security-Policy (CSP), on my local development environment in 11ty. This is essentially a follow-on post from my Securing your static website with HTTP response headers I wrote last year.

Note: this post isn’t exclusive to 11ty—it’s just the static site generator I use for this site. The code can be easily adapted for any static site running on a Node.js-based server—or practically any local web server!

Why is this useful?

<rant>It's incredibly useful for me because my Cloudflare build currently takes 7 minutes and 30 seconds to complete! I've no idea why it takes this long, and as of yet I've had absolutely no response from Cloudflare on why this is! What I do know is that something happened on the 19th December 2024 between 12:33AM and 12:19PM because my Cloudflare Pages build time increased by a massive 460%! See the screenshots below for proof of that fact:

BeforeScreenshot of my Cloudflare Pages build process taking 1 minute 30 seconds for a change I made with the git hash of efd9011

Note the date, time, and git hash in the screenshot above: 12:33AM December 19, 2024, hash efd9011. Now let's compare it to the screenshot below:

AfterScreenshot of my Cloudflare Pages build process taking 7 minute 22 seconds! For a change I made with the git hash of efd9011, a 460% increase!

So in this screenshot above, we have the same git hash: efd9011, but 12 hours have passed, it is now 12:19PM December 19, 2024.

Clearly my code hasn't changed because the git hash is the identical and nothing else has been committed, but for some reason my build time has increased by 460%! It's my guess that something changed on Cloudflare's side in this 12-hour period to drastically increase the build time. Even though I posted about it on the Cloudflare Discord server and the Cloudflare Community Forums, I've had no response to tell me what was changed and if it is possible to fix it. So if anyone from Cloudflare is reading, I'm begging you, please, can you investigate what changed in that 12-hours and tell me if there's something I can change to drop my build time back to 1 minute 30 seconds!</rant>

So you're probably asking: "What on earth has any of this got to do with a "Content-Security-Policy" response header? Well, a 1-minute 30-second rebuild time to update my site's CSP in the 11ty _headers file is a reasonable time to wait to test for issues. But waiting 7 minutes 20 seconds for every little change just isn't efficient or workable! Quite frankly it's incredibly frustrating. And if you've ever worked on writing a CSP before you will realise how convoluted and unreasonably lengthy they can become! There had to be a better way to do this, and there is! So let me take you through the node.js / 11ty solution I used below to make my latest CSP changes.

The code

Let's have a look at the code:

// This is my site's Content Security Policy.
// Modify this CSP, don't just copy / paste it! It will break your site!
// You can also use `var` and `let` depending on your coding syntax, they all work
const CSP = `
base-uri 'self';
child-src 'self';
connect-src 'none';
default-src 'none';
img-src 'self' https://v1.indieweb-avatar.11ty.dev/;
font-src 'self';
form-action 'self' https://webmention.io https://submit-form.com/DmOc8anHq;
frame-ancestors 'self';
frame-src 'self' https://player.vimeo.com/ https://www.slideshare.net/ https://www.youtube.com/ https://giscus.app/ https://www.google.com/;manifest-src 'self';
media-src 'self';
object-src 'none';
script-src 'self' https://ajax.cloudflare.com https://giscus.app/ https://www.google.com/ https://www.gstatic.com/;
style-src 'self' https://giscus.app/;
worker-src 'self';`.replaceAll('\n', ' ');
// This is the middleware for our 11ty development server
eleventyConfig.setServerOptions({
	middleware: [(req, res, next) => {
		if (req.url.endsWith('.html') || req.url === '/') {
			res.setHeader('Content-Type', 'text/html; charset=UTF-8');
			res.setHeader('Content-Security-Policy', CSP);
		}
		next();
	}]
});

As you will see I have formatted the CSP using template literals to make the CSP more readable, and thus easier to modify and test on my local environment. You can find the public gist for the code above here.

A couple of notes on this code:

  1. Notice how I removed newlines from the final CSP variable. Using const signals to other developers that it shouldn't change, but since strings are immutable in JavaScript, modifying it using .replace() creates a new valid CSP header string.
  2. I've added detection for both "naked URLs" and those ending in /index.html. This is just for convenience really, as it saves having to add the index.html to URLs before the response header is sent.
  3. Lastly, the above CSP in its current form will break 11ty's live reload functionality because the connect-src in the CSP is set to none which tells the browser to block all network requests initiated by the page.

The following network communication technologies are blocked by this directive:

In order to fix point 3 there are 2 options:

1) Modify the connect-src directive

This is the obvious fix for the issue:

// add the following to your CSP variable
connect-src 'self' ws: wss: http://localhost:* ws://localhost:*;

By modifying the connect-src directive, we are allowing connections on localhost via HTTP and WebSockets (which 11ty's live reload script relies on to push updates to the browser without having to reload the whole page). And we are also allowing WebSocket connections that are both encrypted (wss:) and unencrypted (ws:).

2) Modify the default-src directive

This is a less obvious fix for the issue:

// add the following to your CSP variable
default-src 'self' ws: wss: http://localhost:* ws://localhost:*;

The reason this works is that default-src is the "fallback" directive. So assuming your connect-src is set to self then the following ws: wss: http://localhost:* ws://localhost:*; would still be followed because the directive is falling back to default-src.

IMPORTANT: don't use default-src if your connect-src is set to none! The value of none will still block all connections and override the default-src settings.

The advantage of modifying the default-src directive is that it will be applied to other directives like script-src and img-src. So it really depends on your CSP requirements for your local development environment! For most readers, I'd guess modifying the connect-src directive will be enough. But I've added both options for completeness.

Summary

Believe it or not, I kept the blog post short this time! I hope this code snippet makes configuring your 11ty CSP a breeze! As always, thanks for reading and if you have any feedback or comments, please let me know!


Post changelog:

  • 04/02/25: Initial post published.
  • 09/02/25: Thanks to vrugtehagel from discord for his couple of tweaks to the const CSP code. The small changes can be seen in the gist revisions if you are interested.

Webmentions

No mentions yet.

A webmention is a way to notify this site when you've linked to it from your own site.