pwnmyvibecode_

Security headers on Cloudflare

For any site proxied through Cloudflare, create a Response Header Transform Rule that sets each security header on all incoming requests. For Cloudflare Pages, a _headers file in your build output does the same job, and HSTS has its own switch under SSL/TLS.

Updated

Check your live site now

Free, no signup, read only. A grade and plain fixes in seconds.

Three places to set headers on Cloudflare

Cloudflare sits between visitors and your site, so it can add headers no matter what serves the page behind it. Which tool you use depends on how the site is hosted:

  • A Response Header Transform Rule: works for any hostname proxied through Cloudflare (the orange cloud in DNS), whether the origin is a VPS, a Render or Railway app, or another host. No code changes.
  • A _headers file: for Cloudflare Pages projects and Workers static assets. Same format as Netlify's file, committed with your code.
  • Worker code: when a header value has to be computed per request. Most sites don't need this for security headers.

Setting security headers with a Transform Rule

  1. In the Cloudflare dashboard, open your domain and go to Rules. Create a Response Header Transform Rule (older dashboards list it under Transform Rules as Modify Response Header).
  2. Give it a name such as Security headers and choose to apply it to all incoming requests.
  3. Add one Set static operation per header, using the names and values below.
  4. Deploy the rule. Nothing changes on your origin and there's nothing to redeploy there.
Rules > Transform Rules > Modify Response Header
Set static  Strict-Transport-Security  =  max-age=31536000; includeSubDomains
Set static  Content-Security-Policy  =  default-src 'self' https: data: blob: 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests
Set static  X-Frame-Options  =  SAMEORIGIN
Set static  X-Content-Type-Options  =  nosniff
Set static  Referrer-Policy  =  strict-origin-when-cross-origin
Set static  Permissions-Policy  =  camera=(), microphone=(), geolocation=()

Use Set rather than Add. Set replaces any value your origin already sends for that header, so you never end up with two conflicting copies. Add keeps the origin's value and appends another.

Transform Rules only run on proxied traffic. A DNS record set to DNS only (grey cloud) goes straight to your origin and gets none of these headers.

HSTS and HTTPS settings in the Cloudflare dashboard

Cloudflare has dedicated switches for the two transport settings, under SSL/TLS, Edge Certificates:

  • Always Use HTTPS redirects every http request to https at the edge. Turn this on first.
  • HTTP Strict Transport Security (HSTS) sends the Strict-Transport-Security header for you, with a max-age and optional includeSubDomains and preload. If you use this setting, remove the HSTS line from your Transform Rule so it's configured in one place.

Only tick includeSubDomains once every subdomain works over https, and treat preload as a long commitment: once your domain is in browsers' preload lists, removing it takes a long time to reach users.

Headers on Cloudflare Pages with a _headers file

Pages reads a _headers file from your build output directory. The format is identical to Netlify's, so this block works as it is. In a Vite project, putting it in public/ copies it into the output on every build:

public/_headers
/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  Content-Security-Policy: default-src 'self' https: data: blob: 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests
  X-Frame-Options: SAMEORIGIN
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

Headers from _headers apply to static files. Responses from Pages Functions aren't covered by the file, so set headers in the function's own Response for API routes, or use a Transform Rule, which covers everything on the hostname.

A stricter CSP behind Cloudflare

The CSP above is a starter policy that still allows inline scripts, so header checkers will report it as weak. Tighten it the same way on any host: switch the header name to Content-Security-Policy-Report-Only with script-src 'self', browse the site with the console open, allow the hosts that are really yours, then enforce it.

If the site needs inline scripts, the fix belongs in the app: move them into files, list their sha256 hashes in script-src, or generate nonces in your server code. Don't use a Worker to add a nonce to every script tag in the HTML. It would also add the nonce to any script an attacker managed to inject into the page, which defeats the point of the policy.

How to confirm Cloudflare is sending the headers

terminal
curl -sI https://your-site.com | grep -i -E 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|server'

A server: cloudflare line confirms the request went through the proxy. If the headers are missing but that line is there, check that the rule is deployed and its filter matches the hostname you tested.

Questions

Does Cloudflare add security headers by default?

No. Cloudflare offers managed transforms that can add a few basic security headers, plus the HSTS switch, but a CSP and the rest of the set come from your own rules or your origin.

Should headers be set at Cloudflare or in my app?

Either is fine for fixed headers like HSTS, nosniff and X-Frame-Options, and the edge is handy when you can't easily change the origin. A strict CSP with nonces has to come from the app, because only the app knows which scripts it rendered.

Why don't my Transform Rule headers show up?

The usual causes are a DNS record set to DNS only instead of proxied, a rule filter that doesn't match the hostname, or a rule saved but not deployed.