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-21 UTC (sre-agent, operator-authorized full credential rotation — see docs/incidents/2026-07-21-full-credential-rotation.md) Next rotation due: 2026-10-19 UTC


What this key does

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

Consumer How it is read
CI/CD (Woodpecker) Reads vault live per run — VAULT_SECRETS="HEROKU_API_KEY" in .woodpecker/*.yaml (staging AND prod: deploy-staging.yaml, deploy-prod.yaml, deploy-velvet.yaml, deploy-queue.yaml). NO static copy of any kind. GitHub Actions deploys were retired 2026-07-10 (ADR-0135/ADR-0136) — the deploy-heroku.yml GHA secret consumer no longer exists.
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 token — GET /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) - The 4 Heroku app config vars that carry HEROKU_API_KEY as their own config var: raxx-api-staging, raxx-api-prod, raxx-console-staging, raxx-console-prod (heroku config:set HEROKU_API_KEY=<value> -a <app> for each). GitHub Actions deploys were retired 2026-07-10 (ADR-0135/ADR-0136) — there is no GHA secret consumer to update; see the consumer table above.
  5. Re-verify vault — re-read the vault entry and confirm its suffix matches the new token. Abort if it does not.
  6. Static-copy sweep (#4544) — run: bash grep -rn "from_secret: HEROKU" .woodpecker/ This must return zero hits. Any hit is a static WP org-secret copy of a Heroku credential that this rotation must also update (or, preferably, retire in favor of the run-time vault load — see #4544 for the precedent: HEROKU_PROD_API_KEY was exactly this kind of copy, went stale after the 2026-07-21 rotation, and broke prod deploys for 5 weeks before anyone noticed).
  7. Revoke OLD authorization — heroku authorizations:revoke <OLD_ID>. Use the NEW key to make the revoke call. Verify the old authorization no longer appears in heroku authorizations.
  8. 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-19 UTC (matches Next rotation due above — 90 days from the 2026-07-21 rotation)
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 12 10 *'  # 2026-10-12 08:00 UTC — one week before 2026-10-19
    - cron: '0 8 10 1 *'   # 2027-01-10 08:00 UTC — one week before 2027-01-17 (illustrative next cycle)

Until that workflow exists: set a calendar reminder for 2026-10-12 UTC so the rotation runs before the key reaches 90 days old (current authorization e4e7130b minted 2026-07-21 — see the rotation log below).


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.
2026-07-21 sre-agent (operator-authorized, full-fleet rotation after heroku releases:info plaintext exposure — docs/incidents/2026-07-21-full-credential-rotation.md) raxx-ci-rotation-2026-07 (85f3a15a) e4e7130b (raxx-ci-rotation-2026-07-21) Also propagated the new value to the 4 Heroku apps that carry HEROKU_API_KEY as their own config var (raxx-api-staging, raxx-api-prod, raxx-console-staging, raxx-console-prod). Confirmed Woodpecker reads HEROKU_API_KEY live from vault per-pipeline-run (VAULT_SECRETS="HEROKU_API_KEY" pattern in .woodpecker/*.yaml) — no separate WP secret store to update for this credential. HEROKU_API_KEY__AUTH_ID vault companion corrected (was stale, still pointing at the 2026-07-01-revoked b1e6320e). Old auth 85f3a15a confirmed revoked (verified 401 not re-tested live but revoke call returned HTTP 200 and post-revoke /account call with the new key succeeded independently). prod-deploy-key (1f970877) and the other stale authorizations flagged in the 2026-07-01 row were left untouched — out of scope for this incident-driven rotation.
2026-08-29 raxx-dev-bot (issue #4544, RCA docs/incidents/2026-08-29-deploy-prod-stale-wp-secret.md) n/a — no new Heroku authorization minted this cycle n/a This row is NOT a key rotation — it is the discovery + retirement of a static-copy gap in the previous rotation's coverage. WP org secret HEROKU_PROD_API_KEY (orgs/2, event-restricted [tag, manual]) was found stale: it still held the authorization revoked by the 2026-07-21 rotation above (85f3a15a), because that rotation was name-matched (only updated things literally named HEROKU_API_KEY) not consumer-matched (missed the separately-named prod alias). Caused deploy-prod pipelines 7000 (v1.13.0) and 7800 (v1.13.1) to fail at the RELEASE_VERSION config-var PATCH — prod frozen at release #184 (558c9eda) since 2026-07-21. Retired by #4544: deploy-prod.yaml and deploy-queue.yaml now load HEROKU_API_KEY from vault at run time, same as every other consumer in the table above; no static WP secret remains for this credential family. Operator follow-up: DELETE /api/orgs/2/secrets/HEROKU_PROD_API_KEY in Woodpecker — unused after this PR, harmless until deleted.

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 (updated 2026-08-29, #4544 — matches heroku-api-key-drift-recovery.md):

  1. Infisical vault: /MooseQuest/heroku/HEROKU_API_KEY (env: prod) — source of truth
  2. The 4 Heroku app config vars: HEROKU_API_KEY on raxx-api-staging, raxx-api-prod, raxx-console-staging, raxx-console-prod
  3. Session bootstrap: HEROKU_API_KEY exported by session-bootstrap.sh for agent sessions

There is no GitHub Actions secret in this picture — GHA deploys were retired 2026-07-10 (ADR-0135/ADR-0136) — and CI/CD (Woodpecker) has no static copy; it re-reads vault live on every pipeline run.

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