Cloudflare Managed Transforms vs Custom Rules for Security Headers
This guide is part of the Cloudflare Security Headers at the Edge reference and settles the recurring question: for edge security headers, do you flip a one-click Managed Transform, author a custom Response Header Transform Rule, or write a Worker? All three run in the Cloudflare edge, all three can add or strip response headers, and they can silently overwrite each other and your origin. This page maps their exact capabilities, the precedence order that decides which value reaches the browser, and a decision path for choosing one.
Configuration Syntax & Exact Values
Managed Transforms are pre-built toggles under Rules → Transform Rules → Managed Transforms. The response-side Add security headers transform injects this exact, non-configurable set:
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: same-origin
expect-ct: max-age=86400, enforce
Annotated breakdown — read this before enabling it:
x-content-type-options: nosniff— current and correct; blocks MIME sniffing.x-xss-protection: 1; mode=block— a deprecated header. Modern browsers ignore it, and in legacy engines its auditor introduced its own vulnerabilities. See deprecated headers.x-frame-options: SAMEORIGIN— permits same-origin framing. If you need clickjacking lockdown you wantDENYor a CSPframe-ancestorsdirective, neither of which this transform can emit.referrer-policy: same-origin— stricter than most sites intend; it sends no referrer cross-origin, which breaks analytics attribution. Compare with Referrer-Policy.expect-ct: max-age=86400, enforce— a deprecated header. Certificate Transparency is now enforced unconditionally by browsers;Expect-CTis a no-op at best.
The companion Remove “X-Powered-By” headers transform strips the X-Powered-By disclosure and has no downside. The critical takeaway: Managed Transforms cannot emit Content-Security-Policy, cannot emit HSTS, and cannot change any of the fixed values above.
A custom Response Header Transform Rule expresses the same intent with values you control:
# Expression — match all HTML document responses
(http.response.content_type.media_type eq "text/html")
# Header operations (set overwrites any existing copy)
set Content-Security-Policy default-src 'self'; object-src 'none'; frame-ancestors 'none'
set Strict-Transport-Security max-age=63072000; includeSubDomains; preload
set X-Frame-Options DENY
set X-Content-Type-Options nosniff
set Referrer-Policy strict-origin-when-cross-origin
remove X-Powered-By
Because every one of these mechanisms lives in the edge, precedence — not just capability — decides the header the browser actually receives.
Server-Side Configuration
Managed Transforms (dashboard)
Managed Transforms have no per-header syntax — they are toggles. Enable exactly the two that are unambiguously safe and leave the fixed-value Add security headers transform off if you plan to emit modern headers yourself:
Rules → Transform Rules → Managed Transforms
[x] Remove "X-Powered-By" headers # always safe
[ ] Add security headers # OFF: ships deprecated x-xss-protection + expect-ct
Managed Transforms are implemented internally as Cloudflare-owned Transform Rules. They do not count against your plan’s Transform Rule quota and do not appear in your Transform Rules list, but they run in the same response phase.
Custom Transform Rule (API)
For any real policy — CSP, HSTS, X-Frame-Options: DENY — use a Response Header Transform Rule. The set operation is mandatory: it overwrites any origin or Managed-Transform copy so exactly one authoritative value reaches the client.
# Append one consolidated rule to the response-header phase entry-point ruleset
curl -sS -X POST \
"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/{ruleset_id}/rules" \
-H "Authorization: Bearer {API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"action": "rewrite",
"action_parameters": {
"headers": {
"Content-Security-Policy": { "operation": "set", "value": "default-src '\''self'\''; object-src '\''none'\''; frame-ancestors '\''none'\''" },
"Strict-Transport-Security": { "operation": "set", "value": "max-age=63072000; includeSubDomains; preload" },
"X-Frame-Options": { "operation": "set", "value": "DENY" },
"Referrer-Policy": { "operation": "set", "value": "strict-origin-when-cross-origin" },
"X-Powered-By": { "operation": "remove" }
}
},
"expression": "(http.response.content_type.media_type eq \"text/html\")",
"description": "Hardened security headers"
}'
Worker (only when a rule cannot)
Reach for a Worker exclusively when you need to read or rewrite the response body — most importantly a per-request CSP nonce that must appear in both the header and every inline <script nonce>. A Worker sees the response last and its set wins over every edge layer below it.
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
const headers = new Headers(response.headers);
const nonce = crypto.randomUUID().replace(/-/g, "");
// set() replaces any origin/Managed-Transform/Transform-Rule copy
headers.set(
"Content-Security-Policy",
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; object-src 'none'`
);
headers.set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload");
return new Response(response.body, { status: response.status, headers });
},
};
The three mechanisms differ on capability, not just precedence. This matrix is the reason a decision is needed at all.
Diagnostic & Verification Steps
Confirm which layer actually shaped a header by inspecting the wire, bypassing the edge cache.
# See every value the browser will receive
curl -sI -H 'Cache-Control: no-cache' https://example.com \
| grep -iE 'content-security|strict-transport|x-frame|x-content-type|referrer-policy|x-xss|expect-ct'
If the Add security headers Managed Transform is on and no custom rule overrides it, you will see the deprecated pair — a clear signal to disable it:
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: same-origin
expect-ct: max-age=86400, enforce
After adding a custom Transform Rule with set, the same request shows your values instead, and X-Frame-Options flips to DENY:
content-security-policy: default-src 'self'; object-src 'none'; frame-ancestors 'none'
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
Prove there is no duplication from origin plus edge — the count must be exactly one:
curl -sI -H 'Cache-Control: no-cache' https://example.com | grep -ci '^x-frame-options:'
Expected output: 1.
Confirm the response was shaped at the edge and not served stale from cache:
curl -sI https://example.com | grep -iE 'cf-ray|cf-cache-status'
Expected output:
cf-ray: 8a1f2c3d4e5f6789-LHR
cf-cache-status: DYNAMIC
In browser DevTools, open the Network tab, tick Disable cache, reload, and read the document request’s Response Headers. A value differing from your origin config means an edge layer overwrote it — walk the precedence order above to find which.
Edge Cases, Security Implications & Safe Rollback
Three traps account for nearly every “my header is wrong on Cloudflare” ticket:
- The
Add security headerstransform ships deprecated headers. It emitsx-xss-protectionandexpect-ct, both of which the deprecated-headers guidance says to stop sending, plusx-frame-options: SAMEORIGINwhen you likely wantDENY. Treat this transform as a legacy convenience, not a hardening baseline. Enable only Remove “X-Powered-By” headers and set real policy with a Transform Rule. - Silent overwrite of an origin header. A header you carefully set on Nginx with
add_header ... alwaysor on Apache withHeader always setis replaced the moment a Managed Transform or a Transform Rulesettargets the same header. The browser sees the edge value, not yours. Decide one authoritative layer per header and verify with the cache-bypasscurl. - Two mechanisms fighting. A Managed Transform sets
x-frame-options: SAMEORIGIN, a custom rule setsDENY; the rule wins because it runs after the Managed Transform, but the intent is unclear to the next engineer. Never leave both active for the same header — turn the Managed Transform off once a rule owns that header.
The decision path below resolves which mechanism should own a header before you configure anything.
Safe rollback. Every mechanism here is non-destructive and reverts without propagation delay. Toggle a Managed Transform off in the dashboard; PATCH a Transform Rule with {"enabled": false} or DELETE it; unbind or roll back a Worker to its previous version. The origin headers return immediately. The single irreversible value in this stack is HSTS preload: once the domain is on the browser preload list, removal takes months, so confirm full HTTPS coverage across every subdomain before you ship preload from any layer.
Conclusion
Stage the change against a non-production hostname or a Content-Security-Policy-Report-Only variant first, verify with the cache-bypass curl checks that exactly the intended layer owns each header, then promote a single consolidated Transform Rule to production. Enable Remove "X-Powered-By" as a Managed Transform, leave Add security headers off, and reserve Workers for headers that genuinely need a per-request value. Start HSTS at a short max-age, confirm HTTPS everywhere, then raise it and add preload.
Frequently Asked Questions
Does the Cloudflare “Add security headers” Managed Transform set a Content-Security-Policy?
No. The transform emits a fixed set — x-content-type-options, x-xss-protection, x-frame-options: SAMEORIGIN, referrer-policy: same-origin, and expect-ct — none of which is CSP or HSTS. For those you must author a custom Transform Rule or a Worker.
Which wins when a Managed Transform and a Transform Rule set the same header?
The Transform Rule wins. Managed Transforms run first in the response-header phase, and custom Transform Rules run after them, so a rule’s set operation overwrites the Managed Transform’s value. A Worker, seeing the response last, can overwrite both.
Will an edge header overwrite the one I set at my origin?
Yes, whenever an edge layer targets the same header name with set. A Managed Transform or a Transform Rule replaces the origin’s Content-Security-Policy or X-Frame-Options outright, so the browser receives the edge value. Pick one authoritative layer per header and verify on the wire.
Should I enable the “Add security headers” Managed Transform?
Not as a hardening baseline. It ships the deprecated x-xss-protection and expect-ct headers and a permissive x-frame-options: SAMEORIGIN. Enable only Remove "X-Powered-By" headers and set real policy with a Transform Rule.
Do Managed Transforms use up my plan’s Transform Rule quota? No. Managed Transforms are implemented as Cloudflare-owned internal Transform Rules that do not count against your quota and do not appear in your Transform Rules list, though they run in the same response phase.