System: Cloudflare API Tokens (User-level, zone-scoped) Owner: sre-agent Last incident: n/a Last reviewed: 2026-05-04 UTC Related issues: #755 (provision CF_WAF_EDIT_RAXX_APP + CF_DNS_EDIT_GETRAXX_COM), #754 (token rename taxonomy), #81 (SDLC hardening epic)
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 |
d0cf0adeda107092f6e8c0646460f8f8 |
Zone:WAF: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 |
see rotation INDEX | CF Access management | — | 90 days | Access provisioning |
CF_PAGES_READ |
see rotation INDEX | CF Pages read | all zones | 90 days | Console site probes |
Companion secret naming convention (per docs/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.
{"message":"request is not authorized"} — scope mismatch; see failure mode A below.{"code":1000,"message":"Invalid API token"} — token expired, revoked, or wrong value in vault.CF_WAF_EDIT_RAXX_APP returns 403 on the WAF entrypoint — token missing WAF:Edit scope or scoped to wrong zone.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",...}} ```
__EXPIRES_AT companion 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_APP is scoped to raxx.app only — 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.
| 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 |
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.
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_AT companion 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 ```
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".
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:
/MooseQuest/cloudflare and env is prod.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.
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).
CF_ACCESS_MGMT (vault: /MooseQuest/cloudflare/CF_ACCESS_MGMT) with API Tokens Write scope — confirmed active.GET /zones).GET /user/tokens/permission_groups.| 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 |
Authenticate to Infisical and retrieve CF_ACCESS_MGMT token 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.
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):
https://raxx-console-prod.herokuapp.com/security/secrets.CF_WAF_EDIT_RAXX_APP).GET /user/tokens/{id} to confirm it is active.PUT /user/tokens/{id}/value using CF_ACCESS_MGMT. The rolled value is written to vault and propagated to registered consumers.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.
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.
Escalate to operator when:
CF_ACCESS_MGMT itself is invalid (can't roll tokens without it — no automated path).CF dashboard (token management):
https://dash.cloudflare.com/profile/api-tokens