Skip to content

Commit 2f6ae52

Browse files
tbjersclaude
andauthored
Convert WAF setup script from bash+jq to Node.js (#7)
* Convert WAF setup script from bash+jq to Node.js Replaces setup-waf-rules.sh with setup-waf-rules.mjs using built-in fetch (Node 18+). No external dependencies — avoids the jq-not-found issue on Windows/Git Bash. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix WAF skip rule: use 'bic' product name, drop botFightMode 'browserIntegrityCheck' is not a valid product name — the correct value is 'bic'. 'botFightMode' cannot be skipped per-path via WAF rules at all; it must be toggled zone-wide. The 403 'Just a moment...' response is BIC, so skipping 'bic' is sufficient. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 31ac4ae commit 2f6ae52

3 files changed

Lines changed: 86 additions & 70 deletions

File tree

docs/INSTALLATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ Run the provided setup script to add a WAF skip rule for those paths:
290290

291291
```bash
292292
CLOUDFLARE_API_TOKEN=<your-token> ZONE_DOMAIN=yourdomain.com \
293-
bash scripts/setup-waf-rules.sh
293+
node scripts/setup-waf-rules.mjs
294294
```
295295

296296
The script is idempotent — safe to re-run. It requires a token with **Zone → WAF → Edit** permission.

scripts/setup-waf-rules.mjs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
// Creates a WAF skip rule to disable Browser Integrity Check (BIC) for
3+
// machine-to-machine endpoints. BIC challenges non-browser User-Agents,
4+
// which breaks GitHub Actions runners posting to /ingest.
5+
//
6+
// Note: Bot Fight Mode cannot be bypassed per-path via WAF rules — it must
7+
// be toggled at the zone level (Security → Bots) if it causes issues.
8+
//
9+
// /ingest — protected by GitHub Actions OIDC token
10+
// /webhooks/github — protected by HMAC webhook signature
11+
//
12+
// Usage:
13+
// CLOUDFLARE_API_TOKEN=... ZONE_DOMAIN=yourdomain.com node scripts/setup-waf-rules.mjs
14+
//
15+
// Requires: Node.js 18+ (uses built-in fetch)
16+
17+
const { CLOUDFLARE_API_TOKEN, ZONE_DOMAIN } = process.env;
18+
if (!CLOUDFLARE_API_TOKEN) throw new Error('CLOUDFLARE_API_TOKEN is required');
19+
if (!ZONE_DOMAIN) throw new Error('ZONE_DOMAIN is required (e.g. yourdomain.com)');
20+
21+
const API = 'https://api.cloudflare.com/client/v4';
22+
const PHASE = 'http_request_firewall_custom';
23+
const DESCRIPTION = 'Skip bot/BIC checks for OIDC+HMAC-protected endpoints';
24+
const EXPRESSION =
25+
'(http.request.uri.path eq "/ingest") or (http.request.uri.path eq "/webhooks/github")';
26+
27+
const headers = {
28+
Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
29+
'Content-Type': 'application/json',
30+
};
31+
32+
async function cf(path, options = {}) {
33+
const res = await fetch(`${API}${path}`, { headers, ...options });
34+
const body = await res.json();
35+
if (!body.success) {
36+
throw new Error(`Cloudflare API error on ${path}: ${JSON.stringify(body.errors)}`);
37+
}
38+
return body.result;
39+
}
40+
41+
// 1. Look up zone ID by domain name
42+
console.log(`Looking up zone for ${ZONE_DOMAIN}...`);
43+
const zones = await cf(`/zones?name=${ZONE_DOMAIN}`);
44+
if (!zones.length) {
45+
throw new Error(`No zone found for ${ZONE_DOMAIN}. Check ZONE_DOMAIN and token permissions.`);
46+
}
47+
const zoneId = zones[0].id;
48+
console.log(`Zone ID: ${zoneId}`);
49+
50+
// 2. Get (or create) the WAF custom rules phase entrypoint
51+
let rulesetId;
52+
try {
53+
const entrypoint = await cf(`/zones/${zoneId}/rulesets/phases/${PHASE}/entrypoint`);
54+
rulesetId = entrypoint.id;
55+
} catch {
56+
console.log('No WAF custom ruleset found — creating empty entrypoint...');
57+
const created = await cf(`/zones/${zoneId}/rulesets/phases/${PHASE}/entrypoint`, {
58+
method: 'PUT',
59+
body: JSON.stringify({ rules: [] }),
60+
});
61+
rulesetId = created.id;
62+
}
63+
console.log(`Ruleset ID: ${rulesetId}`);
64+
65+
// 3. Check if the skip rule already exists (idempotent)
66+
const ruleset = await cf(`/zones/${zoneId}/rulesets/${rulesetId}`);
67+
const existing = ruleset.rules?.find((r) => r.description === DESCRIPTION);
68+
if (existing) {
69+
console.log(`Skip rule already exists (${existing.id}) — nothing to do.`);
70+
process.exit(0);
71+
}
72+
73+
// 4. Add the skip rule
74+
console.log('Adding skip rule...');
75+
const added = await cf(`/zones/${zoneId}/rulesets/${rulesetId}/rules`, {
76+
method: 'POST',
77+
body: JSON.stringify({
78+
action: 'skip',
79+
action_parameters: { products: ['bic'] },
80+
expression: EXPRESSION,
81+
description: DESCRIPTION,
82+
enabled: true,
83+
}),
84+
});
85+
console.log(`Done — rule ID: ${added.id}`);

scripts/setup-waf-rules.sh

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)