Playbook: Cloudflare Credential Rotation
Covers: CF_GLOBAL_API_KEY · CLOUDFLARE_TOKEN_ZINTEGROWANA · CF_EDIT_ALL_ZONES_API_TOKEN
1. CF_GLOBAL_API_KEY — Manual rotation (UI only)
Cloudflare Global API Key has no rotation API — UI only via the “Roll” button. The Roll atomically invalidates the old key. Two workflows break if triggered during the window between Roll and GH Secret update:
deploy-p24-auth-worker.yml— wrangler deploy + secret binding → 401deploy-meta-dispatcher.yml— wrangler deploy + secret binding → 401
Target window: < 30 seconds. Follow the pre-flight and hot-swap sequence exactly.
When to rotate: every 180 days, or immediately on suspected exposure.
Pre-flight (run BEFORE opening the Cloudflare dashboard)
# 1. Confirm no CF Worker deploys are in-flight (both must return [])
gh run list --workflow=deploy-p24-auth-worker.yml -R radieu/p24-infra --status in_progress --json databaseId,status
gh run list --workflow=deploy-meta-dispatcher.yml -R radieu/p24-infra --status in_progress --json databaseId,status
# 2. Stage the update commands in a terminal — do NOT run them yet
# (open a separate PowerShell window, paste these lines, leave cursor after -b "")
#
# gh secret set CF_GLOBAL_API_KEY -b "PASTE_HERE" -R radieu/p24-infra; `
# gh secret set CLOUDFLARE_GLOBAL_API_KEY -b "PASTE_HERE" -R radieu/p24-infraHot-swap sequence (< 30 s window)
- Open:
dash.cloudflare.com→ My Profile → API Tokens → Global API Key → View → Roll - Click Roll and confirm — the old key is now invalid
- Immediately copy the new value (one-time display — do not close this page)
- Within 30 seconds — paste the value into the staged terminal and run both
gh secret setcommands:gh secret set CF_GLOBAL_API_KEY -b "<new_value>" -R radieu/p24-infra gh secret set CLOUDFLARE_GLOBAL_API_KEY -b "<new_value>" -R radieu/p24-infra - Confirm the secret was updated:
gh secret list -R radieu/p24-infra | Select-String "CF_GLOBAL_API_KEY" - Update SOPS — key appears in two files:
secrets/monitoring.env.sops— used bycredential-rotation.ymlCIsecrets/administration.env.sops— admin reference Followdocs/playbooks/sops-edit-operations.md
- Update
.env.localentryCF_GLOBAL_API_KEY - Trigger dependent-token rotation (GH Secret is live — safe to run now):
gh workflow run credential-rotation.yml -R radieu/p24-infra ` -f dry_run=false ` -f force_all=false ` -f only_services="CLOUDFLARE_TOKEN_ZINTEGROWANA,CF_EDIT_ALL_ZONES_API_TOKEN" - Verify rotation workflow completed:
gh run list --workflow=credential-rotation.yml -R radieu/p24-infra --limit 1 - Log in
docs/secrets-rotation-log.md
If a deploy fires during the ~30 s window
If deploy-p24-auth-worker.yml or deploy-meta-dispatcher.yml fails with 401 after the Roll:
- The GH Secret update (step 4) fixes subsequent runs automatically — no manual secrets change needed
- Re-trigger the failed run:
gh run rerun <run-id> -R radieu/p24-infra - No other intervention needed.
Why not fully zero-downtime?
Cloudflare issues one Global API Key per account — there is no way to hold two active Global Keys
simultaneously (unlike zone-scoped API tokens which support parallel validity). The workaround is
using scoped tokens (CLOUDFLARE_TOKEN_ZINTEGROWANA, CF_EDIT_ALL_ZONES_API_TOKEN) for all
day-to-day operations. Future: migrate all consumers off the Global Key entirely — see
per-service-credential-isolation-plan.md §6.
2. CLOUDFLARE_TOKEN_ZINTEGROWANA — Auto-rotation
Zone-scoped token: DNS Edit for zintegrowana.online.
Rotation frequency: 180 days (automated weekly check).
Auto-rotator: rotate_cloudflare_token_zintegrowana in scripts/rotate-credentials.py
Requires: CF_GLOBAL_API_KEY to have been rotated first (token rotation needs User:API Tokens:Edit).
Manual trigger
gh workflow run credential-rotation.yml -R radieu/p24-infra `
-f dry_run=false `
-f force_all=false `
-f only_services="CLOUDFLARE_TOKEN_ZINTEGROWANA"SOPS update after rotation
The script updates GH Secret but NOT SOPS. Update manually:
# Get new value from GH Secrets (one-time after workflow run)
# Follow docs/playbooks/sops-windows-crlf.md to update secrets/monitoring.env.sops
# Then push -- secrets-sync.yml deploys to vps-i1 monitoring/.envVerification
# Confirm old token is invalid (should return 401/invalid)
$new = (gh secret list -R radieu/p24-infra --json name | ConvertFrom-Json | Where-Object name -eq CLOUDFLARE_TOKEN_ZINTEGROWANA)
# Actual value test -- DNS lookup via CF API
Invoke-RestMethod "https://api.cloudflare.com/client/v4/zones/57cb3d8f24c7cc319fb703394edc7b87/dns_records?per_page=1" `
-Headers @{ Authorization = "Bearer $env:CLOUDFLARE_TOKEN_ZINTEGROWANA" }3. CF_EDIT_ALL_ZONES_API_TOKEN — Auto-rotation
Broad-scope token: DNS Edit for ALL zones.
Rotation frequency: 180 days (automated weekly check).
Auto-rotator: rotate_cf_edit_all_zones_token in scripts/rotate-credentials.py
Requires: CF_GLOBAL_API_KEY to be current (same constraint as above).
Manual trigger
gh workflow run credential-rotation.yml -R radieu/p24-infra `
-f dry_run=false `
-f force_all=false `
-f only_services="CF_EDIT_ALL_ZONES_API_TOKEN"Register in dev_r_services (one-time, if not already present)
Run via Supabase dashboard SQL editor or migration:
INSERT INTO dev_r_services (
service_name, element_type, status, criticality,
rotation_freq, last_rotated, next_due, auto_rotate,
project_id, description, workbook_url, compliance_workbook
) VALUES (
'CF_EDIT_ALL_ZONES_API_TOKEN', 'credential', 'active', 'critical',
'180 days', CURRENT_DATE, CURRENT_DATE + interval '180 days', true,
'p24-infra',
'Cloudflare API Token -- DNS Edit for all zones. Used by cloudflare-security-check.yml, dns-manager.py, and CF Worker management.',
'docs/playbooks/cloudflare-credential-rotation.md', 'yes'
) ON CONFLICT (service_name) DO UPDATE
SET auto_rotate = true,
rotation_freq = '180 days',
workbook_url = EXCLUDED.workbook_url,
compliance_workbook = 'yes',
updated_at = now();SOPS update after rotation
Same pattern as CLOUDFLARE_TOKEN_ZINTEGROWANA — update secrets/monitoring.env.sops manually after workflow runs.
4. CF_API_TOKEN — Auto-rotation (previously Tier 3, promoted 2026-07-08)
The p24-infra general-purpose API token (Workers Scripts Write + Zone DNS Write scope).
Previously marked manual-only because it was mistakenly assumed to need User:API Tokens:Edit
to self-rotate. Correct approach: use CF_GLOBAL_API_KEY (X-Auth-Key) to create a new token
and delete the old one — no User:API Tokens:Edit on CF_API_TOKEN required.
Auto-rotator: rotate_cf_api_token in scripts/rotate-credentials.py
Requires:
CLOUDFLARE_GLOBAL_API_KEY— from GH Secret (mirrorsCF_GLOBAL_API_KEYinsecrets/administration.env.sops)CLOUDFLARE_EMAIL— from GH Secret (Cloudflare account email)
Manual trigger:
gh workflow run credential-rotation.yml -R radieu/p24-infra `
-f dry_run=false `
-f force_all=true `
-f only_services="CF_API_TOKEN"What the rotator does:
- Lists all account tokens via X-Auth-Key — finds old token by ID (verify) or name (
p24-infra*). - Preserves old token’s policies exactly; falls back to Workers Scripts Write + Zone DNS Write.
- Creates
p24-infra-api-YYYY-MM-DDtoken with same policies. - Updates GH Secret
CF_API_TOKEN+ SOPSsecrets/monitoring.env.sops. - Deletes old token.
- After PR merged:
secrets-sync.ymlauto-deploys to vps-i1 monitoring stack.
After rotation — trigger secrets-sync:
gh workflow run secrets-sync.yml --repo radieu/p24-infra -f target=vps-i15. Dry-run testing
Before live rotation, verify the implementation:
gh workflow run credential-rotation.yml -R radieu/p24-infra `
-f dry_run=true `
-f force_all=true `
-f only_services="CLOUDFLARE_TOKEN_ZINTEGROWANA,CF_EDIT_ALL_ZONES_API_TOKEN,CF_API_TOKEN"Expected output: [DRY RUN] CLOUDFLARE_TOKEN_ZINTEGROWANA, [DRY RUN] CF_EDIT_ALL_ZONES_API_TOKEN, and [DRY RUN] CF_API_TOKEN.
6. Rotation log entry format
Append to docs/secrets-rotation-log.md:
| <YYYY-MM-DD HH:MM UTC> | — | CF_GLOBAL_API_KEY | <reason> | radieu | yes |
| <YYYY-MM-DD HH:MM UTC> | — | CLOUDFLARE_TOKEN_ZINTEGROWANA | <reason> | auto | yes |
| <YYYY-MM-DD HH:MM UTC> | — | CF_EDIT_ALL_ZONES_API_TOKEN | <reason> | auto | yes |
| <YYYY-MM-DD HH:MM UTC> | — | CF_API_TOKEN | <reason> | auto | yes |
6b. Log operations to infra_operations audit log
After each rotation, log to the central ops audit table:
# Bash (on Linux / VPS):
source /opt/p24-infra/scripts/lib/log_op.sh
log_op "radieu" "credential_rotation" "CF_GLOBAL_API_KEY" "success" \
"Scheduled 180d rotation — rolled via Cloudflare dashboard" "ci"
log_op "auto" "credential_rotation" "CLOUDFLARE_TOKEN_ZINTEGROWANA" "success" \
"Scheduled 180d rotation — auto-rotated by credential-rotation.yml" "ci"# PowerShell (Windows dev machine):
$env:SUPABASE_URL = (Get-Content "d:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_URL=").ToString().Split("=",2)[1].Trim()
$env:SUPABASE_SERVICE_KEY = (Get-Content "d:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_SERVICE_KEY=").ToString().Split("=",2)[1].Trim()
# Call log_op() from Python:
python -c "
import os; os.environ['SUPABASE_URL'] = os.environ.get('SUPABASE_URL','')
os.environ['SUPABASE_SERVICE_KEY'] = os.environ.get('SUPABASE_SERVICE_KEY','')
import sys; sys.path.insert(0, 'd:/code_2026/p24-infra')
from scripts.lib.log_op import log_op
log_op('radieu', 'credential_rotation', 'CF_GLOBAL_API_KEY', 'success',
'Scheduled 180d rotation via Cloudflare dashboard', 'ci')
"7. Escalation
If the Global API Key cannot be rolled (MFA issue, account locked):
- File GitHub issue:
🔴 CRITICAL: CF_GLOBAL_API_KEY rotation blocked -- Cloudflare account inaccessible - Temporarily disable any workflows that use
CLOUDFLARE_GLOBAL_API_KEY - Contact Cloudflare support
References
scripts/rotate-credentials.py— auto-rotators.github/workflows/credential-rotation.yml— weekly jobdocs/secrets-rotation-log.md— rotation historydocs/playbooks/sops-windows-crlf.md— SOPS write proceduredocs/secrets-management.md— full secrets architecture