Skip to content

fix(github): evict a cached App JWT on rejection to stop fleet-wide token-minting poisoning #2453

Description

@JSONbored

Parent: #1936

Problem

createAppJwt (src/github/app.ts ~line 381) caches the App-level JWT for 8 minutes (appJwtCache, keyed only by GITHUB_APP_ID), with no eviction path on rejection. #2406 already hardened this for installation tokens (expireCachedInstallationToken + retry-once on a rejected installation token via withInstallationTokenRetry), but the App-level JWT itself has no equivalent.

If GitHub rejects the currently-cached App JWT for any transient reason (a validation hiccup, a clock-skew edge case, a brief App suspend/reinstate) while env.GITHUB_APP_PRIVATE_KEY is unchanged, mintInstallationToken (app.ts ~lines 244-251) keeps handing back the same poisoned JWT from cache (nothing about the cache-validity check — keyed on privateKey === env.GITHUB_APP_PRIVATE_KEY and expiresAtMs > nowMs — changes on a rejection). Every subsequent POST /app/installations/{id}/access_tokens across every installation on the instance gets a 401 and throws, until the 8-minute cache window naturally lapses. appJwtCache is otherwise only ever cleared by the test-only clearInstallationTokenCacheForTest().

Production call sites of createInstallationToken (src/queue/processors.ts, src/services/agent-approval-queue.ts, src/services/agent-action-executor.ts, src/github/backfill.ts, src/github/labels.ts, src/review/rag-index.ts, src/review/grounding-wire.ts) either propagate the throw or silently swallow it — none re-signs a fresh JWT. Net effect: a single JWT rejection can stall merges/comments/check-runs/approvals fleet-wide for up to 8 minutes.

Fix

On a 401 (or bad-credentials-shaped error) from the App-installations access-token endpoint, evict the current appJwtCache entry for env.GITHUB_APP_ID, re-sign a fresh JWT, and retry once — mirroring the withInstallationTokenRetry pattern already used for installation tokens.

Regression tests

  • A rejected App JWT triggers eviction + one fresh re-sign + retry, and a subsequent installation-token mint succeeds without waiting out the cache TTL.
  • A JWT rejection for a genuinely bad private key (not transient) does not infinite-loop — still terminates after the single retry.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions