Overview
Security headers are cheap and pay off on every request. Five headers (CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy) block most browser-side attack classes when set correctly. Inject them through Cloudflare Transform Rules so the origin does not have to know about them, and roll out CSP in Content-Security-Policy-Report-Only mode before enforcing.
Inject headers with Response Header Transform Rules
Transform Rules sit on the response path and modify headers without touching the origin. They run before caching, so the cache stores the modified response.
# Transform Rule: add baseline security headers
When: http.host eq "example.com"
Then:
Set static: Strict-Transport-Security
value: max-age=63072000; includeSubDomains; preload
Set static: X-Content-Type-Options
value: nosniff
Set static: Referrer-Policy
value: strict-origin-when-cross-origin
Set static: Permissions-Policy
value: camera=(), microphone=(), geolocation=(), interest-cohort=()
Reach for “Set static” when the value is the same for every response; “Set dynamic” when you need a Worker-style expression. Source-control the rule set in Terraform once the policy stabilizes.
Set HSTS only after HTTPS is rock solid
Strict-Transport-Security tells browsers to refuse plaintext HTTP to the host for the duration of max-age. A misconfigured HSTS rule with includeSubDomains will lock users out of subdomains that have not migrated.
- Test with a short
max-agefirst:max-age=300; includeSubDomains. - Graduate to one year (
max-age=31536000) once every subdomain serves valid HTTPS. - Add
preloadand submit to the HSTS preload list only after a month of stable enforcement at the long max-age. - Cloudflare’s SSL/TLS dashboard can set HSTS too; pick one location for the rule and avoid two sources of truth.
For the DNS and TLS setup that makes HSTS safe, see cloudflare-dns and the SSL/TLS Full strict requirement in cloudflare.
Ship CSP report-only first, then enforce
A Content Security Policy is a list of allowed sources for scripts, styles, images, frames, and fetch destinations. The wrong policy breaks the site silently in production; the report-only mode reports violations without blocking, so you can iterate.
# Phase 1: report-only
Set static: Content-Security-Policy-Report-Only
value: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com;
img-src 'self' data: https:; style-src 'self' 'unsafe-inline';
frame-ancestors 'none'; report-uri /csp-report
# Phase 2 (after 1 to 4 weeks of clean reports): enforce
Set static: Content-Security-Policy
value: ...same policy, no -Report-Only suffix
- Pipe
report-uri(or modernreport-to) at a logging endpoint, even a Worker that writes to cloudflare-kv or a sink. - Start with
default-src 'self'; whitelist hosts by category (script-src,img-src,connect-src). - Block clickjacking with
frame-ancestors 'none'(or specific origins).frame-ancestorssupersedesX-Frame-Options. - Use both
Content-Security-PolicyandX-Frame-Options: DENYfor now; some legacy browsers still need the latter.
Mind the CSP nonce caveats at the edge
Strict CSP wants 'unsafe-inline' out of script-src and inline scripts marked with a per-response nonce or hash. Nonces must change on every response, which collides with edge caching.
- A static
Content-Security-Policyheader cached at the edge cannot carry a fresh nonce. Every cached response would replay the same nonce, defeating the protection. - Three workarounds: (1) cache the HTML and accept hash-based CSP (computed at build time); (2) generate the nonce in a Worker and bypass cache for the HTML; (3) leave
'unsafe-inline'inscript-srcand accept the lower bar. - For Quartz, Astro, and other static sites, prefer hashes over nonces. Hashes are stable, edge-cacheable, and let you keep
'unsafe-inline'out of the policy.
Set Referrer-Policy and Permissions-Policy by default
Two headers worth setting on every site, regardless of stack.
Referrer-Policy: strict-origin-when-cross-originsends only the origin (not the full URL) on cross-origin requests. The default in modern browsers, but make it explicit.Permissions-Policydisables browser features the site does not use. Camera, microphone, geolocation, and FLoC (interest-cohort) are the common ones to block:
Permissions-Policy: camera=(), microphone=(), geolocation=(),
interest-cohort=(), payment=(), usb=(), accelerometer=()
Disable interest-cohort even on sites that do not embed third parties; it signals an explicit opt-out from FLoC-style cohort tracking.
Test with a header scanner before declaring victory
A clean policy on paper can still ship with a typo. Verify from the outside.
- securityheaders.com grades the response and lists missing or weak headers. Aim for A or A+.
- Mozilla Observatory checks the same set plus TLS and gives an actionable score.
curl -I https://example.comfrom a shell prints the live response headers; useful for CI smoke tests.- For CSP specifically, watch the browser console after enforcement; a single broken third-party script can render the page unusable.
Wire the header scan into technical reviews; a missing HSTS or CSP is as cheap to fix as a missing meta tag and pays out across every page.