Raxx · internal docs

internal · gated ↑ index

Rotation SOP — GitHub Personal Access Token (PAT)

Mode: operator-assisted (scripted propagation available — see Step 5) Last validated: 2026-05-05 UTC Validation method: read-only-docs Average duration: 5m (scripted) / 7m (manual) Required role: ops

Applies to: GITHUB_API_DISPATCH_TOKEN, GITHUB_API_READONLY_TOKEN, and any other classic or fine-grained PATs used by Raxx infrastructure for GitHub API access (CI workflows, console build status integration, repo automation).

Bridge script available. Steps 5–6 can be run in one command once the new token is stored in vault. See Step 5 for usage. This script is an interim bridge until Velvet's GitHub adapter (#947) absorbs the responsibility natively.

Recommendation: migrate to fine-grained PATs. GitHub recommends fine-grained tokens over classic PATs whenever possible. Fine-grained tokens have better scoping, mandatory expiry, per-repo permissions, and clearer audit trails. New tokens should be fine-grained unless a needed feature is classic-only. Track legacy classic PATs in the routing matrix and migrate on the next rotation cycle.

When to run

Prerequisites

Steps

1. Pre-rotation checks

# Confirm current token works
curl -sS -H "Authorization: Bearer $CURRENT_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/user | jq '.login'
# Expect: the GitHub username (or app/bot login)

For fine-grained tokens, also verify scope:

# List the resources the token can see
curl -sS -H "Authorization: Bearer $CURRENT_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/user/repos | jq '. | length'

2. Generate the new credential

Fine-grained PAT (preferred): 1. Navigate to https://github.com/settings/personal-access-tokens/new. 2. Token name: descriptive (e.g., raxx-console-readonly-2026-04-24). 3. Expiration: select 90 days (or shortest practical for the use case). 4. Resource owner: select MooseQuest org (or personal account). 5. Repository access: select specific repos this token needs (least-privilege). 6. Permissions: select the minimal set: - For GITHUB_API_READONLY_TOKEN: Repository → Actions: Read-only, Repository → Metadata: Read-only, Repository → Contents: Read-only. 7. Click Generate token. 8. Copy the token value immediately — shown once. Format: github_pat_....

Classic PAT (only if fine-grained cannot satisfy the use case): 1. Navigate to https://github.com/settings/tokens/new. 2. Note: descriptive (e.g., raxx-classic-2026-04-24). 3. Expiration: 90 days. 4. Scopes: minimal set (e.g., read:org, repo:status, actions:read). 5. Click Generate token. 6. Copy. Format: ghp_....

3. Validate the new credential

NEW_TOKEN="..."
curl -sS -H "Authorization: Bearer $NEW_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/user | jq '.login'
# Expect: same username/login as step 1

Validate against the consumer's actual workload:

# Example: console reads recent CI runs
curl -sS -H "Authorization: Bearer $NEW_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/raxx-app/TradeMasterAPI/actions/runs?per_page=1" | jq '.workflow_runs[0].status'
# Expect: a run status string ("completed", etc.)

4. Store in Infisical

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

5. Propagate to downstream consumers

Option A — scripted (preferred)

Run the bridge propagation script. It reads the new value from vault, sets it on all Heroku apps and the GitHub Actions secret, then verifies via the GitHub API:

# Required env vars (set before running):
export INFISICAL_CLIENT_ID="..."
export INFISICAL_CLIENT_SECRET="..."
export INFISICAL_PROJECT_ID="..."
export HEROKU_API_KEY="..."
export GITHUB_TOKEN="..."   # a token with secrets:write on raxx-app/TradeMasterAPI

# Propagate GITHUB_API_DISPATCH_TOKEN (defaults: both console apps + repo secret):
scripts/ops/rotate-github-pat.sh GITHUB_API_DISPATCH_TOKEN

# Propagate GITHUB_API_READONLY_TOKEN:
scripts/ops/rotate-github-pat.sh GITHUB_API_READONLY_TOKEN

# Dry-run to preview what would happen without touching anything:
scripts/ops/rotate-github-pat.sh GITHUB_API_DISPATCH_TOKEN --dry-run

Override defaults if needed:

scripts/ops/rotate-github-pat.sh GITHUB_API_DISPATCH_TOKEN \
  --vault-path /MooseQuest/github/ \
  --apps raxx-console-prod,raxx-console-staging \
  --gh-secret-repo raxx-app/TradeMasterAPI

The script prints a SUMMARY with the status of each destination and next-steps for revoking the old token.

Note: This script is a bridge artifact until Velvet's Heroku + GitHub adapters (#947) ship. When #947 merges, this manual step is replaced by the Velvet rotation flow.

Option B — manual (use if the script is broken)

Consumer How
Console (raxx-console-prod) heroku config:set GITHUB_API_DISPATCH_TOKEN="$NEW_TOKEN" -a raxx-console-prod >/dev/null 2>&1
Console (raxx-console-staging) heroku config:set GITHUB_API_DISPATCH_TOKEN="$NEW_TOKEN" -a raxx-console-staging >/dev/null 2>&1
Raptor (if it makes GitHub API calls) heroku config:set GITHUB_API_DISPATCH_TOKEN=... -a raxx-api-prod >/dev/null 2>&1
GitHub Actions (a workflow that calls gh api outside default GITHUB_TOKEN) gh secret set GITHUB_API_DISPATCH_TOKEN -b "$NEW_TOKEN" -R raxx-app/TradeMasterAPI
Operator local zshrc DM via Slack D0AJ7K184TV

Safety: always append >/dev/null 2>&1 to every heroku config:set call. The Heroku CLI echoes the entire config-var payload to stdout on success, exposing the secret value in terminal history and CI logs. See feedback_heroku_config_set_echoes_secrets.md.

6. Verify downstream

After dyno restart:

# Console build-status panel should populate
curl -sS https://console.raxx.app/api/status/builds \
  -H "Cookie: <ops session>" | jq '.builds.api[0].conclusion'
# Expect: a string (not null), no rate-limit errors
# Tail logs for any GitHub API auth errors
heroku logs --tail -a raxx-console-prod | grep -iE 'github|401|403'
# Expect: no auth failures.

7. Revoke the old credential

GitHub does not support "regenerate in place" — you must revoke the old token explicitly:

  1. Navigate to https://github.com/settings/tokens (classic) or https://github.com/settings/personal-access-tokens (fine-grained).
  2. Locate the OLD token.
  3. Click Delete (classic) or Revoke (fine-grained).
  4. Confirm.

Verify:

curl -sS -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer $OLD_TOKEN" \
  https://api.github.com/user
# Expect: 401

8. Audit log entry

action: secret.rotate.completed
actor: <admin_id>
context: {
  "secret_name": "GITHUB_API_READONLY_TOKEN",
  "token_type": "fine-grained" or "classic",
  "method": "operator-assisted-settings"
}

Rollback

Until step 7, both tokens are valid. To roll back: 1. Revert Heroku config vars to the OLD token from Infisical history. 2. Restart consumers. 3. Skip step 7. Investigate.

After step 7 (revoke), the old token is dead. Generate a brand-new fresh token and redo from step 2.

Vendor doc references

Known gotchas