System: Cloudflare WAF Rate Limiting — raxx.app zone Owner: operator Last incident: n/a (initial setup) Last reviewed: 2026-04-30
Cloudflare WAF rate-limit rules are zone-level rulesets that count requests per IP and apply
an action (block, managed challenge, JS challenge) when a threshold is exceeded within a
counting window. They live under the http_ratelimit phase on the zone.
Zone ID: f12dbb5cac57d5591a5058874498a6d1 (raxx.app)
WAF rate-limit rules require Zone > WAF > Edit scope on the Cloudflare API token.
The tokens currently in vault at /MooseQuest/cloudflare/ and their documented scopes:
| Vault key | Documented scope | WAF edit? |
|---|---|---|
CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN |
CF Pages read + Zero Trust Access apps/policies | No — 403 on http_ratelimit phase confirmed 2026-04-30 |
CLOUDFLARE_EDIT_DNS |
DNS edit on raxx.app zone | Unknown — not tested |
CLOUDFLARE_ACCESS_MGMT_TOKEN |
CF Access management | No |
CF_PAGES_DEPLOY_TOKEN |
CF Pages deploy | No |
CLOUDFLARE_PAGES_READ_TOKEN |
CF Pages read-only | No |
To mint a WAF-scoped token (operator action — requires CF dashboard access):
1. Go to https://dash.cloudflare.com/profile/api-tokens
2. Create Token → "Edit zone" template
3. Permissions: Zone > WAF > Edit
4. Zone resources: raxx.app (specific zone)
5. Store in Infisical at /MooseQuest/cloudflare/ as CLOUDFLARE_WAF_EDIT_TOKEN
6. Confirm it has no broader than Zone > WAF > Edit + Zone > Zone > Read (least privilege)
| Field | Value |
|---|---|
| Rule ID | TBD — pending token provisioning (see #719) |
| Phase | http_ratelimit |
| Expression | (http.host eq "tickets.raxx.app" and http.request.uri.path eq "/login" and http.request.method eq "POST") |
| Threshold | 10 requests per IP per 60 seconds |
| Action | Block |
| Mitigation timeout | 300 seconds (5 minutes) |
| Applies to | POST only — GET /login is not rate-limited |
GET /login returns 429 (rule expression bug — should be POST only)Check current rule state:
bash
ZONE_ID="f12dbb5cac57d5591a5058874498a6d1"
curl -s \
-H "Authorization: Bearer ${CLOUDFLARE_WAF_EDIT_TOKEN}" \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_ratelimit/entrypoint" \
| python3 -m json.tool
Verify the rule expression — confirm it contains http.request.method eq "POST" and does
NOT match GET.
Check Cloudflare Analytics > Security > Rate Limiting to see event volume over the past hour.
Check if the http_ratelimit phase ruleset ID has changed (CF does not change ruleset IDs,
but if the zone was re-created this could happen).
Symptom:
- GET /zones/<zone>/rulesets/phases/http_ratelimit/entrypoint returns HTTP 403
{"message":"request is not authorized"}
- Any PUT/POST to the rate-limit phase returns the same 403
Cause:
The API token being used does not have Zone > WAF > Edit scope. This was the blocker
on 2026-04-30 when CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN was found to cover only
CF Pages read + Zero Trust Access.
Fix:
1. Mint a new CLOUDFLARE_WAF_EDIT_TOKEN in the Cloudflare dashboard (instructions above).
2. Store it in Infisical.
3. Export it: export CLOUDFLARE_WAF_EDIT_TOKEN=<value>
4. Re-run the rule deployment commands in the "Deploy the rule" section below.
Verification: GET /zones/<zone>/rulesets/phases/http_ratelimit/entrypoint returns 200.
Symptom: Admin reports 429 during normal login attempts.
Cause: Threshold too aggressive. 10 req/min is correct for a human but a password manager retry loop or browser autofill could hit it with rapid retries.
Fix (raise threshold to 20):
ZONE_ID="f12dbb5cac57d5591a5058874498a6d1"
RULESET_ID="<get from GET phase entrypoint>"
RULE_ID="<get from GET phase entrypoint>"
curl -s -X PATCH \
-H "Authorization: Bearer ${CLOUDFLARE_WAF_EDIT_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/${RULESET_ID}/rules/${RULE_ID}" \
-d '{
"ratelimit": {
"characteristics": ["ip.src"],
"period": 60,
"requests_per_period": 20,
"mitigation_timeout": 300
}
}'
Hard rule: Do NOT lower below 5 req/min. Do NOT extend block beyond 5 minutes (300s).
Symptom: No rule in http_ratelimit phase; brute-force attempts not being blocked.
Fix: Re-deploy the rule using the "Deploy the rule" procedure below.
This procedure requires CLOUDFLARE_WAF_EDIT_TOKEN with Zone > WAF > Edit scope.
ZONE_ID="f12dbb5cac57d5591a5058874498a6d1"
# Step 1: Check if the http_ratelimit entrypoint ruleset already exists
GET_RESP=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer ${CLOUDFLARE_WAF_EDIT_TOKEN}" \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_ratelimit/entrypoint")
HTTP_CODE=$(echo "$GET_RESP" | tail -1)
BODY=$(echo "$GET_RESP" | head -n -1)
echo "Existing ruleset status: $HTTP_CODE"
# Step 2: Deploy the rule via PUT to the phase entrypoint
# NOTE: PUT replaces ALL rules in the ruleset. If other rules exist, include them too.
# Inspect existing rules from Step 1 before overwriting.
curl -s -X PUT \
-H "Authorization: Bearer ${CLOUDFLARE_WAF_EDIT_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_ratelimit/entrypoint" \
-d '{
"rules": [
{
"description": "Brute-force defense: tickets.raxx.app/login POST",
"expression": "(http.host eq \"tickets.raxx.app\" and http.request.uri.path eq \"/login\" and http.request.method eq \"POST\")",
"action": "block",
"ratelimit": {
"characteristics": ["ip.src"],
"period": 60,
"requests_per_period": 10,
"mitigation_timeout": 300
}
}
]
}' | python3 -m json.tool
IMPORTANT: PUT replaces the entire ruleset. If there are other rate-limit rules on the zone,
fetch the current state first, append the new rule to the existing rules array, then PUT.
After deployment, run a rapid-fire POST test. The 11th+ request should return HTTP 429.
# Run 12 rapid POST requests to /login with dummy credentials
for i in $(seq 1 12); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=test@example.com&password=wrongpassword" \
"https://tickets.raxx.app/login")
echo "Request $i: HTTP $CODE"
done
Expected: requests 1-10 return 200 or 302 (login form response); requests 11-12 return 429.
Also verify GET is NOT blocked:
curl -s -o /dev/null -w "%{http_code}" "https://tickets.raxx.app/login"
# Expected: 200 (login page loads normally)
ZONE_ID="f12dbb5cac57d5591a5058874498a6d1"
# To disable (set enabled: false on the specific rule):
curl -s -X PATCH \
-H "Authorization: Bearer ${CLOUDFLARE_WAF_EDIT_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/${RULESET_ID}/rules/${RULE_ID}" \
-d '{"enabled": false}'
# To delete the rule entirely (PATCH with DELETE verb is not supported — use PUT to replace ruleset without the rule):
# GET current rules, remove the target rule, PUT the modified array.
Escalate to operator when: - No API token exists with WAF edit scope (must mint a new one in CF dashboard) - Rate-limiting events show a novel attack pattern not covered by the POST /login rule - Legitimate users are being blocked in significant numbers (data from CF Analytics) - Zone ID changes (should not happen, but if raxx.app zone is re-created)
CF dashboard: https://dash.cloudflare.com CF WAF Analytics: https://dash.cloudflare.com → raxx.app → Security → WAF