Overview

Browser-side attack classes (clickjacking, mixed content, script injection, MIME sniffing) are blocked mostly by response headers. Cloudflare Response Header Transform Rules add them without changing the origin, which suits hosts such as github-pages where you cannot set headers. Roll CSP out in report-only mode before enforcing.

Add baseline headers with a Response Header Transform Rule

Match the hostname, then use “Set static” for fixed values:

When: http.host eq "example.com"
Set static: Strict-Transport-Security = max-age=31536000; includeSubDomains
Set static: X-Content-Type-Options = nosniff
Set static: Referrer-Policy = strict-origin-when-cross-origin
Set static: Permissions-Policy = camera=(), microphone=(), geolocation=(), payment=(), usb=()

Transform Rules run after Cloudflare decides caching, so they do not alter cache behavior, and modifying Cache-Control through them does not change how Cloudflare caches the object (docs). For logic beyond static values, use a Worker or Snippet. Avoid the “Add security headers” Managed Transform as your policy: it sets legacy headers (X-XSS-Protection, Expect-CT, Referrer-Policy: same-origin) and its own docs warn it can block resources.

Raise HSTS in stages

HSTS makes browsers refuse plaintext HTTP for max-age seconds. With includeSubDomains, one subdomain without valid HTTPS becomes unreachable.

  1. Start with max-age=300; includeSubDomains.
  2. Move to max-age=31536000 once every subdomain serves valid HTTPS.
  3. Add preload and submit to the preload list only after a month of stable enforcement; preload needs a max age of at least 12 months, and removal from browser lists is slow.

Cloudflare’s SSL/TLS dashboard can also set HSTS (1 to 12 months). Set it in one place only. Never remove HTTPS before the max age expires, or visitors are locked out. Prerequisites: cloudflare-dns and Full (strict) in cloudflare.

Ship CSP report-only, then enforce

# Phase 1
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com;
  img-src 'self' data:; style-src 'self'; frame-ancestors 'none'; report-to csp
# Phase 2 (after 1 to 4 weeks of clean reports): same value under Content-Security-Policy
  • Start from default-src 'self' and allow hosts per category (script-src, img-src, connect-src).
  • Send reports to a logging endpoint (a Worker that writes to a log sink works). report-to needs a Reporting-Endpoints header; report-uri still works in older browsers.
  • frame-ancestors blocks clickjacking and supersedes X-Frame-Options in current browsers; adding X-Frame-Options: DENY is redundant but harmless.
  • Watch the browser console after enforcing; one blocked third-party script can break the page.

Prefer hashes over nonces behind a cache

A nonce must differ on every response, but a cached HTML page replays the same nonce and a static header cannot generate one. For static sites (Quartz, Astro), compute script hashes at build time and keep 'unsafe-inline' out of script-src. If you need nonces, generate them in a Worker that rewrites the HTML, and bypass the cache for those pages.

Verify from the outside

  • curl -sI https://example.com shows the live headers; use it in CI smoke tests.
  • securityheaders.com and Mozilla Observatory grade the response.

Include a header check in technical reviews; a missing HSTS or CSP is as cheap to fix as a missing meta tag.