Playbook: GitHub App Installation Token for n8n
GitHub App: n8n.io pull request flow
App ID: 2109526
Installation ID: 142989418 (radieu account, all repos)
n8n sub-workflow: GitHub App Token Generator (ID 6b7m52mpur3tXO1G)
Implemented: 2026-06-27 — replaces PAT-based GitHub auth for new n8n workflows
Why this exists
GitHub Personal Access Tokens (PATs) require browser + 2FA to create or rotate. The GitHub App approach stores an RSA private key in SOPS and generates short-lived installation tokens (1h TTL) on demand — no browser, no 2FA, fully autonomous rotation.
| PAT | GitHub App token | |
|---|---|---|
| Lifetime | 90 days | 1 hour |
| Rotation | Browser + 2FA | API call (autonomous) |
| Scope | Fixed at creation | Per-request scoping possible |
| Secret stored | Token value | RSA private key |
Secrets (in secrets/n8n-bms4.env.sops)
| Key | Description |
|---|---|
GITHUB_APP_ID | 2109526 |
GITHUB_APP_PRIVATE_KEY_B64 | Base64-encoded RSA private key (PEM) |
GITHUB_APP_INSTALLATION_ID | 142989418 — radieu account installation |
These are deployed to bms-4’s n8n container via secrets-sync.yml on merge to dev.
n8n Code nodes read them via process.env.GITHUB_APP_*.
Using the sub-workflow in n8n
Call the GitHub App Token Generator sub-workflow (ID 6b7m52mpur3tXO1G) at the start of any
workflow that needs GitHub API access:
Step 1 — Add “Execute Sub-workflow” node
Node type: Execute Sub-workflow
Sub-workflow: GitHub App Token Generator (6b7m52mpur3tXO1G)
Step 2 — Use the token in HTTP Request nodes
The sub-workflow returns { token: "ghs_...", expires_at: "2026-06-27T20:17:01Z" }.
In subsequent HTTP Request nodes:
URL: https://api.github.com/repos/radieu/p24-infra/issues
Method: POST
Authentication: Header Auth (inline)
Header name: Authorization
Header value: Bearer {{ $('GitHub App Token Generator').item.json.token }}
Add header: X-GitHub-Api-Version: 2022-11-28
Step 3 — Do NOT store the token
Tokens expire in 1 hour. Each workflow execution calls the sub-workflow to get a fresh one. Never store the token value in n8n credentials or environment variables.
Token generation internals (Code node logic)
const crypto = require('crypto');
const https = require('https');
// Reads from bms-4 environment (deployed from SOPS)
const appId = process.env.GITHUB_APP_ID;
const pem = Buffer.from(process.env.GITHUB_APP_PRIVATE_KEY_B64, 'base64').toString('utf8');
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
// Build RS256 JWT (5-minute window)
const now = Math.floor(Date.now() / 1000);
const header = Buffer.from(JSON.stringify({alg:'RS256',typ:'JWT'})).toString('base64url');
const payload = Buffer.from(JSON.stringify({iat:now-10, exp:now+300, iss:appId})).toString('base64url');
const si = `${header}.${payload}`;
const sign = crypto.createSign('RSA-SHA256');
sign.update(si);
const jwt = `${si}.${sign.sign(pem, 'base64url')}`;
// Exchange JWT for 1-hour installation token
// POST /app/installations/{id}/access_tokens → { token: "ghs_...", expires_at: "..." }Rotating the private key
- Go to
https://github.com/settings/apps/2109526/installations→ Edit - Scroll to “Private keys” → Generate a private key (downloads new
.pem) - Save to
D:\downloads\(or any temp location) - Run:
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$pemBytes = [System.IO.File]::ReadAllBytes("D:\downloads\<new-key>.pem")
$env:NEW_KEY_B64 = [Convert]::ToBase64String($pemBytes)
$plain = sops --decrypt --input-type dotenv --output-type dotenv d:\code_2026\p24-infra\secrets\n8n-bms4.env.sops
$updated = ($plain -split "`n" | Where-Object { $_ -notmatch "^GITHUB_APP_PRIVATE_KEY_B64=" -and $_ -ne "" })
$updated += "GITHUB_APP_PRIVATE_KEY_B64=$env:NEW_KEY_B64"
$env:NEW_KEY_B64 = ""
$temp = "d:\code_2026\p24-infra\secrets\n8n-bms4-edit.env.sops"
[System.IO.File]::WriteAllText($temp, ($updated -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
$enc = sops --encrypt --input-type dotenv --output-type dotenv $temp
[System.IO.File]::WriteAllText("d:\code_2026\p24-infra\secrets\n8n-bms4.env.sops", ($enc -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
sops --decrypt --input-type dotenv --output-type dotenv d:\code_2026\p24-infra\secrets\n8n-bms4.env.sops | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt" }
[System.IO.File]::Delete($temp)- Delete the old key from GitHub App settings (can now have multiple keys during rotation)
- Commit + PR to main
secrets-sync.ymlauto-deploys to bms-4 on merge
Migrated workflows (as of 2026-06-27)
The following workflows were migrated from GitHub PAT - p24-infra credential to App tokens:
| Workflow | ID | Migrated |
|---|---|---|
alertmanager-to-incidents-v3 | HGT5EAXGhsxdSSkk | 2026-06-27 |
resource-incident-analysis | DL0Q9CUdn38vkiqx | 2026-06-27 |
Both now have a Get GitHub Token (Execute Sub-workflow) node wired before any GitHub API call.
The GitHub PAT - p24-infra n8n credential (GwnEOfJI3qXM02nb) and the underlying PAT
(CLAUDE_RUNNER_WORKER_GH_TOKEN on github.com/settings/tokens) can now be safely deleted.
Migrating additional n8n workflows from PAT to App tokens
For any future workflow that needs GitHub API access:
- Add “Execute Sub-workflow” node →
GitHub App Token Generator(6b7m52mpur3tXO1G) before the GitHub call - In GitHub HTTP Request nodes — set headers inline (no credential):
Authorization:={{ 'Bearer ' + $('Get GitHub Token').item.json.token }}Accept:application/vnd.github+jsonX-GitHub-Api-Version:2022-11-28
- Do NOT store the token — each execution gets a fresh 1h token
For existing workflows still using GITHUB_PAT_ALL_WRITES (github-token-clasic-full-access):
migrate before revoking that token. The pattern is identical to the two workflows above.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Missing GITHUB_APP env vars | SOPS not deployed to bms-4 | Merge PR to main → secrets-sync.yml runs |
GitHub API 401 | JWT clock skew | The iat: now-10 already accounts for drift; if still failing, restart n8n container |
GitHub API 403 | App lacks permission | Check App permissions at github.com/settings/apps/2109526 |
GitHub API 404 on repo operation | App not installed or repo not covered | Check github.com/settings/installations/142989418 |