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 → 401
  • deploy-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-infra

Hot-swap sequence (< 30 s window)

  1. Open: dash.cloudflare.comMy ProfileAPI TokensGlobal API KeyViewRoll
  2. Click Roll and confirm — the old key is now invalid
  3. Immediately copy the new value (one-time display — do not close this page)
  4. Within 30 seconds — paste the value into the staged terminal and run both gh secret set commands:
    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
  5. Confirm the secret was updated:
    gh secret list -R radieu/p24-infra | Select-String "CF_GLOBAL_API_KEY"
  6. Update SOPS — key appears in two files:
    • secrets/monitoring.env.sops — used by credential-rotation.yml CI
    • secrets/administration.env.sops — admin reference Follow docs/playbooks/sops-edit-operations.md
  7. Update .env.local entry CF_GLOBAL_API_KEY
  8. 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"
  9. Verify rotation workflow completed:
    gh run list --workflow=credential-rotation.yml -R radieu/p24-infra --limit 1
  10. 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:

  1. The GH Secret update (step 4) fixes subsequent runs automatically — no manual secrets change needed
  2. Re-trigger the failed run:
    gh run rerun <run-id> -R radieu/p24-infra
  3. 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/.env

Verification

# 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 (mirrors CF_GLOBAL_API_KEY in secrets/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:

  1. Lists all account tokens via X-Auth-Key — finds old token by ID (verify) or name (p24-infra*).
  2. Preserves old token’s policies exactly; falls back to Workers Scripts Write + Zone DNS Write.
  3. Creates p24-infra-api-YYYY-MM-DD token with same policies.
  4. Updates GH Secret CF_API_TOKEN + SOPS secrets/monitoring.env.sops.
  5. Deletes old token.
  6. After PR merged: secrets-sync.yml auto-deploys to vps-i1 monitoring stack.

After rotation — trigger secrets-sync:

gh workflow run secrets-sync.yml --repo radieu/p24-infra -f target=vps-i1

5. 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):

  1. File GitHub issue: 🔴 CRITICAL: CF_GLOBAL_API_KEY rotation blocked -- Cloudflare account inaccessible
  2. Temporarily disable any workflows that use CLOUDFLARE_GLOBAL_API_KEY
  3. Contact Cloudflare support

References

  • scripts/rotate-credentials.py — auto-rotators
  • .github/workflows/credential-rotation.yml — weekly job
  • docs/secrets-rotation-log.md — rotation history
  • docs/playbooks/sops-windows-crlf.md — SOPS write procedure
  • docs/secrets-management.md — full secrets architecture