Raxx · internal docs

internal · gated ↑ index

Rotation SOP — Cloudflare User API Token

Mode: programmatic Last validated: 2026-05-01 UTC Validation method: read-only-docs Average duration: 5m Required role: superadmin

Applies to (new taxonomy names — #754): - CF_PAGES_DEPLOY (was CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN) - CF_ACCESS_MGMT (was CLOUDFLARE_ACCESS_MGMT_TOKEN) — the management token itself; see cloudflare-access-service-token.md for Access service tokens - CF_PAGES_READ (was CLOUDFLARE_PAGES_READ_TOKEN)

Legacy names (CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN, CLOUDFLARE_ACCESS_MGMT_TOKEN, CLOUDFLARE_PAGES_READ_TOKEN) continue to work until removed by the cleanup card (see docs/secrets/cf-token-taxonomy.md).

Important correction to earlier verbal summary: Cloudflare User API Tokens can be rolled programmatically via PUT /user/tokens/{id}/value. The dashboard "Roll" button is one path, but it is not the only path. The Global API Key cannot be rolled (and we do not store it). This SOP defaults to the programmatic path; the dashboard path is documented as a fallback for cases where the calling token has lost permission to roll itself.

When to run

Prerequisites

Steps

1. Pre-rotation checks

# Confirm the token is currently working (sanity check before rolling)
curl -sS -H "Authorization: Bearer $CURRENT_TOKEN" \
  https://api.cloudflare.com/client/v4/user/tokens/verify | jq .
# Expect: {"success":true,"result":{"status":"active",...}}

Identify the token_id to roll:

curl -sS -H "Authorization: Bearer $ROLLING_TOKEN" \
  https://api.cloudflare.com/client/v4/user/tokens | jq '.result[] | {id, name, status}'

2. Generate the new credential

Programmatic (preferred):

NEW_VALUE=$(curl -sS -X PUT \
  -H "Authorization: Bearer $ROLLING_TOKEN" \
  -H "Content-Type: application/json" \
  https://api.cloudflare.com/client/v4/user/tokens/$TOKEN_ID/value \
  --data '{}' | jq -r '.result')
# $NEW_VALUE is the new token secret. Capture once; not retrievable again.

PUT /user/tokens/{token_id}/value rolls the secret in place — the token_id, name, scopes, and metadata are preserved. The old secret is invalidated atomically.

Dashboard fallback (use only if the programmatic path is unavailable): 1. Navigate to https://dash.cloudflare.com/profile/api-tokens. 2. Locate the token by name. 3. Click the menu icon → Roll. 4. Confirm. Copy the new value (shown once).

3. Validate the new credential

curl -sS -H "Authorization: Bearer $NEW_VALUE" \
  https://api.cloudflare.com/client/v4/user/tokens/verify | jq .
# Expect: {"success":true,"result":{"status":"active",...}}

For CLOUDFLARE_PAGES_READ_TOKEN, additionally:

curl -sS -H "Authorization: Bearer $NEW_VALUE" \
  https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/pages/projects | jq '.result | length'
# Expect a non-zero project count.

4. Store in Infisical

Update the Infisical secret in the appropriate project (Raptor backend / console / GitHub Actions). Use Infisical's web UI or CLI:

# Write new taxonomy name (primary) + legacy name (for backward compat during migration)
infisical secrets set CF_PAGES_DEPLOY="$NEW_VALUE" \
  --path /MooseQuest/cloudflare/ --env=prod
infisical secrets set CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN="$NEW_VALUE" \
  --path /MooseQuest/cloudflare/ --env=prod   # legacy alias — remove after cleanup card

Infisical retains version history automatically — the previous value is recoverable for the rollback window.

5. Propagate to downstream consumers

Consumer How
Heroku Raptor (prod + staging) heroku config:set CF_PAGES_DEPLOY="$NEW_VALUE" -a raxx-api-prod (and -staging). Also set legacy: heroku config:set CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN="$NEW_VALUE" -a raxx-api-prod. Triggers dyno restart.
GitHub Actions gh secret set CF_PAGES_DEPLOY -a actions -b "$NEW_VALUE" (and set legacy CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN until workflows are updated)
Operator local env Update manually after Infisical sync — DM via Slack D0AJ7K184TV
Console (raxx-console-prod) heroku config:set CF_ACCESS_MGMT="$NEW_VALUE" -a raxx-console-prod (legacy: CLOUDFLARE_ACCESS_MGMT_TOKEN)

6. Verify downstream

After Heroku dyno restart completes:

# Raptor: confirm the new token is in use by hitting a CF-dependent endpoint
curl -sS https://api.raxx.app/health/dependencies | jq '.cloudflare'
# Expect: {"ok": true, ...}

# Console: confirm CF Pages probes still pass
curl -sS https://console.raxx.app/api/status/sites \
  -H "Cookie: <ops session>" | jq '.sites[] | select(.provider=="cloudflare_pages")'
# Expect: liveness.ok = true for all CF Pages sites.

7. Revoke the old credential

The PUT /user/tokens/{id}/value call already invalidated the old secret — no separate revocation step needed. To confirm:

curl -sS -H "Authorization: Bearer $OLD_VALUE" \
  https://api.cloudflare.com/client/v4/user/tokens/verify
# Expect: HTTP 401 / {"success":false,"errors":[{"code":1000,"message":"Invalid API token"}]}

8. Audit log entry

Write to console_audit_log via the rotation pipeline callback (#253). Manual operator entry if rotated outside the console UI:

action: secret.rotate.completed
actor: <admin_id>
context: {"secret_name": "CF_PAGES_DEPLOY", "method": "programmatic", "rotated_at": "2026-05-01T12:00:00Z"}

Rollback

The previous token value is invalidated at the moment the new one is generated — there is no "revert to old token" path. Rollback options:

  1. Roll forward: if the new token fails validation, generate another fresh one (same PUT call, repeat until validated). Each call invalidates the previous attempt.
  2. Recover from Infisical history: only useful if the new value was lost before storage. Infisical retains the previous stored value, but Cloudflare does not retain the previous secret. The Infisical history value will not authenticate against Cloudflare after a roll.
  3. Create a replacement token from scratch: if the rolled token is fundamentally broken (e.g., scope corruption — extremely rare), use the dashboard to create a new token with the same scopes, store in Infisical with a new name, propagate, then delete the broken one.

Vendor doc references

Known gotchas