Skip to content

Commit f0053ba

Browse files
tbjersclaude
andcommitted
Add WAF skip rule setup script for machine-to-machine endpoints
scripts/setup-waf-rules.sh uses the Cloudflare Rulesets API to add a skip rule that bypasses Bot Fight Mode and Browser Integrity Check for /ingest (OIDC-protected) and /webhooks/github (HMAC-protected). Without this, enabling zone-level bot protection blocks GitHub Actions runners. Wrangler cannot manage zone-level WAF rules; this script fills the gap. Documented in INSTALLATION.md step 11. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4966776 commit f0053ba

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

docs/INSTALLATION.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,19 @@ Deployed coverage-tracker triggers
282282

283283
The DNS record is created automatically. If you see `No targets deployed`, check that the `routes` entry in `wrangler.jsonc` uses `"custom_domain": true` and not a wildcard path.
284284

285+
### Add WAF skip rules
286+
287+
If you enable **Bot Fight Mode** or **Browser Integrity Check** on your Cloudflare zone, those security features will block the GitHub Actions runner (a non-browser client) from reaching `/ingest` and `/webhooks/github`. Both endpoints have their own auth (OIDC and HMAC respectively), so they don't need zone-level bot protection.
288+
289+
Run the provided setup script to add a WAF skip rule for those paths:
290+
291+
```bash
292+
CLOUDFLARE_API_TOKEN=<your-token> ZONE_DOMAIN=yourdomain.com \
293+
bash scripts/setup-waf-rules.sh
294+
```
295+
296+
The script is idempotent — safe to re-run. It requires a token with **Zone → WAF → Edit** permission.
297+
285298
---
286299

287300
## 12. Install the GitHub App on your repos

scripts/setup-waf-rules.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bash
2+
# Creates WAF skip rules for machine-to-machine endpoints that must not be
3+
# gated by Bot Fight Mode or Browser Integrity Check.
4+
#
5+
# /ingest — protected by GitHub Actions OIDC token
6+
# /webhooks/github — protected by HMAC webhook signature
7+
#
8+
# Usage:
9+
# CLOUDFLARE_API_TOKEN=... ZONE_DOMAIN=yourdomain.com bash scripts/setup-waf-rules.sh
10+
#
11+
# Requires: curl, jq
12+
13+
set -euo pipefail
14+
15+
: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN is required}"
16+
: "${ZONE_DOMAIN:?ZONE_DOMAIN is required (e.g. yourdomain.com)}"
17+
18+
API="https://api.cloudflare.com/client/v4"
19+
AUTH=(-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json")
20+
PHASE="http_request_firewall_custom"
21+
DESCRIPTION="Skip bot/BIC checks for OIDC+HMAC-protected endpoints"
22+
EXPRESSION='(http.request.uri.path eq "/ingest") or (http.request.uri.path eq "/webhooks/github")'
23+
24+
# 1. Look up zone ID by domain name
25+
echo "Looking up zone for ${ZONE_DOMAIN}..."
26+
ZONE_ID=$(curl -sf "${AUTH[@]}" "${API}/zones?name=${ZONE_DOMAIN}" \
27+
| jq -r '.result[0].id // empty')
28+
if [[ -z "${ZONE_ID}" ]]; then
29+
echo "Error: no zone found for ${ZONE_DOMAIN}. Check ZONE_DOMAIN and token permissions."
30+
exit 1
31+
fi
32+
echo "Zone ID: ${ZONE_ID}"
33+
34+
# 2. Get (or create) the WAF custom rules phase entrypoint
35+
ENTRYPOINT=$(curl -sf "${AUTH[@]}" "${API}/zones/${ZONE_ID}/rulesets/phases/${PHASE}/entrypoint" || echo '{"result":{}}')
36+
RULESET_ID=$(echo "${ENTRYPOINT}" | jq -r '.result.id // empty')
37+
38+
if [[ -z "${RULESET_ID}" ]]; then
39+
echo "No WAF custom ruleset found — creating empty entrypoint..."
40+
RULESET_ID=$(curl -sf -X PUT "${AUTH[@]}" \
41+
"${API}/zones/${ZONE_ID}/rulesets/phases/${PHASE}/entrypoint" \
42+
-d '{"rules":[]}' | jq -r '.result.id')
43+
fi
44+
echo "Ruleset ID: ${RULESET_ID}"
45+
46+
# 3. Check if the skip rule already exists (idempotent)
47+
EXISTING=$(curl -sf "${AUTH[@]}" "${API}/zones/${ZONE_ID}/rulesets/${RULESET_ID}" \
48+
| jq -r --arg desc "${DESCRIPTION}" '.result.rules[]? | select(.description == $desc) | .id')
49+
if [[ -n "${EXISTING}" ]]; then
50+
echo "Skip rule already exists (${EXISTING}) — nothing to do."
51+
exit 0
52+
fi
53+
54+
# 4. Add the skip rule
55+
echo "Adding skip rule..."
56+
RESULT=$(curl -sf -X POST "${AUTH[@]}" \
57+
"${API}/zones/${ZONE_ID}/rulesets/${RULESET_ID}/rules" \
58+
-d "{
59+
\"action\": \"skip\",
60+
\"action_parameters\": {
61+
\"products\": [\"botFightMode\", \"browserIntegrityCheck\"]
62+
},
63+
\"expression\": \"${EXPRESSION}\",
64+
\"description\": \"${DESCRIPTION}\",
65+
\"enabled\": true
66+
}")
67+
68+
RULE_ID=$(echo "${RESULT}" | jq -r '.result.id')
69+
echo "Done — rule ID: ${RULE_ID}"

0 commit comments

Comments
 (0)