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:

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.

Edge response-header precedence order A response header flows left to right through origin, Managed Transforms, Transform Rules and a Worker, with each later stage able to overwrite the value set by an earlier one. Origin emits header Managed Transforms Transform Rules Worker base value fixed set only any static value final say precedence increases — each later stage overwrites the header set before it
Later stages win: a Managed Transform overrides the origin, a Transform Rule overrides the Managed Transform, and a Worker has the final word.

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.

Capability matrix: Managed Transforms vs Transform Rules vs Workers A grid comparing the three edge mechanisms across configurable values, CSP and HSTS support, per-request nonces, body access, quota use and setup effort. Managed Transforms Transform Rules Worker Custom header values no yes yes Emit CSP / HSTS no yes yes Per-request nonce no no yes Reads response body no no yes Uses rule quota no yes n/a Setup effort one click low code + CPU
Managed Transforms trade all flexibility for zero config; Workers unlock body access at the cost of code and CPU.

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 decision path below resolves which mechanism should own a header before you configure anything.

Decision path for choosing an edge header mechanism A top-down decision tree routing from per-request needs to a Worker, custom values to a Transform Rule, a quick baseline to Managed Transforms, and otherwise keeping the header at origin. Per-request value? nonce or dynamic logic Worker yes no CSP / HSTS or custom values? Transform Rule yes no Strip X-Powered-By or nosniff baseline only? Managed Transform yes no Keep header at origin
Route each header once: dynamic to a Worker, real policy to a Transform Rule, disclosure stripping to a Managed Transform, everything else stays at origin.

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.