Raxx · internal docs

internal · gated ↑ index

Rotation SOP — Heroku Platform API Token

Mode: operator-assisted Last validated: 2026-04-24 UTC Validation method: read-only-docs (dry-run-on-prod-with-rollback feasible — see below) Average duration: 8m Required role: superadmin

Applies to: HEROKU_API_KEY, HEROKU_PLATFORM_API_TOKEN, and any other long-lived Heroku Platform API authorization tokens used by the console, the rotation pipeline, and CI workflows.

Earlier verbal summary said "manual." Confirmed: there is no programmatic self-rotation API. Heroku exposes heroku authorizations:create (CLI) and POST/DELETE /oauth/authorizations (REST) — but creating a new authorization requires a user-level OAuth token or the operator's authenticated CLI session, which itself cannot be rotated programmatically without operator presence. We classify this as operator-assisted (operator runs CLI commands) rather than fully manual (dashboard-only).

When to run

Prerequisites

Steps

1. Pre-rotation checks

# List existing authorizations
heroku authorizations
# Look for the description matching the token being rotated (e.g., "raxx-api-prod-platform-token")
# Capture the OLD authorization ID for revocation in step 7

2. Generate the new credential

# Create a new long-lived authorization
heroku authorizations:create \
  --description "raxx-platform-token-2026-04-24" \
  --scope "global"

Output includes the new token value (Token) and the authorization ID. Capture the token value now — it is shown once.

For a more restricted scope (preferred if the consumer doesn't need global access):

heroku authorizations:create \
  --description "raxx-platform-token-2026-04-24-readonly" \
  --scope "read-protected,write-protected"

REST equivalent (if CLI unavailable):

curl -sS -n -X POST \
  -H "Accept: application/vnd.heroku+json; version=3" \
  -H "Content-Type: application/json" \
  https://api.heroku.com/oauth/authorizations \
  --data '{"description":"raxx-platform-token-2026-04-24","scope":["global"]}'

3. Validate the new credential

NEW_TOKEN="<captured value>"
curl -sS -H "Accept: application/vnd.heroku+json; version=3" \
  -H "Authorization: Bearer $NEW_TOKEN" \
  https://api.heroku.com/account | jq '.email'
# Expect: kris@moosequest.net (or the Heroku account email)

Confirm the token can perform the actions consumers need:

# Read app config (used by rotation pipeline + console)
curl -sS -H "Accept: application/vnd.heroku+json; version=3" \
  -H "Authorization: Bearer $NEW_TOKEN" \
  https://api.heroku.com/apps/raxx-api-prod/config-vars | jq 'keys | length'
# Expect: a non-zero count.

4. Store in Infisical

infisical secrets set HEROKU_API_KEY="$NEW_TOKEN" \
  --projectId="$INFISICAL_PROJECT_ID" --env=prod

5. Propagate to downstream consumers

Consumer How
Raptor (raxx-api-prod, raxx-api-staging) heroku config:set HEROKU_API_KEY="$NEW_TOKEN" -a raxx-api-prod (and -staging)
Console (raxx-console-prod) heroku config:set HEROKU_API_KEY="$NEW_TOKEN" -a raxx-console-prod
GitHub Actions (deploy workflows) gh secret set HEROKU_API_KEY -a actions -b "$NEW_TOKEN"
Rotation pipeline per #253 deploy spec — likely a Heroku config var on the pipeline app
Operator local zshrc DM via Slack D0AJ7K184TV after Infisical sync

6. Verify downstream

After Heroku propagation completes (each config:set triggers a dyno restart):

# Confirm Raptor is up and using the new token (health endpoint hits Heroku Platform API on a /health/dependencies route if present)
curl -sS https://api.raxx.app/health | jq '.heroku_api'
# OR a deploy-via-token smoke test on staging
gh workflow run deploy-staging.yml -f sanity=true
gh run watch
# Expect: success

7. Revoke the old credential

heroku authorizations:revoke <OLD_AUTHORIZATION_ID>

REST equivalent:

curl -sS -X DELETE \
  -H "Accept: application/vnd.heroku+json; version=3" \
  -H "Authorization: Bearer $ROTATING_TOKEN" \
  https://api.heroku.com/oauth/authorizations/$OLD_AUTHORIZATION_ID

Verify:

curl -sS -H "Accept: application/vnd.heroku+json; version=3" \
  -H "Authorization: Bearer $OLD_TOKEN" \
  https://api.heroku.com/account
# Expect: HTTP 401

8. Audit log entry

action: secret.rotate.completed
actor: <admin_id>
context: {"secret_name": "HEROKU_API_KEY", "old_authorization_id": "<id>", "new_authorization_id": "<id>", "method": "operator-assisted"}

Rollback

Heroku permits multiple active authorizations. Until step 7 completes, the old authorization is still valid:

  1. Revert the Heroku config vars to the old token (Infisical retains the previous version).
  2. Restart dynos (heroku ps:restart -a raxx-api-prod) to pick up the reverted token.
  3. Skip step 7 for now; investigate why the new token is broken; then redo from step 2 (creating a fresh new authorization, not reusing the broken one).

After step 7 (revocation), the old token cannot be restored — Heroku does not retain revoked authorizations.

Vendor doc references

Known gotchas