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.
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).
app_id knownGITHUB_APP_PRIVATE_KEY (PEM format)# 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.
https://github.com/organizations/<org>/settings/apps/<app-name># 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
infisical secrets set GITHUB_APP_PRIVATE_KEY="$(cat /path/to/new-private-key.pem)" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
| Consumer | How |
|---|---|
| Apps that mint installation tokens | heroku config:set GITHUB_APP_PRIVATE_KEY="$(cat new-key.pem)" -a <app> |
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
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.
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"
}
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.
@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, which heroku ps:restart handles automatically.