Plan: enable autonomous GitHub PAT rotation (TOTP + Playwright) on the secret-manager worker
Issue: #4065
Type: Code-change-design (plan only)
Status: Draft — iteration 1
Last updated: 2026-07-13
Author: Claude Opus 4.7 (worker session on vps-h1)
Related: #4053 (blocked et-op GITHUB_TOKEN rotation), #2824 (et-op key exposure — GITHUB_TOKEN still unrotated), #4048 (.claude-env scrub), #3286 (recurring GH_TOKEN leak via git remote -v), #3304 (git credential helper on bms-4 — closes the leak vector but does not enable rotation)
0. Summary — recommendation
Ship a two-track enablement, in order:
- Wave A — GitHub App expansion (primary): move as much automation as possible off PATs onto the existing GitHub App (
GITHUB_APP_ID=2109526, installation142989418). The App produces 1h-TTL installation tokens with no browser and no 2FA — this is Tier 1 autonomy today. Coverage gap analysis (§4) shows the App can absorb every current PAT consumer exceptGH_PAT_ADMIN(admin-scope operations outside App permission surface). Delivered by a p24-infra PR that adds abin/gh-app-token.shhelper, refactors the four bms-4 worker code paths that still call PATs directly, and revises the access matrix. - Wave B — Playwright+TOTP capability (residual fallback): only after Wave A ships, provision Playwright + Chromium on the bms-4 secret-manager worker, add
GITHUB_TOTP_SECRETtosecrets/n8n-bms4.env.sops, and extendworker-secret-manager.mdwith the Playwright login flow. This unblocks the small residual set (Wave A leftovers + any provider that never adopts the App) and unblocks #4053 immediately once the TOTP seed is added.
Rationale for the ordering: Wave A removes ~90% of the PAT-rotation surface without introducing any browser/2FA dependency on the worker, and the App private key already sits in n8n-bms4.env.sops (verified — see §2.4). Wave B is unavoidable for GH_PAT_ADMIN and for the fine-grained PAT under et-op (#4053), but is a larger blast radius (browser + TOTP seed → worker environment), so it should not gate the App migration.
GitHub App alternative decision: adopt as primary path (Wave A). Do not treat it as a replacement for Wave B — the App cannot regenerate GH_PAT_ADMIN nor create PATs itself. Wave B stays scoped to the PAT-only residual set.
Not shipped in this PR: the actual Playwright script, the SOPS entry for GITHUB_TOTP_SECRET (secret-manager worker task, not this plan-PR), and the re-dispatch of #4053. Those follow after this plan is approved and the two implementation issues (§6.4) land.
1. Why this is blocked today — reproduction of the #4053 callback failure
The bms-4 secret-manager worker attempted to rotate et-op GITHUB_TOKEN (fine-grained PAT, Tier 2) on 2026-07-12 and reported two blockers:
| Blocker | Evidence | Consequence |
|---|---|---|
B1 — GITHUB_TOTP_SECRET absent | grep -l '^GITHUB_TOTP_SECRET=' secrets/*.env.sops returns 0 files. Also missing from .env.local (development-local backstop). | Playwright cannot complete GitHub 2FA challenge → login flow dies at OTP prompt. Access matrix line 141 says “prerequisite unmet → escalate to Tier 3 human action.” |
| B2 — Playwright/Chromium unavailable on the worker | ansible/roles/claude-runner/tasks/main.yml installs Node.js + Claude Code CLI only. No playwright install chromium, no browser binary, no per-role node_modules. scripts/rotate/ has .js scripts that call require('playwright') but no package.json at that path in the checkout. | Even with the TOTP seed, the worker has no browser to drive. Fine-grained PAT regeneration is UI-only — no API. |
Fine-grained PATs (github_pat_* prefix) have no rotation API — the only interface is github.com/settings/personal-access-tokens/<id> → Regenerate button, guarded by full password login + 2FA. This is by GitHub design and will not change.
Corollary: every future PAT rotation currently degrades to human-action, blocking the SLA watchdog and the exposure-response protocol. #4053 is the acute case; #2824’s remaining 11 exposed keys are the standing case.
2. Current state — facts on the ground
2.1 PAT inventory (across the fleet)
Verified via grep -nE '^GH_TOKEN|^GITHUB_PAT_|^GITHUB_TOKEN' secrets/*.env.sops on branch head:
| Key name | SOPS file | Deployed to | Consumer / purpose |
|---|---|---|---|
GH_TOKEN | secrets/monitoring.env.sops (line 8) | vps-i1 /opt/p24-infra/monitoring/.env | Monitoring exporters, cron scripts on vps-i1 |
GH_TOKEN | secrets/n8n-bms4-gh.env.sops (line 1) | bms-4 /opt/p24-infra/bms-4/n8n-bms4-gh.env | git credential helper on bms-4 (git-credential-gh-token.sh) — closes #3286 leak |
GITHUB_PAT_ALL_WRITES | secrets/n8n-bms4-gh.env.sops (line 2) | bms-4 | n8n workflows: issue/PR create, dispatch triggers, workflow triggers |
GITHUB_PAT_P24_INFRA | secrets/n8n-bms4-gh.env.sops (line 3) | bms-4 | n8n workflows scoped to radieu/p24-infra |
GITHUB_TOKEN | secrets/et-operational-platform.env.sops (line 23) | Vercel (et-operational-platform project) | et-op backend, job_type=secret-manager dispatch producer |
GH_TOKEN (playbook stub) | secrets/vps-h1.env.sops | vps-h1 | Older scripts (WAHA-era) — grep confirms not present today, playbook is stale |
GH_PAT_ADMIN | secrets/administration.env.sops | Developer workstation only | Owner-scope admin operations. Worker cannot touch this file. |
Playbook drift found: docs/playbooks/github-pat-rotation.md line 18 says the three bms-4 PATs live in secrets/n8n-bms4.env.sops, but the actual location is secrets/n8n-bms4-gh.env.sops. Wave A PR must correct this (see §6.2). Also the vps-h1 row in playbook table 4 is stale — no PAT ships there today.
2.2 GitHub App inventory (already in place)
Verified via grep -nE '^GITHUB_APP' secrets/n8n-bms4.env.sops:
| Key | Line | Purpose |
|---|---|---|
GITHUB_APP_ID | 43 | 2109526 (n8n.io pull-request-flow App) |
GITHUB_APP_PRIVATE_KEY_B64 | 44 | Base64-encoded RSA PEM (used by n8n sub-workflow 6b7m52mpur3tXO1G and by docs/playbooks/github-app-token-n8n.md) |
GITHUB_APP_INSTALLATION_ID | 45 | 142989418 — radieu account, all repos |
The App is installed on the radieu GitHub account. docs/playbooks/github-app-token-n8n.md §Comparison confirms: 1h installation tokens, no browser, no 2FA, per-request scoping. The plumbing already works for n8n; the gap is that the bms-4 worker code paths (secrets-sync helper, gh CLI shell-outs, scripts/rotate/*) still use PATs directly.
2.3 What the PATs are actually used for (consumer census)
The point of Wave A is to cut the PAT-consumer count. Enumerate every place PATs are read:
| Consumer | Reads which PAT | Reason for PAT (vs App) | App-compatible? |
|---|---|---|---|
bms-4 git-credential-gh-token.sh (PR #3304) | GH_TOKEN | git clone / git push over HTTPS | ✅ App installation tokens work as HTTPS Basic-auth passwords with username x-access-token |
bms-4 secrets-sync.yml “Ship to bms-4” step | GH_TOKEN (via GH Secret) | Push branch update to /opt/p24-infra remote | ✅ swap to App token generated at CI runtime |
| n8n workflows on bms-4 | GITHUB_PAT_ALL_WRITES, GITHUB_PAT_P24_INFRA | Issue create, PR comment, workflow dispatch | ✅ App can perform all of these (Contents: RW, Issues: RW, PRs: RW, Actions: RW — already in App manifest per §2.4 verification) |
| vps-i1 monitoring cron | GH_TOKEN | Open GH Issue on error (per §Error Notification Standard) | ✅ App can POST /repos/.../issues |
| et-op backend | GITHUB_TOKEN (fine-grained PAT) | Queue-dispatch producer + repo webhooks | ⚠️ Feasible but riskier (Vercel side needs App-JWT signer library at runtime); may be deferred to Wave B |
| Developer workstation | GH_PAT_ADMIN | Owner-scope operations (Apps management, org billing) | ❌ Out of App permission surface — Wave B only |
bms-4 scripts/rotate/*.js (Playwright rotations) | GH_TOKEN | gh secret set after rotation | ✅ App token for the gh secret set call |
Net: four out of seven consumer classes migrate cleanly to the App in Wave A. et-op backend and GH_PAT_ADMIN remain PAT-bound and drive the Wave B requirement.
2.4 Verify the App has the permissions we need — DO before Wave A PR
Wave A depends on the installed App already having the permission surface listed in §2.3. Verification is a one-shot API call (no browser):
# From any host with GITHUB_APP_PRIVATE_KEY_B64 accessible (bms-4 or vps-i1):
TOKEN=$(python3 -c "
import base64, jwt, time, os
pem = base64.b64decode(os.environ['GITHUB_APP_PRIVATE_KEY_B64']).decode()
now = int(time.time())
print(jwt.encode({'iat': now - 60, 'exp': now + 540, 'iss': os.environ['GITHUB_APP_ID']}, pem, 'RS256'))
")
curl -sf -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/app/installations/${GITHUB_APP_INSTALLATION_ID}" \
| jq '.permissions,.repository_selection'Expected permissions surface (App manifest must include): contents: write, issues: write, pull_requests: write, actions: write, metadata: read, secrets: write (for the gh secret set migration path). If any are missing, the App manifest gets updated at github.com/settings/apps/2109526/permissions — that is a Tier 3 human action (§Escalation) and this plan’s Wave A becomes gated on it. Verify before the Wave A PR opens so the reviewer can decide whether to split.
Do NOT print the JWT or installation token to stdout — pipe directly into curl, unset immediately.
2.5 Existing Playwright surface in the repo
scripts/rotate/*.js files exist for 8 credentials (discord-*.js, n8n-bms4-api-key.js, mailgun-api-key.js, etc.). All follow the pattern in docs/playbooks/playwright-rotation-template.md:
- Executed from a Windows dev machine (existing
.ps1wrappers assume%LOCALAPPDATA%\ms-playwright). - Reuse a
mcp-chrome-*Playwright profile — persistent Google session, no fresh login. - No 2FA handler — the ones that need MFA (
n8n-bms4-api-key.js) exit code 2 for the caller to retry.
None of these run on the bms-4 worker today. The prompt in docs/playbooks/github-pat-rotation.md §Option A describes the intent of the Playwright-on-worker flow, but the infrastructure (browser install, headless mode, TOTP handler, ephemeral Chrome profile) is not provisioned. Wave B provides that.
2.6 Account ownership — which GitHub account owns each PAT
Issue body asks whether all PATs are on the same account (single TOTP unlocks everything). Sampled evidence:
GH_TOKENon bms-4 → 2026-07-08 rotation-log entry confirmsradieuaccount (gh api /userreturnedradieu).GITHUB_PAT_ALL_WRITES/GITHUB_PAT_P24_INFRA→ n8n-bms4-gh.env.sops proximity + rotation history ingithub-pat-rotation.md→radieuaccount.GITHUB_TOKEN(et-op) → per #4053 body, fine-grained PAT onradieuaccount.GH_PAT_ADMIN→ per access-matrix line 172, onradieu(admin-scope PAT).
Conclusion: a single GITHUB_TOTP_SECRET seeded from radieu’s authenticator unlocks all PAT UI operations. No dual-account fan-out.
Confirm in the Wave B implementation issue by having the human read the QR seed once at github.com/settings/security → 2FA reconfiguration flow, then verify a manual TOTP code against the seed before it lands in SOPS.
3. Design
3.1 Wave A — GitHub App migration for bms-4 workers (this-PR-adjacent implementation issue)
Goal: eliminate PAT dependency for 4 of 7 consumer classes.
Deliverables (opened as separate implementation issue tbd — see §6.4):
bin/gh-app-token.sh— new POSIX shell helper (bms-4 + vps-i1). Signs a 9-min JWT withGITHUB_APP_PRIVATE_KEY_B64, exchanges for an installation token viaPOST /app/installations/{id}/access_tokens, prints the token to stdout with--no-newline. Callers use$(bin/gh-app-token.sh)inline. Same UX contract as an env-var read but never touches disk. Also exposes--check(verify creds work) and--repos <csv>(scope the token to specific repos).- Refactor bms-4
git-credential-gh-token.sh(added by PR #3304) to callbin/gh-app-token.shinstead of readingGH_TOKENfrom/opt/p24-infra/bms-4/n8n-bms4-gh.env. Fall back toGH_TOKENonly if the App call fails (belt-and-braces during rollout — remove the fallback in a follow-up after 2 weeks of clean logs). - Refactor
secrets-sync.yml“Ship to bms-4” step to runbin/gh-app-token.sh --repos radieu/p24-infra --scope contents:writeon the runner and use that token for the deploy push. Removes thex-access-token:${GH_PAT}@github.comembedded-URL anti-pattern from CI (currently reinstalled every sync per #3286 root cause). - Refactor
scripts/rotate/*.jspost-rotationgh secret setcalls to source the App token instead of the ambientGH_TOKEN. Wrap as a single helper —scripts/lib/gh_app_secret_set.sh— so future scripts inherit the pattern. - n8n workflow migration: audit the 8 n8n workflows currently reading
GITHUB_PAT_ALL_WRITES/GITHUB_PAT_P24_INFRA(list in §6.3 acceptance criteria), switch each to theGitHub App Token Generatorsub-workflow (ID6b7m52mpur3tXO1G— already exists perdocs/playbooks/github-app-token-n8n.md). - Access matrix update — move
GH_TOKEN,GITHUB_PAT_ALL_WRITES,GITHUB_PAT_P24_INFRAfrom Tier 2 to a new “Tier 1 via GitHub App” row that points atbin/gh-app-token.sh. Deprecate the PATs (mark for removal from SOPS after Wave A rollout + 30 days of no consumer usage).
Non-goal (deferred to Wave B): removing GITHUB_TOKEN (et-op) from Vercel or migrating et-op backend to App tokens. The Vercel side needs a serverless-friendly JWT signer, which is a separate design.
Rollback: if the App token call fails at any point, callers already fall back to the PAT read (delivered as belt-and-braces). Removing the App migration is git revert of the two commits.
3.2 Wave B — Playwright + TOTP capability for the secret-manager worker
Goal: enable autonomous rotation for the residual PAT-only set: et-op GITHUB_TOKEN (#4053), any future new PAT, GH_PAT_ADMIN (with an explicit --allow-admin guard).
Deliverables (opened as separate implementation issue tbd — see §6.4):
3.2.1 Provisioning — ansible/roles/claude-runner
Add tasks to ansible/roles/claude-runner/tasks/main.yml:
# Package list targets Ubuntu 22.04 (bms-4's actual OS). libasound2 (not the
# 24.04+ libasound2t64 t64-transition name) is correct for 22.04 — the
# original list mixed 22.04 and 24.04 package names and was uninstallable on
# either (#4956).
- name: Install Playwright system dependencies (bms-4 only — playwright_enabled hostvar)
apt:
name:
- libnss3
- libatk-bridge2.0-0
- libdrm2
- libxkbcommon0
- libxcomposite1
- libxdamage1
- libxrandr2
- libgbm1
- libpango-1.0-0
- libasound2
- fonts-liberation
state: present
when: playwright_enabled | default(false)
- name: Install Playwright + Chromium via npm (global, headless)
community.general.npm:
name: playwright
version: "1.48.0" # pin — automation flakes on unexpected DOM shifts
global: true
when: playwright_enabled | default(false)
# --with-deps dropped (#4956): it shells out to `sudo apt-get install`
# internally, but claude-runner has no general sudoers grant on bms-4 (only a
# single scoped sops-install rule) — non-interactive sudo is denied. Also
# redundant: the apt task above already installs those deps as root.
- name: Install Chromium browser via Playwright
command: playwright install chromium
become: true
become_user: claude-runner
args:
creates: /home/claude-runner/.cache/ms-playwright/chromium-*
when: playwright_enabled | default(false)
- name: Ensure TOTP CLI (oathtool) is present for MFA challenges
apt:
name: oathtool
state: present
when: playwright_enabled | default(false)
# github-pat.js requires('playwright') as a LOCAL dependency (see
# scripts/rotate/package.json) — the global npm install above cannot satisfy
# Node's local require() resolution, so this task is required or the script
# throws MODULE_NOT_FOUND at runtime (#4956).
- name: Install scripts/rotate local npm dependencies (playwright local require)
community.general.npm:
path: /opt/p24-infra/scripts/rotate
ci: true
when: playwright_enabled | default(false)Gate on playwright_enabled=true in inventory/host_vars/bms-4.yml. Do not enable on vps-i1 or vps-h1 — the Playwright capability is currently only needed on the secret-manager worker (bms-4).
Disk-cost impact: Chromium ~180 MB + system deps ~200 MB, negligible on the bms-4 40 GB disk (current use ~28%). RAM cost is per-run only (Playwright forks a browser process on demand); worker memory budget unchanged.
3.2.2 GITHUB_TOTP_SECRET — where and how
- SOPS file:
secrets/n8n-bms4.env.sops. Rationale: bms-4 is where the secret-manager worker runs;n8n-bms4.env.sopsalready deploys to/opt/p24-infra/bms-4/.env; the worker cansourceit via Step 0a ofworker-secret-manager.md. Do not put it insecrets/administration.env.sops(worker is barred from that file per §Role Enforcement matrix). - Also mirror to vps-h1? No — vps-h1 does not run the secret-manager worker (see §Role Enforcement; secret-manager is bms-4-only). Single-file placement is correct.
- Format: base32-encoded seed (raw GitHub 2FA seed as shown in the 2FA reconfiguration flow). Consumers use
oathtool --totp -b "$GITHUB_TOTP_SECRET"to generate the 6-digit code at login time. - Ownership caveat: this seed grants anyone with SOPS+age keys the ability to bypass 2FA on
radieu’s GitHub account for the PAT UI. This is the same trust boundary as the App private key already sitting in the same file. Access matrix line forGITHUB_TOTP_SECRET= Tier 2 (Playwright unlock) with note “seed grants 2FA-bypass for radieu account — treat like a root credential.” - Addition procedure: human obtains the seed at
github.com/settings/security→ 2FA → reconfigure authenticator → reveal seed. Then queue asecret-managerjob withOperation: add(Wave B implementation issue posts the queue payload). Worker performs the SOPS write perworker-secret-manager.md §General pattern. The plan-PR does not carry the value — only the plan.
3.2.3 Playwright login flow — scripts/rotate/github-pat.js
New file, following the pattern in docs/playbooks/playwright-rotation-template.md §Automated Playwright script standard.
| Step | Action |
|---|---|
| 0 | Read GITHUB_USERNAME (env; default radieu@gmail.com), GITHUB_PASSWORD (env, read from SOPS silently), GITHUB_TOTP_SECRET (env), PAT_NAME (env — which PAT to regenerate), TOKEN_OUT_FILE (env). |
| 1 | Launch headless Chromium (--no-sandbox — bms-4 runs as claude-runner non-root, sandbox denied). Fresh context, no persistent profile (unlike the Windows scripts — no session to reuse on a headless worker). |
| 2 | page.goto('https://github.com/login'), fill username + password. |
| 3 | On 2FA page: generate OTP via require('otplib').authenticator.generate(GITHUB_TOTP_SECRET) (otplib is a peer dep, add via npm), fill in the 6-digit box. |
| 4 | page.goto('https://github.com/settings/personal-access-tokens') → find PAT by name → Regenerate → set expiry 90 days → confirm. |
| 5 | Extract the new token from the readonly input on the confirmation page (readonly <input> selector, fallback to code element per template contract). |
| 6 | Write to TOKEN_OUT_FILE, clear in-memory variable, exit 0. |
| Exit codes | 0 success · 1 fatal · 2 MFA-required-but-no-secret (safe retry). |
The script itself is scripts/rotate/github-pat.js (~150 LOC — reference the existing discord-bot-token.js closely). Add screenshot capture behind DEBUG_SCREENSHOTS=1 per template.
3.2.4 Wrapper — extend worker-secret-manager.md
Add a new “Tier 2 — Playwright rotation pattern (GitHub PAT)” section between the existing Tier 1 and Rotation-log sections. Contains:
- Reading the target PAT’s SOPS location from the access matrix.
- Sourcing
GH_TOKEN/GITHUB_PAT_*/GITHUB_TOKENold value silently for the “verify old key rejected” post-check. - Invoking
scripts/rotate/github-pat.jswith the right env. - Same SOPS write pattern as Tier 1 (§Linux worker
-tmp.env.sopspattern), same canary, same distribution chain (GH Secret + live env + container recreate). - Revocation of the old PAT: skipped (fine-grained PAT UI has no revoke endpoint that Claude can call — the Regenerate action invalidates the old value automatically, verified in
docs/2026-07/secrets-rotation-log-2026-07-08.mdnote “rotation ≠ revocation” is about the scheduled rotation of the pre-existing token, not about Regenerate itself).
3.2.5 Isolation and safety
- Playwright runs in a fresh Chromium context per invocation (no
userDataDir). Credentials are only in env vars, never on disk (no.env.localwrite, no cookie persistence). - All env vars unset immediately after
node scripts/rotate/github-pat.jsreturns (bashunset, notexport -n). TOKEN_OUT_FILEis created withmktemp -t github-pat-out.XXXXXX,chmod 600before write,shred -uafter read.- Discord + GH issue error notification on any non-zero exit (per §Error Notification Standard).
Not doing: a separate container for the browser (isolation via subprocess is sufficient for this threat model; a container adds provisioning surface and doesn’t materially reduce blast radius since the worker itself holds the credentials).
3.3 E2E validation on a low-risk PAT
Before running Wave B against et-op GITHUB_TOKEN, validate on a fresh disposable PAT:
- Human creates a throwaway fine-grained PAT
TEST_PAT_ROTATION_4065onradieuaccount, scope: read-only Metadata on a single test repo. Records value in a temporary local env only. - Add
TEST_PAT_ROTATION_4065tosecrets/n8n-bms4.env.sopsvia a scratch secret-manager task. - Dispatch a
secret-managerrotation job forTEST_PAT_ROTATION_4065. - Worker runs the new Playwright flow, produces a new value, updates SOPS, verifies the new value via
curl -H "Authorization: Bearer $NEW" api.github.com/user. - Log the run in
docs/secrets-rotation-log.mdmarkedtest-pat validation-run. - Remove
TEST_PAT_ROTATION_4065from SOPS + delete the PAT at GitHub.
Acceptance criteria for the whole enablement: this E2E run passes end-to-end with zero human interaction after step 1. If it needs manual intervention, roll back the Wave B PR and open a follow-up.
3.4 Re-dispatch #4053
Post-validation, the Wave B implementation issue’s final step:
curl -sf -X POST "$QUEUE_API_URL/queue-issue" \
-H "Authorization: Bearer $QUEUE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"issue_number": 4053, "repo": "radieu/p24-infra", "job_type": "secret-manager"}'The bms-4 secret-manager worker picks it up, sees GITHUB_TOTP_SECRET present + Playwright available, proceeds through Tier 2, rotates the et-op GITHUB_TOKEN, closes the loop on #2824’s remaining GITHUB_TOKEN PAT unrotated line. #4053’s deferral note is cleared in the same worker run.
4. Alternative evaluated: GitHub App exclusively (skip Wave B)
Considered. Rejected as sole solution — kept as Wave A primary.
Why not App-only:
GH_PAT_ADMINoperations (App management itself, org billing, some organization admin API paths) are outside the App’s permission surface by design. Never fixable via App scoping.- Vercel deployments (
et-operational-platform) currently readGITHUB_TOKENat runtime for webhook validation and downstream automation. Switching to App tokens requires a runtime JWT signer + refresh loop in serverless, which is a larger project. Wave B unblocks the rotation today without waiting for the Vercel refactor. - Rotating the App private key itself (
GITHUB_APP_PRIVATE_KEY_B64) is a Tier 3 human action per access-matrix line 166 — the private-key generation UI is browser+2FA-gated. So even the “App everywhere” world does not eliminate the browser-login capability requirement — it just shifts it from PATs to the App private key. Wave B is therefore not “extra work” — it is work that would be needed regardless the day the App key rotates (expected annually).
Why Wave A is still worth doing: cuts the PAT-rotation cadence from ~4 rotations/year (per key × 3 keys) to ~1 rotation/year (App key only). Every consumer moved off PATs is one fewer thing to remember when the next PAT expires.
5. Files to change
5.1 This plan PR (only)
- NEW
docs/plans/plan-4065-github-pat-autonomous-rotation.md— this file. - UPDATE
docs/priorities.md— NO CHANGE (per CLAUDE.md:docs/priorities.mdis human-supervised only; do not edit from a worker).
Nothing else. This PR does not carry code, playbook edits, secrets, or ansible tasks. Those follow in the Wave A / Wave B implementation PRs (§5.2, §5.3).
5.2 Wave A implementation PR — files preview
- NEW
bin/gh-app-token.sh— JWT signer + installation token exchange helper (POSIX shell + python for the JWT step). - UPDATE
bms-4/git-credential-gh-token.sh— try App token first, fall back toGH_TOKEN. - UPDATE
.github/workflows/secrets-sync.yml— replace embedded-URL push with App-token push in the “Ship to bms-4” step. - NEW
scripts/lib/gh_app_secret_set.sh— App-authedgh secret setwrapper. - UPDATE
scripts/rotate/*.js(~8 files) — switch post-rotation secret-set calls to the new helper. - UPDATE n8n workflows (8 items, exported via n8n API + committed to
infra-src/n8n-workflows/) — replace PAT credential nodes withGitHub App Token Generatorsub-workflow calls. - UPDATE
docs/playbooks/secret-rotation-access-matrix.md— moveGH_TOKEN/GITHUB_PAT_*from Tier 2 to Tier 1-via-App; keep the Tier 2 row forGH_PAT_ADMINonly. - UPDATE
docs/playbooks/github-pat-rotation.md— correct the SOPS file (n8n-bms4-gh.env.sops, notn8n-bms4.env.sops) and add an “App-first” section that supersedes Option A for non-GH_PAT_ADMINPATs.
5.3 Wave B implementation PR — files preview
- UPDATE
ansible/roles/claude-runner/tasks/main.yml— Playwright + Chromium install tasks (per §3.2.1). - UPDATE
ansible/inventory/host_vars/bms-4.yml— setplaywright_enabled: true. - NEW
scripts/rotate/github-pat.js— Playwright automation (per §3.2.3). - NEW
scripts/rotate/package.json+scripts/rotate/package-lock.json— declareplaywright@1.48.0andotplib(they’re currently referenced but not tracked). - UPDATE
infra/agent-prompts/worker-secret-manager.md— new Tier 2 section (per §3.2.4). - UPDATE
docs/playbooks/github-pat-rotation.md— replace Option A prose with the new worker-side flow (points atscripts/rotate/github-pat.js). - UPDATE
docs/playbooks/secret-rotation-access-matrix.md— addGITHUB_TOTP_SECRETrow (Tier 2 with the safety note). - NEW SOPS ENTRY
GITHUB_TOTP_SECRETinsecrets/n8n-bms4.env.sops— added by the secret-manager worker in the same PR (not by the plan author). Value provided byradieuat issue-creation time.
5.4 New GitHub Issues opened by this plan
The plan-PR does not implement — it queues follow-up issues:
- Issue A (Wave A): “[plan-child] Migrate bms-4 workers off GitHub PATs to installation tokens” — implements §3.1 / §5.2. Labels:
enhancement, refs #4065. - Issue B (Wave B): “[plan-child] Provision Playwright +
GITHUB_TOTP_SECRETon bms-4 secret-manager worker” — implements §3.2 / §5.3. Labels:enhancement,secret-manager, refs #4065, #4053. - Issue C (E2E validation): “[plan-child] Validate autonomous GitHub PAT rotation on TEST_PAT_ROTATION_4065” — implements §3.3. Blocks on Issue B. Refs #4065.
- Issue D (Re-dispatch #4053): worker action inside Issue C (no separate issue); tracked as a comment on #4053.
Opened by the human after this plan-PR merges (or by the worker at plan-approval — decide during review).
6. Regression risks
| Risk | Where | Mitigation |
|---|---|---|
| App token has insufficient permissions for a consumer that PATs handled fine | Wave A — n8n workflow migrations | §2.4 verification step before PR; keep GH_TOKEN fallback in git-credential-gh-token.sh during rollout. |
GITHUB_TOTP_SECRET seed in SOPS grants 2FA bypass on radieu GitHub account to anyone with age keys | Wave B | Same threat model as the App private key already in n8n-bms4.env.sops. Access matrix labels it Tier-2-with-safety-note. Not a new trust surface. |
| Playwright DOM selectors break on next GitHub UI redesign | Wave B scripts/rotate/github-pat.js | Pin playwright@1.48.0. Add DEBUG_SCREENSHOTS mode. On selector failure exit code 1 → human-action fallback (same behavior as today’s blocked state). |
| Headless Chromium runs as non-root claude-runner without sandbox → potential local sandbox-escape | Wave B — --no-sandbox in Chromium launch | Threat model: browser drives only github.com (fixed navigation, no user-controlled URLs). Sandbox-escape risk is bounded by GitHub content trust, which we already trust for all our git operations. Acceptable. |
| Rotation of TOTP seed itself (if compromised) | Wave B ongoing | Same as any other Tier 2 credential — re-run the human step of 2FA reconfiguration, re-add to SOPS. Playbook add-on to §3.2.4 covers this. |
| GitHub PAT UI DOM regressions between the two Waves — during the gap window, PATs are still Tier 2, still blocked | Between Wave A merge and Wave B merge | Wave A does not depend on Wave B — no coupling. If Wave B lags, PAT rotations remain Tier 2 (as today) but Wave A consumers no longer need those PATs to work day-to-day. |
SOPS canary breaks after GITHUB_TOTP_SECRET add | Wave B implementation | Mandatory canary in worker-secret-manager pattern; canary before rename per §Linux worker SOPS pattern. |
PR to main (not dev) — this repo requires it | This plan PR | Confirmed per CLAUDE.md §Branching — all PRs target main directly. |
7. Manual test checklist (for plan review — not for implementation)
- Reviewer confirms Wave A is worth doing before Wave B (do not skip A to jump to B).
- Reviewer confirms the App permission surface via §2.4 API call (or requires that check in the Wave A PR before merging).
- Reviewer confirms
secrets/n8n-bms4.env.sopsis the right home forGITHUB_TOTP_SECRET(bms-4 residency, not administration). - Reviewer confirms Playwright pin (
1.48.0) vs “latest” — pin recommended. - Reviewer approves opening the four follow-up issues (§5.4) as a batch after merge.
- Reviewer approves the deferred et-op-Vercel App-migration scope (Wave A explicitly does not attempt it).
8. Dependencies
- No external services required for this plan-PR. Implementation PRs (Wave A / Wave B) will need:
- Wave A:
python3-jwton bms-4 + vps-i1 (already present peransible/roles/claude-runner). - Wave B: apt packages listed in §3.2.1 + npm registry access from bms-4 (already available — claude-runner installs Claude Code CLI at boot).
- Wave A:
- Blockers upstream: none.
GITHUB_APP_ID+GITHUB_APP_PRIVATE_KEY_B64are already in place (§2.2). Wave B needs one Tier 3 human step (radieu provides TOTP seed) before the implementation issue can complete.
9. Non-scope
- Vercel
et-opruntime migration offGITHUB_TOKEN(Wave B scope covers rotation, not migration). GH_PAT_ADMINmigration to any App — the whole point of admin PATs is operations outside App scope.- The n8n workflow-export tooling (out of scope; existing manual export process from #2141 remains).
- CI runner (
GH_TOKENin GH Actions) — GH provides its ownGITHUB_TOKENenv var per-job, no PAT needed.
Plan-PR only. Implementation follows in the child issues (§5.4) after /review-plan iteration.
File-overlap signature: docs/plans/plan-4065-github-pat-autonomous-rotation.md
files_to_change: docs/plans/plan-4065-github-pat-autonomous-rotation.md