Cloudflare Country Block via WAF Custom Rules
Block traffic from specific countries using Cloudflare WAF Custom Rules and the Cloudflare API.
Prerequisites
- Cloudflare API Token with
Zone:Rulesets:Editpermission - Zone ID for the target domain
curlandjqinstalled
Workflow
1. Get Zone IDs
curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones?account.id=YOUR_ACCOUNT_ID&status=active" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" | jq '.result[] | {name: .name, id: .id}'
2. Check Existing Custom Firewall Rulesets
curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" | \
jq '.result[] | select(.phase == "http_request_firewall_custom")'
If the output is empty: no existing ruleset. Go to 3a. If a ruleset exists, note its id and go to 3b.
3a. Create NEW Ruleset with Block Rule
curl -s -X POST \
"https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"name": "default",
"description": "WAF Custom Rules",
"kind": "zone",
"phase": "http_request_firewall_custom",
"rules": [
{
"action": "block",
"description": "Block traffic from sanctioned countries",
"enabled": true,
"expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
}
]
}' | jq '.success'
3b. Update EXISTING Ruleset
curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"description": "WAF Custom Rules",
"rules": [
{
"action": "block",
"description": "Block traffic from sanctioned countries",
"enabled": true,
"expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
}
]
}' | jq '.success'
4. Verify the Rule is Active
curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" | jq '.result.rules'
Country Codes Reference
| Code | Country |
|---|---|
RU |
Russia |
BY |
Belarus |
IR |
Iran |
KP |
North Korea |
CN |
China |
CU |
Cuba |
SY |
Syria |
VE |
Venezuela |
WAF Expression Examples
# Block multiple countries
(ip.src.country in {"RU" "BY" "IR" "KP"})
# Block country + specific ASN
(ip.src.country eq "RU") or (ip.geoip.asnum eq 12345)
# Block all except allowlisted IPs
(ip.src.country in {"RU" "BY"}) and not (ip.src in {1.2.3.4/32})
Multi-Zone Script
To apply the same block rule across multiple zones:
#!/usr/bin/env bash
API_TOKEN="YOUR_API_TOKEN"
ZONES=("ZONE_ID_1" "ZONE_ID_2" "ZONE_ID_3")
EXPRESSION='(ip.src.country in {"RU" "BY" "IR" "KP"})'
for ZONE_ID in "${ZONES[@]}"; do
EXISTING=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" | \
jq -r '.result[] | select(.phase == "http_request_firewall_custom") | .id')
if [ -z "$EXISTING" ]; then
METHOD="POST"
URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets"
DATA=$(jq -n --arg expr "$EXPRESSION" '{
name: "default", description: "WAF Custom Rules",
kind: "zone", phase: "http_request_firewall_custom",
rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
}')
else
METHOD="PUT"
URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$EXISTING"
DATA=$(jq -n --arg expr "$EXPRESSION" '{
description: "WAF Custom Rules",
rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
}')
fi
RESULT=$(curl -s -X "$METHOD" "$URL" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
--data "$DATA" | jq '.success')
echo "Zone $ZONE_ID: $RESULT"
done
Notes
PUTon an existing ruleset replaces all rules in it. Include all rules in the payload if the ruleset already has other rules.- WAF Custom Rules require a Cloudflare plan that supports rulesets (Free includes basic WAF; Pro/Business/Enterprise for full custom rules).
- Changes are applied globally within seconds; no cache purge needed.