Pete's Log: Creating my own Content Security Policy (and other HTTP header fun)

Entry #2076, (Meta)
(posted when I was 43 years old.)

I've enabled some new fancy HTTP headers on This has allowed me to score an A+ at, which gives me a nice false sense of security. This has led to a number of observations that I shall attempt to present in some coherent manner. Please note that it should be considered a "bad practice" to take security advice from me.

Even though I achieved an A+ grade, I did break a few things to get there. In particular, I opted for a pretty strict CSP:

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

Basically, with a few exceptions, browsers are instructed not to load anything for my website from anywhere but itself. This means that inline JavaScript and CSS are also forbidden (that would require style-src or script-src 'unsafe-inline'). It was easy enough to find the handful of places with inline JavaScript and move that JavaScript into .js files. However, inline CSS is all over the place. I thought for a while of settling for style-src 'self' 'unsafe-inline' but decided if I do that, I'll never clean it up. So I've cleaned up the most obvious places and have left plenty of broken pages around that I will hopefully be motivated to fix as I see them.

The trickiest thing to fix so far was the graphs on the stats page, since they use inline styles to set the bar heights. So for the time being I've switched it to use some JS instead and will try to think of a workaround for non-JS users.

Why worry about inline JS and CSS? Well, if users are allowed to submit any content, there is the potential for them to add JS or CSS to the page that will do bad things.

To the best of my knowledge, there are currently two places that I allow user content submission, and both of them currently escape HTML tags. But I do have ambitions of adding more features. Having a strict Content Security Policy (and other strict security headers) in place now means that if I do add some new feature, I am more likely to notice immediately if I do something dumb instead of getting something fully implemented and then trying to make it more secure after the fact. And also lets me feel a little more comfortable in case I did something wrong with my HTML escaping.

And while I also think it's perfectly OK to ignore specific warnings if you're certain they don't apply to you, there is a certain cognitive overhead in doing so. Every time I went back to rescan my site, I would have to evaluate if the warnings I'm seeing are still the same one(s) I decided was OK or if there's something new. Getting to zero warnings means in the future I instantly know any warnings I see need attention.

Other headers I added:

Referrer-Policy: strict-origin-when-cross-origin

This one still makes me a bit sad, but I get it.

X-Content-Type-Options: nosniff

Don't sniff my web server please

Permissions-Policy: accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()

I don't think Firefox supports this one yet (or it supports an older version called Feature-policy) but it seems to make sense. My website doesn't need your location or camera or payment or any of these other things. Maybe fullscreen though.

Strict-Transport-Security: max-age=31536000; includeSubDomains

This header tells browsers to only do HTTPS for and I hesitated turning it on. Not because it was difficult to configure, but because of past issues with it. Sometime last fall the LetsEncrypt certificate on my firewall expired (well, the certificate had been auto-renewed but the admin web server hadn't restarted so was still serving up the old certificate). Firefox wouldn't let me connect because the firewall had set the HSTS header. Luckily lynx didn't care about HSTS so it let me connect and fix the problem. And then I enabled the feature to restart the web server after renewing the certificate. So anyway, I got over myself and turned this one on too.