How to Configure Cloudflare Transform Rules for Custom Security Headers

This guide is part of the Cloudflare security headers reference and shows the exact Response Header Modification syntax — dashboard, API, and Terraform — for injecting custom security headers at the edge. Transform Rules run in the http_response_headers_transform phase, after the origin forms its response but before the client receives it, which makes them the modern replacement for the deprecated Page Rules header model.

Configuration Syntax & Exact Values

A Response Header Transform Rule has two parts: an expression that selects which responses to modify, and one or more header operations (set, add, remove) with literal values.

# Expression — match all HTML document responses
(http.response.content_type.media_type eq "text/html")
# Header operations
set     Strict-Transport-Security    max-age=63072000; includeSubDomains; preload
set     Content-Security-Policy      default-src 'self'; object-src 'none'; frame-ancestors 'none'
set     X-Frame-Options              DENY
set     X-Content-Type-Options       nosniff
set     Referrer-Policy              strict-origin-when-cross-origin
remove  X-Powered-By

Annotated breakdown of each operation:

Dynamic header values. Transform Rules can build a value from a wirefilter expression instead of a static string — for example concatenating a request field. They cannot read the response body, so a value that must also be embedded in the HTML — most importantly a per-request CSP nonce — is impossible here and requires a Worker (see Edge Cases).

Dashboard

Navigate to Rules → Transform Rules → Modify Response Header → Create rule. Set the expression with the rule builder or the Edit expression field, then add one Set static (or Remove) row per header using the values above.

API

# 1. Find the entry-point ruleset ID for the response-header phase
curl -sS "https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/phases/http_response_headers_transform/entrypoint" \
  -H "Authorization: Bearer {API_TOKEN}" | jq '.result.id'
# 2. Append a rule to that ruleset (POST adds; PUT on the ruleset overwrites all rules)
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": {
        "Strict-Transport-Security": { "operation": "set", "value": "max-age=63072000; includeSubDomains; preload" },
        "Content-Security-Policy":   { "operation": "set", "value": "default-src '\''self'\''; object-src '\''none'\''; frame-ancestors '\''none'\''" },
        "X-Frame-Options":           { "operation": "set", "value": "DENY" },
        "X-Content-Type-Options":    { "operation": "set", "value": "nosniff" },
        "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": "Inject hardened security headers"
  }'

The '\''self'\'' escaping is required because the single quotes in CSP values collide with the shell-quoted JSON body; a dropped escape silently truncates the policy.

Terraform

resource "cloudflare_ruleset" "security_headers" {
  zone_id = var.zone_id
  name    = "Inject hardened security headers"
  kind    = "zone"
  phase   = "http_response_headers_transform"

  rules {
    action     = "rewrite"
    expression = "(http.response.content_type.media_type eq \"text/html\")"
    enabled    = true

    action_parameters {
      headers {
        name      = "Strict-Transport-Security"
        operation = "set"
        value     = "max-age=63072000; includeSubDomains; preload"
      }
      headers {
        name      = "Content-Security-Policy"
        operation = "set"
        value     = "default-src 'self'; object-src 'none'; frame-ancestors 'none'"
      }
      headers {
        name      = "X-Frame-Options"
        operation = "set"
        value     = "DENY"
      }
      headers {
        name      = "X-Powered-By"
        operation = "remove"
      }
    }
  }
}
Transform Rule match and modify pipeline at the edge An origin response enters the edge, the rule expression matches, header operations apply, and the rewritten response is returned to the client. Origin response Expression match? set / add / remove Rewritten to client yes no match: response passes through unchanged
The rule evaluates its expression against each response; on a match it applies the header operations, otherwise the response passes through unchanged.

Server-Side Configuration

Cloudflare

Transform Rules are the Cloudflare-native mechanism and replace the deprecated Page Rules header approach, which never supported arbitrary response-header injection. Deploy via any of the three methods above. Keep all security headers inside a single rule’s header map rather than spreading them across rules — most plans cap active rules per phase, and one consolidated rule is also easier to audit and roll back.

Origin equivalent (brief)

If you prefer to keep the source of truth at the origin, the equivalent on Nginx is add_header ... always (see Nginx Security Headers Configuration) and on Apache Header always set. Whichever layer owns the header, use the edge set operation so the edge value is authoritative and a stray origin copy cannot produce a duplicate.

Diagnostic & Verification Steps

# Bypass the edge cache and inspect injected headers
curl -sI -H 'Cache-Control: no-cache' https://example.com \
  | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy'

Expected output:

strict-transport-security: max-age=63072000; includeSubDomains; preload
content-security-policy: default-src 'self'; object-src 'none'; frame-ancestors 'none'
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
# Confirm exactly one copy of CSP — origin+edge duplication returns 2
curl -sI -H 'Cache-Control: no-cache' https://example.com | grep -ci '^content-security-policy:'

Expected output: 1.

# Confirm the rule ran at the edge (cf-ray present, dynamic/miss cache status)
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, disable cache, reload, and confirm each header appears once under the document request’s Response Headers.

Edge Cases, Security Implications & Safe Rollback

Safe rollback. Transform Rules are non-destructive: disabling reverts instantly to origin headers with no propagation delay.

  1. Disable: PATCH /zones/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id} with {"enabled": false}.
  2. Or delete: DELETE /zones/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id}.
  3. Verify origin fallback headers with the cache-bypass curl above.

The one irreversible value is HSTS preload — once submitted to the browser preload list, removal takes months, so confirm full HTTPS coverage across every subdomain before shipping preload.

Frequently Asked Questions

Should I use set or add for a Transform Rule header?

Use set for every security header. set replaces any existing copy so exactly one authoritative directive reaches the client. add appends a second copy, and browsers enforce the unpredictable intersection of duplicate Content-Security-Policy headers.

Can a Transform Rule generate a per-request CSP nonce?

No. Transform Rules run in the response-header phase and cannot read or rewrite the response body, so a nonce that must also appear in the HTML is impossible. Use a Cloudflare Worker for nonce-based CSP.

Why does my CSP value get truncated when I create the rule via the API?

Unescaped single quotes. CSP keywords like 'self' must be escaped as '\''self'\'' inside the shell-quoted JSON body, otherwise the shell ends the string early and the value is truncated.

Do Transform Rules replace Page Rules for headers?

Yes. Page Rules are deprecated and never supported arbitrary response-header injection. Transform Rules in the http_response_headers_transform phase are the supported mechanism for static headers.

Conclusion

Stage the rule first with a report-only CSP variant or against a non-production hostname, verify with the cache-bypass curl checks, then promote the same single consolidated rule to production using set operations. Start HSTS at a short max-age, confirm full HTTPS coverage, and only then raise it and add preload.