Hardening a Static Blog: Headers, CSP, and One Warning I Left Alone
Table of Contents
I ran securityheaders.com against mrdee.in recently. The grade was not good. Missing HSTS, no CSP, no framing protection — the usual neglect you accumulate when you’re focused on content and treating the hosting layer as someone else’s problem.
It took about 20 minutes to fix. Here’s what I did and why.
The Fix: a _headers file #
Cloudflare Pages reads a _headers file from the root of your published site and applies the directives to every response. No middleware, no Workers, no config panel. Drop a file, push, done.
I added static/_headers to the Hugo repo so it gets copied into the build output automatically:
/*
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self' static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' cloudflareinsights.com; font-src 'self'; frame-ancestors 'none'
What each one does:
- HSTS — tells browsers to only ever connect over HTTPS, for the next two years, including subdomains. The
preloadflag submits the domain to browsers’ built-in HSTS lists. - X-Content-Type-Options: nosniff — stops browsers from guessing content types. Prevents a text file being executed as a script.
- X-Frame-Options: DENY — blocks the site from being embedded in an iframe. Clickjacking mitigation.
- Referrer-Policy — sends the full URL as referrer within the site, but only the origin when crossing to other domains.
- Permissions-Policy — explicitly revokes camera, microphone, and geolocation access. Belt-and-suspenders on a blog that needs none of those.
- Content-Security-Policy — the interesting one. More on this below.
The CSP unsafe-inline Problem #
My first draft of the CSP included 'unsafe-inline' in script-src. The scan came back with an amber warning: this policy contains ‘unsafe-inline’ which is dangerous in the script-src directive.
The standard advice is to replace 'unsafe-inline' with per-script hashes or a nonce. But before doing that, I wanted to understand whether the site actually needed it.
I read through the Congo theme’s layout templates — specifically head.html, schema.html, analytics.html, and vendor.html. What I found:
- All JavaScript is loaded as external files with SRI integrity hashes (
$jsAppearance,$bundleJS). No inline<script>blocks with executable code. - The only inline
<script>tags aretype="application/ld+json"— JSON-LD structured data for schema.org. These are not executed as JavaScript by browsers. - Cloudflare Web Analytics, when injected by Cloudflare Pages, uses an external
<script src="...">tag, not inline code.
'unsafe-inline' wasn’t needed. I removed it. style-src still carries it because Congo applies dynamic appearance classes at runtime (dark/light mode switching) that require it — that’s a separate and lower-severity issue.
The Amber Finding I Left Alone #
After fixing the CSP, one amber finding remained: Access-Control-Allow-Origin: *.
This sounds alarming. Wildcard CORS means any website can fetch your content cross-origin. On an API server with authenticated sessions and sensitive responses, that’s a real problem.
On a public static blog, it’s the correct setting. There’s no private data, no cookies tied to anything sensitive, no user-specific responses. Locking it down to https://mrdee.in would break RSS readers and any site legitimately fetching the content — with zero security benefit. Cloudflare Pages sets this by default on static assets. I didn’t add it, and I didn’t remove it.
Security findings are not equally weighted. This one is amber on a scanner because it can be dangerous in the wrong context. In this context it isn’t.
The final scan came back clean on everything that matters. The whole thing — reading the theme source, writing the headers, pushing, re-scanning — took less time than writing this post.
