Cloudflare API tokens runbook
System: Cloudflare API Tokens (User-level, zone-scoped)
Owner: sre-agent
Last incident: 2026-06-18 (BFM scope enumeration during CI→vault block; see docs/ops/incidents/2026-06-18-bfm-disabled-window.md)
Last reviewed: 2026-06-18 UTC
Related issues: #755 (provision CF_WAF_EDIT_RAXX_APP + CF_DNS_EDIT_GETRAXX_COM), #754 (token rename taxonomy), #81 (SDLC hardening epic)
Inventory
All tokens are stored in Infisical at /MooseQuest/cloudflare/ (env: prod). Each credential has three companion secrets:
| Vault key | CF token ID | Scope | Zone | Rotation cadence | Unblocks |
|---|---|---|---|---|---|
CF_WAF_EDIT_RAXX_APP |
7b3187849e3367a4e0a4ee343f5771df |
Zone:WAF:Edit, Zone:Settings:Edit, Zone:Logs:Edit, Zone:Zone:Read | raxx.app only |
90 days | #747, #719 |
CF_DNS_EDIT_GETRAXX_COM |
6f4a364c8edccc31e365efdd12ec5adf |
Zone:DNS:Edit, Zone:Zone:Read | getraxx.com only |
90 days | getraxx.com DNS ops |
CF_PAGES_DEPLOY |
see rotation INDEX | CF Pages write | all zones | 90 days | Pages deploys |
CF_ACCESS_MGMT / CLOUDFLARE_ACCESS_MGMT_TOKEN |
claude-cf-access-mgmt |
Access:Apps+Policies:Write, Service Tokens:Write, Zero Trust:Write, Account API Tokens:Write | account | 90 days | Access provisioning |
CF_PAGES_READ |
see rotation INDEX | CF Pages read | all zones | 90 days | Console site probes |
CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN |
c737f4b2222fef657aca94ed98af529e |
Zone:Bot Management:Write, Zone:WAF:Write, Zone:Settings:Write, Zone:Logs:Write, Zone:Firewall Services:Write, CF Access:Service Tokens:Write, API Tokens:Write, Pages:Write, D1:Write, and others (see note) | account + raxx.app zone | 90 days | BFM disable/enable (#3634), WAF state migration (#2328, #2378) |
Note on CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN: this is the broadest-scope token in the vault. It carries Zone:Bot Management:Write (CF group id 3b94c49258ec4573b06d51d99b6416c0) which is NOT present on any other vaulted CF token. It is the only token that can read or write the /zones/{zone_id}/bot_management endpoint. Use it for BFM enable/disable operations only; prefer the narrower CF_WAF_EDIT_RAXX_APP for WAF rule management.
Origin CA certificate issuance (added 2026-06-19): CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN has API Tokens:Write scope and can be used to mint short-lived tokens with Zone:SSL and Certificates:Write scope (CF permission group ID: c03055bc037c4ea9afb9a9f104b7b721). This is the correct path for issuing Cloudflare Origin Certificates for raxx.app zone origins (see freescout-cert-renewal.md Failure Mode E). No other vaulted token has SSL+Certificates:Write scope. CF_WAF_EDIT_RAXX_APP does NOT have this scope (confirmed 2026-06-19 incident — returned error 9109 on permission_groups endpoint).
Companion secret naming convention (per docs/archive/secrets/cf-token-taxonomy.md):
<TOKEN_NAME>__EXPIRES_AT # ISO-8601 expiry timestamp
<TOKEN_NAME>__SCOPES # Human-readable scope string
<TOKEN_NAME>__TOKEN_ID # Cloudflare token ID (not the secret value)
Zone IDs:
| Zone | ID |
|---|---|
raxx.app |
f12dbb5cac57d5591a5058874498a6d1 |
getraxx.com |
0bdcee38d1da2d021eb6166f0bd6204f |
raxx.io |
f3d1b89d86d1aa63a003f2826a8f841c |
The minting token (CLOUDFLARE_ACCESS_MGMT_TOKEN / CF_ACCESS_MGMT) has User:API Tokens:Edit
(i.e., API Tokens Write) scope and is used to mint, roll, and revoke all other CF API tokens
programmatically.
How to tell it's broken
- CF API call returns HTTP 403 with
{"message":"request is not authorized"}— scope mismatch; see failure mode A below. - CF API call returns HTTP 401 with
{"code":1000,"message":"Invalid API token"}— token expired, revoked, or wrong value in vault. - Console status poller shows a CF-dependent surface as degraded — check Heroku config var matches vault.
- A workflow using
CF_WAF_EDIT_RAXX_APPreturns 403 on the WAF entrypoint — token missing WAF:Edit scope or scoped to wrong zone.
How to diagnose (in order)
-
Identify which token the failing workflow uses. The workflow README or this runbook's consumer table is the source of truth.
-
Verify the token is active and verify its scope:
```bash # Read token from vault TOKEN=$(infisical secrets get CF_WAF_EDIT_RAXX_APP \ --path /MooseQuest/cloudflare --env prod --plain)
# Verify with Cloudflare curl -sS \ -H "Authorization: Bearer ${TOKEN}" \ "https://api.cloudflare.com/client/v4/user/tokens/verify" | python3 -m json.tool # Expect: {"success":true,"result":{"status":"active",...}} ```
- Check the
__EXPIRES_ATcompanion to see if the token has expired:
bash
infisical secrets get CF_WAF_EDIT_RAXX_APP__EXPIRES_AT \
--path /MooseQuest/cloudflare --env prod --plain
-
If the token is active but the workflow still returns 403, verify you are calling an endpoint within the token's zone.
CF_WAF_EDIT_RAXX_APPis scoped toraxx.apponly — it will return 403 on any other zone's WAF endpoint. -
If the token is expired or invalid, rotate it — see "Known failure modes" below.
Consumer map
| Token | Consumer | How it is used |
|---|---|---|
CF_WAF_EDIT_RAXX_APP |
sre-agent (CI/scripted) | Deploy and update WAF rate-limit rules on raxx.app (issues #747, #719) |
CF_WAF_EDIT_RAXX_APP |
docs/ops/runbooks/cloudflare-rate-limiting.md procedures |
WAF rule deploy/update/disable |
CF_DNS_EDIT_GETRAXX_COM |
Terraform / ad-hoc CLI | DNS record creation and updates for getraxx.com (new subdomains, MX records) |
CF_DNS_EDIT_GETRAXX_COM |
future: #948 Velvet CF adapter | Programmatic rotation of this token itself |
Known failure modes
Failure mode A: 403 on CF API — wrong scope or wrong zone
Symptom: {"message":"request is not authorized"} on a WAF or DNS endpoint.
Cause: Token does not have the required permission, OR the request targets a zone that is outside the token's zone restriction.
Fix: - Confirm you are using the right token for the right zone (see inventory table above). - If the token is correct but 403 persists, check the token's policy in the CF dashboard or via:
bash
MGMT_TOKEN=$(infisical secrets get CF_ACCESS_MGMT \
--path /MooseQuest/cloudflare --env prod --plain)
TOKEN_ID=$(infisical secrets get CF_WAF_EDIT_RAXX_APP__TOKEN_ID \
--path /MooseQuest/cloudflare --env prod --plain)
curl -sS \
-H "Authorization: Bearer ${MGMT_TOKEN}" \
"https://api.cloudflare.com/client/v4/user/tokens/${TOKEN_ID}" | python3 -m json.tool
Confirm policies[].resources contains only the expected zone ID, and permission_groups includes WAF Write (ID: fb6778dc191143babbfaa57993f1d275) or DNS Write (ID: 4755a26eedb94da69e1066d98aa820be).
Verification: Repeat the failing API call and confirm HTTP 200.
Failure mode B: 401 — token invalid (expired or revoked)
Symptom: {"code":1000,"message":"Invalid API token"} on any CF API call.
Cause: Token expired (past expires_on), was revoked manually, or the vault value drifted from CF's actual token secret (e.g., a roll happened outside Velvet).
Fix:
-
Check
__EXPIRES_ATcompanion to confirm expiry. -
Roll the token via Velvet (preferred — see "Rotation via Velvet" section below), or use the programmatic roll path:
```bash MGMT_TOKEN=$(infisical secrets get CF_ACCESS_MGMT \ --path /MooseQuest/cloudflare --env prod --plain) TOKEN_ID="d0cf0adeda107092f6e8c0646460f8f8" # CF_WAF_EDIT_RAXX_APP
NEW_VALUE=$(curl -sS -X PUT \ -H "Authorization: Bearer ${MGMT_TOKEN}" \ -H "Content-Type: application/json" \ "https://api.cloudflare.com/client/v4/user/tokens/${TOKEN_ID}/value" \ -d '{}' | python3 -c "import sys,json; print(json.load(sys.stdin)['result'])") # $NEW_VALUE is shown exactly once — do not echo it ```
- Write the new value to vault (do NOT echo):
bash
# Using Infisical API — omit stdout
See the rotation SOP at docs/ops/runbooks/rotation/cloudflare-user-api-token.md for the full propagate+verify+audit sequence.
Verification: GET /user/tokens/verify with the new value returns HTTP 200 and "status":"active".
Failure mode C: vault value missing or path not found
Symptom: infisical secrets get CF_WAF_EDIT_RAXX_APP ... returns 404.
Cause: Secret was deleted from Infisical, or path/env mismatch in the CLI call.
Fix:
- Confirm the path is
/MooseQuest/cloudflareand env isprod. - If genuinely missing: re-provision using the provisioning procedure below.
Failure mode D: CF_ACCESS_MGMT token itself is invalid
Symptom: All token minting/rolling operations fail with 401. The management token (CF_ACCESS_MGMT) is expired or revoked.
Cause: 90-day rotation cadence missed, or the token was revoked externally.
Fix: Escalate to operator. CF_ACCESS_MGMT must be rotated via the CF dashboard (dashboard fallback path in cloudflare-user-api-token.md) because it cannot roll itself — it requires a token with API Tokens Edit scope, which is itself.
Provisioning a new zone-scoped token
Use this procedure when a new zone-specific CF token is needed (e.g., a new zone is added to the account, or a new permission is required).
Prerequisites
CF_ACCESS_MGMT(vault:/MooseQuest/cloudflare/CF_ACCESS_MGMT) withAPI Tokens Writescope — confirmed active.- Zone ID for the target zone (see zone table above, or look up via
GET /zones). - Permission group IDs — retrieve fresh via
GET /user/tokens/permission_groups.
Key permission group IDs (as of 2026-05-04)
| Permission | Group ID | Scope type |
|---|---|---|
| Zone WAF Write | fb6778dc191143babbfaa57993f1d275 |
com.cloudflare.api.account.zone |
| Zone WAF Read | dbc512b354774852af2b5a5f4ba3d470 |
com.cloudflare.api.account.zone |
| DNS Write | 4755a26eedb94da69e1066d98aa820be |
com.cloudflare.api.account.zone |
| DNS Read | 82e64a83756745bbbb1c9c2701bf816b |
com.cloudflare.api.account.zone |
| Zone Read | c8fed203ed3043cba015a93ad1616f1f |
com.cloudflare.api.account.zone |
Steps
-
Authenticate to Infisical and retrieve
CF_ACCESS_MGMTtoken value. -
Mint the token:
```bash
MGMT_TOKEN="
RESP=$(curl -sS -X POST \
-H "Authorization: Bearer ${MGMT_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/user/tokens" \
-d "{
\"name\": \"CF_
-
Verify the token against the target endpoint (e.g., WAF phase entrypoint or DNS records list).
-
Write to Infisical at
/MooseQuest/cloudflare/: -<TOKEN_NAME>= token value -<TOKEN_NAME>__EXPIRES_AT= expiry timestamp -<TOKEN_NAME>__SCOPES= human-readable scope string -<TOKEN_NAME>__TOKEN_ID= CF token ID -
Add a row to
docs/ops/runbooks/rotation/INDEX.md. -
Add an entry to this runbook's inventory table and consumer map.
Rotation via Velvet
CF User API tokens are rotated via the PUT /user/tokens/{id}/value endpoint. This is the OPERATOR_MANUAL flow in Velvet v2 because Cloudflare User API Tokens cannot be minted programmatically without a separate management token — Velvet uses the management token (CF_ACCESS_MGMT) to perform the roll.
When Velvet CF adapter is wired (#948):
- Navigate to
https://raxx-console-prod.herokuapp.com/security/secrets. - Locate the credential row (e.g.,
CF_WAF_EDIT_RAXX_APP). - Click Rotate to open the Stage Wizard.
- Stage 1 (Verify): Velvet reads the current token from vault and calls
GET /user/tokens/{id}to confirm it is active. - Stage 2 (Mint + Distribute): Velvet calls
PUT /user/tokens/{id}/valueusingCF_ACCESS_MGMT. The rolled value is written to vault and propagated to registered consumers. - Stage 3 (Validate + Revoke): Velvet verifies each consumer healthcheck passes, then marks rotation as done. (CF token roll is atomic — the old value is already invalid after Stage 2.)
Manual fallback (until #948 ships):
Use the programmatic roll path documented in docs/ops/runbooks/rotation/cloudflare-user-api-token.md — Section 2 (Generate the new credential). Token IDs needed:
| Token name | CF Token ID |
|---|---|
CF_WAF_EDIT_RAXX_APP |
d0cf0adeda107092f6e8c0646460f8f8 |
CF_DNS_EDIT_GETRAXX_COM |
6f4a364c8edccc31e365efdd12ec5adf |
After rolling, update __EXPIRES_AT companion to 90 days from the rotation date.
Emergency stop
To immediately revoke a token (suspected compromise):
MGMT_TOKEN=$(infisical secrets get CF_ACCESS_MGMT \
--path /MooseQuest/cloudflare --env prod --plain)
# Delete the token — this revokes it immediately
curl -sS -X DELETE \
-H "Authorization: Bearer ${MGMT_TOKEN}" \
"https://api.cloudflare.com/client/v4/user/tokens/<token-id>"
# Expect: {"success":true,"result":{"id":"<token-id>"}}
After revocation:
1. Remove the secret from Infisical (or overwrite with REVOKED placeholder).
2. Identify all consumers from the consumer map above.
3. Remove or nullify the token from each consumer's config.
4. File a FreeScout incident ticket.
5. Provision a replacement token using the provisioning procedure above.
Escalation
Escalate to operator when:
CF_ACCESS_MGMTitself is invalid (can't roll tokens without it — no automated path).- A token appears active in CF but is returning 403 (scope corruption — rare but needs dashboard inspection).
- A token was deleted externally and consumers are live-impacted.
- The zone ID for a zone is not in the table above and cannot be confirmed via API.
CF dashboard (token management):
https://dash.cloudflare.com/profile/api-tokens