Rotation SOP — GitHub App Installation Access Token (informational)
Mode: programmatic / auto-rotated Last validated: 2026-04-24 UTC Validation method: read-only-docs Average duration: N/A (fully automated) Required role: ops (only for inspection — no operator action required for routine rotation)
Applies to: GitHub App installation access tokens. These are short-lived bearer tokens minted from a GitHub App's private key (the private key is what is stored long-term). If we use a GitHub App for any Raxx automation (e.g., a future "Raxx Bot" GitHub App), the installation token is auto-rotated; the private key is what we actually rotate manually.
This SOP is mostly informational. Installation access tokens have a lifetime of 1 hour and are auto-regenerated by SDKs (Octokit, etc.) before expiry. Routine "rotation" is a non-event for the operator. The thing that actually requires manual rotation is the GitHub App private key, which is covered separately.
When to run
This SOP is only invoked in three scenarios: 1. Diagnosing why an installation token isn't being minted (treat as an incident, not a rotation). 2. Forcing all currently-issued installation tokens to expire ahead of schedule (e.g., post-compromise — by rotating the App's private key, all subsequently-minted installation tokens are signed with the new key; tokens already issued continue to work for up to 1 hour). 3. Onboarding a new GitHub App installation (the first installation token is minted as part of install flow; no rotation involved).
Prerequisites
- [ ] The GitHub App's
app_idknown - [ ] The GitHub App's private key stored in Infisical as
GITHUB_APP_PRIVATE_KEY(PEM format) - [ ] The installation ID for the org/account known
- [ ] GitHub App permissions known and reviewed
How installation token minting works (reference)
# Pseudocode — Octokit and similar SDKs do this for you
import jwt, time, requests
def mint_installation_token(app_id, private_key, installation_id):
now = int(time.time())
payload = {"iat": now, "exp": now + 600, "iss": app_id}
bearer = jwt.encode(payload, private_key, algorithm="RS256")
resp = requests.post(
f"https://api.github.com/app/installations/{installation_id}/access_tokens",
headers={
"Authorization": f"Bearer {bearer}",
"Accept": "application/vnd.github+json"
}
)
return resp.json() # {"token": "ghs_...", "expires_at": "..."}
The returned token is valid for 1 hour. SDKs auto-mint a new one as needed.
"Rotation" steps (forced re-issuance scenario)
1. Pre-rotation check
- Confirm the App is healthy via the App settings page:
https://github.com/organizations/<org>/settings/apps/<app-name> - Confirm the existing private key fingerprint is stored in Infisical alongside the PEM.
2. Generate a new private key
- Settings → Apps → your App → Generate a private key.
- Download the new PEM file.
- There is no "key revocation" UI for App private keys. Instead, after generating a new key, delete the old key from the App's Private keys section.
3. Validate the new private key
# Mint a token using the new key
python -c "
import jwt, time, sys
key = open('/path/to/new-private-key.pem').read()
now = int(time.time())
payload = {'iat': now, 'exp': now + 600, 'iss': $APP_ID}
print(jwt.encode(payload, key, algorithm='RS256'))
" | xargs -I {} curl -sS -X POST \
-H "Authorization: Bearer {}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/app/installations/$INSTALLATION_ID/access_tokens" | jq '.token'
# Expect: a `ghs_...` token string
4. Store in Infisical
infisical secrets set GITHUB_APP_PRIVATE_KEY="$(cat /path/to/new-private-key.pem)" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
5. Propagate
| Consumer | How |
|---|---|
| Apps that mint installation tokens | heroku config:set GITHUB_APP_PRIVATE_KEY="$(cat new-key.pem)" -a <app> |
6. Verify
After consumer restart, watch logs for successful installation token mints:
heroku logs --tail -a <app> | grep -iE 'github.*installation|access_tokens'
# Expect: 200 responses from /app/installations/.../access_tokens
7. Revoke the old private key
- App settings → Private keys → old key → Delete.
- Confirm.
After deletion, no new installation tokens can be minted with the old key, but installation tokens already minted with the old key remain valid for up to 1 hour. There is no path to invalidate already-minted installation tokens directly — they age out naturally. For a compromise scenario where 1 hour is unacceptable, also rotate the consumers of the old installation tokens (e.g., revoke the GitHub App's installation entirely and reinstall) — that is a heavier path.
8. Audit log entry
action: secret.rotate.completed
actor: <admin_id>
context: {
"secret_name": "GITHUB_APP_PRIVATE_KEY",
"app_id": "<id>",
"installation_id": "<id>",
"method": "operator-assisted-app-settings"
}
Rollback
GitHub Apps allow multiple private keys simultaneously. Until step 7, both old and new private keys can mint installation tokens. To roll back: 1. Revert consumers to the OLD private key from Infisical history. 2. Restart consumers. 3. Skip step 7.
After step 7 (delete old key), the old key is unrecoverable. Generate a fresh new private key.
Vendor doc references
- Generating installation access tokens: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
- Managing private keys for GitHub Apps: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps
- Octokit auth-app (auto-rotation): https://github.com/octokit/auth-app.js
Known gotchas
- Installation access tokens are not what you rotate. The private key is. The token is auto-minted with a 1-hour lifetime.
- Old private key deletion is irreversible. Verify the new key works before deleting.
- In-flight installation tokens minted with the old key are valid for up to 1 hour after the key is deleted. This is by design; we accept it as part of the trust model. For compromise scenarios where the residual window is unacceptable, uninstall + reinstall the App.
- Maximum of 25 private keys per App — plenty of room for routine overlap, but clean up unused old keys to keep the panel tidy.
- Octokit SDKs handle minting. If we use Octokit (or
@octokit/auth-app), the consumer code rarely sees raw installation tokens — it sees the SDK's lazy-minted current token. After private key rotation, consumers may need to re-instantiate the auth instance, whichheroku ps:restarthandles automatically. - As of 2026-04-24, Raxx does not yet have a GitHub App installation set up. This SOP becomes operationally relevant when one is provisioned (a follow-up question for the operator).