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:
set— replaces every existing copy of the header (origin or prior rule) with this single value. This is the only correct operation for security headers: it guarantees exactly one authoritative directive on the wire.add— appends an additional copy, leaving any origin value in place. Browsers enforce the intersection of duplicateContent-Security-Policylines, soaddproduces unpredictable policy. Reserve it for genuinely multi-valued headers.remove— deletes the header. Use it to strip origin information leaks such asX-Powered-ByorServer.
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"
}
}
}
}
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
- Rule ordering. Rules in the response-header phase evaluate top-to-bottom; a later rule with
seton the same header overrides an earlier one. Consolidate into one rule to remove ambiguity, or verify the dashboard order matches intent. - Conflict with origin headers. If the origin emits
Content-Security-Policyand your rule usesadd, the client receives two copies and enforces their intersection. Always usesetto overwrite the origin value; verify with thegrep -cicommand above returning1. - CSP nonces need a Worker. Transform Rules cannot read or rewrite the response body, so a per-request nonce that must appear in both the header and the inline
<script nonce>attributes is impossible here. Use a Worker for security headers for that case. - Cached responses. A header baked into a cached object is served unchanged until expiry. Purge the cache (
POST /zones/{zone_id}/purge_cache) or use Development Mode, then re-test withCache-Control: no-cache.
Safe rollback. Transform Rules are non-destructive: disabling reverts instantly to origin headers with no propagation delay.
- Disable:
PATCH /zones/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id}with{"enabled": false}. - Or delete:
DELETE /zones/{zone_id}/rulesets/{ruleset_id}/rules/{rule_id}. - Verify origin fallback headers with the cache-bypass
curlabove.
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.