Blog
Security Headers and CSP for This Blog: What I Actually Turned On
A blog is a static site with no user logins, and the response headers are still worth doing properly. Not because "static sites get turned into botnets every day," but because headers are cheap, they shrink the blast radius of a class of XSS and wrong-MIME attacks, and they make scan reports less alarming. A static site's equities aren't high, but getting nested into a phishing page, having something execute with the wrong type, or leaking article paths to a pile of external links is avoidable embarrassment. So instead of "checking off a security list," I'll walk through the headers I actually have enabled, one by one: what each one defends against, how to pick the values for a static site, and who's the authority when Cloudflare is in front.
Strict-Transport-Security
Strict-Transport-Security: max-age=31536000; includeSubDomains
What it does: tells the browser to reach this host only over HTTPS (and optionally its subdomains) for max-age seconds. Confirm the whole site's certs and redirects are solid before committing to a long max-age; turn it on too early and you weld your HTTP debugging window shut. The preload list (preload) is more aggressive, and a home blog usually doesn't need to rush into it on day one. HSTS enforces the transport channel; it doesn't replace CSP and it doesn't handle content injection itself.
If the CDN already adds HSTS at the edge, a second copy at the origin is usually fine — but watch for duplication, make sure the policies agree, and don't write includeSubDomains while some subdomains aren't fully HTTPS yet.
X-Content-Type-Options
X-Content-Type-Options: nosniff
What it does: blocks MIME sniffing, defusing the classic "server sends it as text, browser runs it as script" trap. A static blog has almost no reason to turn this off. If some ancient asset depends on sniffing to work, fix the asset's Content-Type instead of deleting the header. It pairs with CSP: one governs type trust, the other governs load sources.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
What it does: controls how much Referer leaves when you navigate to an external site. strict-origin-when-cross-origin is the usual compromise: same-site keeps the full path, cross-site HTTPS→HTTPS sends only the origin, and downgrade cases leak even less. If your blog embeds analytics or has a lot of outbound links, an over-generous Referer hands your article paths to third parties. Need to be stricter? no-referrer exists — but some analytics and link previews go deaf.
Permissions-Policy
Permissions-Policy: camera=(), microphone=(), geolocation=()
What it does: explicitly disables the browser's powerful capability APIs. A static site shouldn't be asking for camera and microphone anyway; the point is to stop embedded third-party scripts or a widget added by mistake in the future from quietly invoking them. Add and remove on demand: if one page really needs map geolocation, open a hole for that origin rather than allowing it globally. The old name Feature-Policy is obsolete; new sites write Permissions-Policy.
Clickjacking: X-Frame-Options and frame-ancestors
X-Frame-Options: DENY
# or in CSP:
# frame-ancestors 'none'
What it does: stops your page from being embedded in someone else's iframe for UI disguising. Modern guidance prefers CSP's frame-ancestors; X-Frame-Options still has broad support and the two can coexist, but make sure their semantics don't fight each other. If the blog needs to be embeddable by a trusted domain (a rare docs-site case), whitelist it instead of deleting the protection outright.
Content-Security-Policy: report first, then enforce
CSP is the one in this set most likely to cause collateral damage, and the most worth doing. It declares which origins the browser is allowed to load scripts, styles, images, fonts, and connections from. A static blog can be fairly strict, but you still need whitelists for the CDN, fonts, cover images, and possibly an analytics domain. If a Hexo-style generator stuffs syntax-highlighting or comment scripts inline into the HTML, the console lights up red the first time you enforce CSP — that's expected, not the site being broken.
The pragmatic path:
- Ship
Content-Security-Policy-Report-Onlyfirst, watch it for a week - Check the browser console and the report endpoint (if you have one)
- Tighten it up, then switch to the enforcing
Content-Security-Policy
Example starting point (adjust the domain to your site):
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data: https:; \
style-src 'self' 'unsafe-inline'; script-src 'self'; \
base-uri 'self'; frame-ancestors 'none'
Keep script-src as free of 'unsafe-inline' as possible; it's cleaner if your generator supports nonces or externalizes scripts. Theme and analytics scripts are supply chain: CSP caps the blast radius, it's not a silver bullet. Keep dependencies small, and add SRI to third-party scripts where you can. If img-src is written too tight, external images and emoji CDNs break; noting the real referrer domains in the whitelist during report-only beats jumping straight to default-src 'none' and hurting readers.
How to verify when a CDN is stacked on top
Headers can go in OpenResty, Nginx, or Cloudflare Transform Rules. Make exactly one place the "authority," and don't let the other silently overwrite it with a conflicting value. Verification:
curl -sI https://blog.example.com | grep -iE \
'strict-transport|content-security|x-content-type|referrer-policy|permissions-policy|x-frame'
securityheaders.com is a fine reference, but the score isn't a KPI — don't tear out necessary features to chase an A+. After changing your theme or adding analytics, do a regression pass: that's when CSP blows up most often. Locally, open the homepage and one post with code highlighting and external images in an incognito window and check whether the console shows policy blocks; that catches the "headers are there, page is broken" inconsistency that curling response headers alone can't.
If the blog runs through both Cloudflare and the origin, after changing headers test a cache-bypassed URL and a cache-hit URL separately, and confirm Transform Rules and origin headers aren't one-old-one-new. Security headers are hygiene; for a low-equity asset like a blog, do it, forget it until the next frontend change, then re-verify item by item — item by item, not just a single aggregate score.