Raxx · internal docs

internal · gated

Heroku API Key — Rotation Runbook

System: Heroku Platform API credential (HEROKU_API_KEY) Owner: sre-agent / operator (Kristerpher) Cadence: Every 90 days (quarterly) Detailed per-step SOP: docs/ops/runbooks/rotation/heroku-platform-token.md Drift recovery: docs/ops/runbooks/heroku-api-key-drift-recovery.md Issue: #251 Last rotated: 2026-07-01 UTC (sre-agent, operator-authorized) Next rotation due: 2026-10-01 UTC


What this key does

HEROKU_API_KEY is a global-scope Heroku OAuth authorization used by:

Consumer How it is read
CI/CD (deploy-heroku.yml) GitHub Actions secret HEROKU_API_KEY
Vault (source of truth) Infisical /MooseQuest/heroku/HEROKU_API_KEY, env prod
Agent sessions (SRE, ops) Session-bootstrapped from vault via session-bootstrap.sh

The billing-collector key (HEROKU_BILLING_API_KEY) is a separate read-scoped authorization rotated on its own schedule — it is NOT covered here.


Rotation procedure (summary)

Follow docs/ops/runbooks/rotation/heroku-platform-token.md for the full copy-pasteable commands. The high-level sequence is:

  1. List existing authorizations — capture the OLD authorization ID.
  2. Create new authorization via POST /oauth/authorizations (global scope). Description convention: raxx-ci-rotation-YYYY-MM.
  3. Verify new tokenGET /account must return kris@moosequest.net. Also verify app-list read (GET /apps). Abort if either fails.
  4. Update all stores (in this order, never out of order): - Infisical vault: PATCH /api/v3/secrets/raw/HEROKU_API_KEY (path /MooseQuest/heroku, env prod) - GitHub Actions secret: gh secret set HEROKU_API_KEY --repo raxx-app/TradeMasterAPI
  5. Re-verify vault — re-read the vault entry and confirm its suffix matches the new token. Abort if it does not.
  6. Revoke OLD authorizationheroku authorizations:revoke <OLD_ID>. Use the NEW key to make the revoke call. Verify the old authorization no longer appears in heroku authorizations.
  7. Record audit entry — log secret.rotate.completed in the rotation log below and update Last rotated / Next rotation due in this file.

Guardrails (never skip)


Rotation schedule

Trigger Cadence
Scheduled Every 90 days (quarterly). Next: 2026-10-01 UTC
Off-cycle Suspected compromise, accidental log/commit of value, employee offboarding
Post-incident Any incident where the key was exposed, transmitted in plaintext, or logged

Scheduled reminder

A GitHub Actions cron should enforce the 90-day cadence. Until a dedicated credential-expiry-monitor workflow exists, track via a recurring GitHub issue or calendar event. Create a type:reliability issue due 1 week before each rotation date so it lands in the weekly sweep.

Suggested cron check (to be added to a future credential-monitor workflow):

# .github/workflows/credential-expiry-monitor.yml (future)
# Fires 7 days before each rotation due date; opens a reminder issue.
on:
  schedule:
    - cron: '0 8 24 9 *'   # 2026-09-24 08:00 UTC — one week before 2026-10-01
    - cron: '0 8 24 12 *'  # 2026-12-24 08:00 UTC — one week before 2027-01-01

Until that workflow exists: set a calendar reminder for 2026-09-24 UTC so the rotation runs before the key reaches 90 days old (created 2026-07-01).


Rotation log

Date (UTC) Actor Old auth description New auth ID Notes
2026-07-01 sre-agent (operator-authorized, issue #251) raxx-automation: dispatch + git push (rotated 2026-05-02) (b1e6320e) 85f3a15a (raxx-ci-rotation-2026-07) Vault/session drift detected: session held prod-deploy-key (1f970877), vault held raxx-automation — documented below. Orphan auth 558f0e9d also revoked.

Authorization inventory (at 2026-07-01 rotation)

After the 2026-07-01 rotation, the following Heroku authorizations remain. Entries not maintained by this runbook are flagged for the operator.

Authorization ID (prefix) Description Scope Status
85f3a15a raxx-ci-rotation-2026-07 global Current CI/vault key — this runbook
1f970877 prod-deploy-key global Session bootstrap key (operator). Separate from vault key; see drift note below.
f44d9427 github-actions-craps-deploy global Legacy CI key — candidate for revocation
73f5fe49 raxx-platform-token-2026-05-06 global Stale — candidate for revocation
9d5ec3c4 raxx-platform-token-2026-05-06 global Stale duplicate — candidate for revocation
99f3c0a4 billing-collector-readonly-2026-05-06 global Billing stale — candidate for revocation
059730bb claude-code-2026 global Claude Code IDE session — not managed here
565e7b32 raxx-billing-collector global Billing — rotated separately
0ef029d2, 6952371b, dce15776 billing collector — Raxx Console - 2026-06-04 read Billing read-only — rotated separately

Recommended operator cleanup (next maintenance window): Revoke 73f5fe49, 9d5ec3c4, f44d9427, and 99f3c0a4. These are not in active use.


Vault/session drift note (2026-07-01 discovery)

During the 2026-07-01 rotation, the vault key and the session-bootstrapped key were discovered to be different authorizations:

Both were valid at time of discovery. The vault key was rotated (new vault key is 85f3a15a). The prod-deploy-key (1f970877) was NOT revoked — it is still active. The operator's session HEROKU_API_KEY_PROD now points to a key that exists but is NOT the vault-canonical key.

Action required: After verifying session-bootstrap.sh reads from vault on refresh, revoke 1f970877 manually:

heroku authorizations:revoke 1f970877-9b2f-4899-ac67-030049fd7833

Only do this AFTER confirming the next session bootstrap reads the new vault key.


Drift detection

Between rotations, the following three stores must hold the same token:

  1. Infisical vault: /MooseQuest/heroku/HEROKU_API_KEY (env: prod)
  2. GitHub Actions secret: HEROKU_API_KEY on raxx-app/TradeMasterAPI
  3. Session bootstrap: HEROKU_API_KEY_PROD exported by session-bootstrap.sh

Drift symptom: CI deploy fails with Error: The token provided to HEROKU_API_KEY is invalid. See heroku-api-key-drift-recovery.md for recovery procedure.

Proactive check (monthly):

# Confirm vault key authenticates to Heroku (no value printed)
VAULT_KEY=$(infisical secrets get HEROKU_API_KEY \
  --env=prod --path=/MooseQuest/heroku --plain)
STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \
  -H "Accept: application/vnd.heroku+json; version=3" \
  -H "Authorization: Bearer $VAULT_KEY" \
  https://api.heroku.com/account)
echo "Vault key HTTP status: $STATUS"   # expect 200

Escalation

Wake the operator when:

Contact: ops@raxx.app or Slack D0AJ7K184TV.


References