Skip to main content

cfwr.sh

Bash scripts to create and manage Cloudflare WAF rules for WordPress sites through the Cloudflare API. Supports batch operations across multiple zones.

cfwr.sh is the entry point. It runs one of two scripts:

ScriptWhat it is
cfwrv1.shThe stable CLI: the released command set, backed by inc/.
cfwrv2.shThe single-file refactor, still in progress. It covers the core WAF and profile commands.

Choosing a version​

./cfwr.sh -d example.com -c create-rules default # asks, on a terminal
./cfwr.sh -v1 -d example.com -c create-rules default # force the stable CLI
./cfwr.sh -v2 -d example.com -c list-rules --json # force the refactor
CWR_VERSION=2 ./cfwr.sh list-profiles # same, for scripts
  • On a terminal it prints a short banner explaining that v2 is a refactor in progress, then asks Version [1]:. Enter selects v1; 2 selects v2.
  • In a pipe, a cron job, a CI run or any non-interactive shell it does not ask: it runs v1 and prints one stderr line naming -v2.
  • -v1/--v1 and -v2/--v2 may appear anywhere in the arguments; the flag is removed before the chosen script sees the rest.
  • The chooser's own banner and menu go to stderr, so ./cfwr.sh -v2 … --json | jq stays clean.

Both scripts stay runnable directly — ./cfwrv1.sh … and ./cfwrv2.sh … behave exactly as cfwr.sh -v1 … and cfwr.sh -v2 … without the chooser. The short symlinks cloudflare-wordpress-rules and cfwpr point at cfwr.sh.

v1 usage​

cfwrv1.sh -d <domain> -c <command> [options]

RULES COMMANDS (Rulesets API)
create-rules <profile> Create rules on domain using profile
update-rules <profile> Update rules on domain using profile
upgrade-default-rules Upgrade MWP default rules on domain
list-rules List rules on domain
delete-rule <id> Delete specific rule by ID
delete-rules Delete all rules on domain

PROFILE COMMANDS
list-profiles List available rule profiles
print-profile <profile> Print rules from profile
validate-profile <profile> Validate profile JSON syntax

RULESET COMMANDS
list-rulesets List rulesets on domain
get-ruleset <id> Get specific ruleset by ID
get-ruleset-fw-custom Get http_request_firewall_custom ruleset
ruleset-get-entrypoint Get the WAF custom rules entry point
ruleset-add-rule <json> Add a single rule to the entry point
ruleset-update-rule <id> <json> Update a single rule in the entry point
ruleset-delete-rule <id> Delete a single rule from the entry point
migrate-to-rulesets [--delete-old] Migrate old Firewall Rules to Rulesets API

FILTER COMMANDS (DEPRECATED since 2025-06-15)
list-filters List filters on domain
get-filter <id> Get specific filter by ID
delete-filter <id> Delete specific filter by ID
delete-filters Delete all filters on domain

SETTINGS COMMANDS
get-settings Get security settings on domain
set-settings <setting> <value> Set security setting
Settings: security_level, challenge_ttl, browser_integrity_check, always_use_https

AUTH COMMANDS
list-auth-profiles List available authentication profiles

OPTIONS
-d, --domain <domain> Domain to operate on (can be used multiple times)
-zf, --zones-file <file> Load zones from file (one per line)
-y, --yes Skip confirmation prompt for multi-zone ops
-c, --command <cmd> Command to execute
--cf-profile <name> Cloudflare auth profile from .cloudflare
--cf-auth-profile <name> Alias for --cf-profile
--config <file> Credentials file instead of ~/.cloudflare
--challenge-ttl <seconds> Challenge Passage TTL (skips the interactive prompt)
--debug Enable debug mode
--json list-rules: one compact JSON object per zone
--csv list-rules: CSV rows including expressions
--raw list-rules: unmodified entry point response
--long list-rules: detailed block per rule
--table-only DEPRECATED: alias for the default table
-dr, --dryrun Dry run, don't send to Cloudflare
-h, --help Show this help and exit

Examples​

# Create rules on a single domain
cfwrv1.sh -d domain.com -c create-rules default

# Create rules and force a specific auth profile
cfwrv1.sh -d domain.com -c create-rules default --cf-profile PROD

# List rules on a domain
cfwrv1.sh -d domain.com -c list-rules

# List rules as one JSON object per zone (pipe to jq)
cfwrv1.sh -d domain.com -c list-rules --json

# List rules as CSV, including expressions
cfwrv1.sh -d domain.com -c list-rules --csv > rules.csv

# Show each rule's logging, action parameters and full expression
cfwrv1.sh -d domain.com -c list-rules --long

# Print the unmodified Cloudflare entry point response
cfwrv1.sh -d domain.com -c list-rules --raw

# Deprecated alias for the default table (kept for existing scripts)
cfwrv1.sh -d domain.com -c list-rules --table-only

# Delete a specific rule
cfwrv1.sh -d domain.com -c delete-rule 1234567890

# Get security settings
cfwrv1.sh -d domain.com -c get-settings

# Set security level
cfwrv1.sh -d domain.com -c set-settings security_level high

# One-time migration from old Firewall Rules to the Rulesets API
cfwrv1.sh -d domain.com -c migrate-to-rulesets

# Migration + cleanup of old rules
cfwrv1.sh -d domain.com -c migrate-to-rulesets --delete-old

Challenge Passage TTL​

Challenge Passage is how long a visitor who passed a challenge is exempt from being challenged again. --challenge-ttl <seconds> sets it on the zone before the rules are created, which skips the interactive prompt that create-rules would otherwise show:

cfwrv1.sh -d domain.com -c create-rules default --challenge-ttl 3600

Three things worth knowing:

  • The value must be one of Cloudflare's allowed TTLs — 300, 900, 1800, 2700, 3600, 7200, 10800, 14400, 28800, 57600, 86400, 604800, 2592000, 31536000. Anything else is rejected with the list printed.
  • It only ever raises the setting. If the zone already has an equal or longer TTL, the command leaves it alone and says so.
  • It runs as part of create-rules only. To change the setting on its own in v1, use -c set-settings challenge_ttl <seconds>; v2 exposes it through create-rules --challenge-ttl only.

list-rules output formats​

list-rules writes data to stdout and diagnostics (errors, warnings, --debug) to stderr, so every format can be piped safely. A zone with no rules is a success: it exits 0 with an empty result.

Table (default)​

$ cfwrv1.sh -d example.com -c list-rules
example.com · 8e88ca76443f8e20af9c78042803b58d
default · http_request_firewall_custom · 4 rules · version 12 · updated 2026-10-16 20:32:35

# ACTION ENABLED RULE ID UPDATED DESCRIPTION
1 skip yes eb107ee2c59741a581214b3e359fffc5 2026-10-16 20:32:35 R1V207 - Allow URI Query, URL, User Agents...
2 block yes a8d7034442ea4b76aad7a48b5454dc38 2026-10-16 20:32:35 R2V207 - Block URI Query, URL, User Agents...
3 managed_challenge yes 85d6b0bce6fa4445bc7d1a2ad87a913c 2026-10-16 20:32:35 R3V207 - Managed Challenge /wp-admin...
4 js_challenge yes df7e5f803cf845d7858ea7737b084691 2026-10-16 20:32:35 R4V207 - Challenge Outside of GEO...

UPDATED is the per-rule last_updated Cloudflare reports, shown in UTC as YYYY-MM-DD HH:MM:SS (? when a rule carries no timestamp); the summary line above the table shows the ruleset's own last_updated in the same format. Cloudflare does not expose a creation date for a rule.

--long replaces the table with a detail block per rule: id, enabled state, logging, last_updated, action parameters and the full expression. --table-only is a deprecated alias for the default table.

JSON​

One compact object per zone — domain, zone_id, ruleset (id, name, phase, version, last_updated, rule_count) and the rules array exactly as Cloudflare returns it:

cfwrv1.sh -d example.com -c list-rules --json | jq '.rules[].action'

For several zones, each zone prints its own object; combine them with jq -s:

cfwrv1.sh -zf zones.txt -c list-rules --json | jq -s 'map({domain, count: (.rules | length)})'

CSV​

A single header row per run, then one row per rule: domain,zone_id,position,rule_id,action,enabled,logging,description,expression. Fields are quoted by @csv, so commas and quotes in expressions and descriptions stay in one field.

cfwrv1.sh -d example.com -c list-rules --csv > rules.csv

Raw​

--raw prints the Cloudflare entry point response unchanged, including its success, errors and messages envelope. Use it when debugging the API response itself.

Applying rules prints the same table​

create-rules, update-rules and upgrade-default-rules reuse the table above instead of dumping the API response:

  • Before replacing — the command prints This zone already has N rule(s), the existing rules table (count, ruleset updated line, per-rule UPDATED) and The profile contains N rule(s), all on stderr, then asks Replace all existing rules? [y/N].
  • After applying — the resulting rules table goes to stdout, preceded by a counted success line such as Replaced 4 rule(s) in ruleset <id>. A newly created entry point prints Created entry point ruleset <id> with N rule(s).
  • -y / --yes skips the prompt and the existing-rules preview; the resulting table still prints.

Every request failure still exits non-zero and reports the Cloudflare error. In v1 stdout carries the table; in v2 the table is human output and goes to stderr (see v2 differences).

v2 differences​

cfwrv2.sh implements the core workflow in one self-contained file, with all human output going through the vendored bash-ui library. The full contract is on the v2 script page.

Commands: create-rules, update-rules, upgrade-default-rules, list-rules, delete-rule, delete-rules, list-profiles, print-profile, validate-profile.

What changes against v1:

v1 (cfwrv1.sh)v2 (cfwrv2.sh)
Command form-d <domain> -c <command>Both that and cfwrv2.sh <command> … -d <domain>
list-rules table, validation report, promptsstdoutstderr, always
stdoutmixeddata only: list-rules --json/--csv/--raw, print-profile
delete-rule with -ystill promptsskips the prompt
-dr / --dryrunwarns, then continuesexits 2 with a message
Not available—filters, ruleset administration, migrate-to-rulesets, get-settings/set-settings, list-auth-profiles, token and Turnstile

The API traffic is unchanged: for every command v2 implements it sends the same Rulesets requests, with the same bodies, as v1. tests/test-cfwrv2.sh asserts that by running both scripts against the same fixture and comparing the captured request bodies.

Profiles​

Profiles are JSON files in profiles/. The profile selector is resolved by filename first (profiles/<selector>.json), then by the profile's internal name field. That means default always targets the latest numbered release, and any name shown by list-profiles is also a valid selector.

  • default — symlink to the latest numbered release.
  • mwp-rules-beta — staging profile for rule changes.
  • mwp-rules-v<NNN> — versioned release snapshots.

The full schema is documented in the profile format reference. Rule changes follow the profile management workflow.