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:
- List existing authorizations — capture the OLD authorization ID.
- Create new authorization via
POST /oauth/authorizations(global scope). Description convention:raxx-ci-rotation-YYYY-MM. - Verify new token —
GET /accountmust returnkris@moosequest.net. Also verify app-list read (GET /apps). Abort if either fails. - Update all stores (in this order, never out of order):
- Infisical vault:
PATCH /api/v3/secrets/raw/HEROKU_API_KEY(path/MooseQuest/heroku, envprod) - GitHub Actions secret:gh secret set HEROKU_API_KEY --repo raxx-app/TradeMasterAPI - Re-verify vault — re-read the vault entry and confirm its suffix matches the new token. Abort if it does not.
- Revoke OLD authorization —
heroku authorizations:revoke <OLD_ID>. Use the NEW key to make the revoke call. Verify the old authorization no longer appears inheroku authorizations. - Record audit entry — log
secret.rotate.completedin the rotation log below and updateLast rotated/Next rotation duein this file.
Guardrails (never skip)
- Verify the new token works BEFORE revoking the old one. No downtime gap.
- If you cannot identify the old authorization ID precisely via suffix matching, STOP and escalate rather than blind-revoke.
- Never print secret values to stdout/logs. Use suffix-only comparisons for validation.
- If more than one
raxx-ci-rotation-YYYY-MMauthorization is created (e.g. due to a retry), revoke all but the one written to vault before proceeding.
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:
- Vault (source of truth):
raxx-automation: dispatch + git push(b1e6320e) — rotated out - Session (
HEROKU_API_KEY_PROD):prod-deploy-key(1f970877) — still active
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:
- Infisical vault:
/MooseQuest/heroku/HEROKU_API_KEY(env: prod) - GitHub Actions secret:
HEROKU_API_KEYonraxx-app/TradeMasterAPI - Session bootstrap:
HEROKU_API_KEY_PRODexported bysession-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:
- A rotation attempt fails at the vault-write step (new token created but not stored — two valid tokens in flight; revoke the new one and restart).
- The old authorization cannot be identified by suffix matching (possible if the key was set via a mechanism that does not create a named authorization).
- Any rotation step returns an unexpected 5xx from Heroku.
- More than one rotation attempt leaves orphan authorizations and deduplication is unclear.
Contact: ops@raxx.app or Slack D0AJ7K184TV.
References
- Full per-step SOP:
docs/ops/runbooks/rotation/heroku-platform-token.md - Drift recovery:
docs/ops/runbooks/heroku-api-key-drift-recovery.md - Session bootstrap:
docs/ops/runbooks/session-bootstrap.md - Heroku OAuth docs:
https://devcenter.heroku.com/articles/oauth - Heroku Platform API:
https://devcenter.heroku.com/articles/platform-api-reference - Issue: #251