Skip to content

orb(broker): enrollment secrets are append-only — re-enrolling mints a second valid secret and revokes nothing, and self-hosters cannot reach the revoke route at all #9149

Description

@JSONbored

Problem

issueOrbEnrollment (src/orb/broker.ts:57-75) is a bare INSERT. It never revokes, updates, or even
counts sibling rows for the same installation_id:

INSERT INTO orb_enrollments (…) VALUES (…, 'enrolled', …)

handleOrbEnrollment (src/orb/oauth.ts:112) calls it on every successful callback, so an admin who
re-runs the install flow accumulates N simultaneously-valid secrets.

src/orb/relay.ts:533-538 already documents the consequence — "issueOrbEnrollment INSERTs a new row per
enrollment without revoking prior enrolled rows"
— but #1783 only fixed the push-target election, not
the lifecycle.

The only revocation path, revokeOrbEnrollment, is reachable solely through
POST /v1/internal/orb/enrollments/:enrollId/revoke (src/api/routes.ts:4577), which sits behind
INTERNAL_JOB_TOKEN. A self-hosting maintainer has no way to reach it.

Trigger

A maintainer's .env leaks (or the browser page that displayed the secret does). They re-run the GitHub
install flow expecting rotation. They get a second secret; the leaked one keeps working.

Impact

A leaked enrollment secret is unrevocable GitHub write access (merge/close/comment) to the
maintainer's repos, until they uninstall the App entirely. The advertised zero-touch self-service
enrollment has no matching self-service revocation.

Requirements

  • In issueOrbEnrollment, mark prior state='enrolled' rows for the same installation_id as revoked —
    or add an explicit ?rotate=1 confirmation step in the OAuth landing page so the operator chooses.
  • Expose a maintainer-facing revoke behind the same admin-of-installation check verifyInstallationAdmin
    already implements correctly.
  • Surface the count of live enrollments somewhere the operator can see it.
  • Add Cache-Control: no-store to secretPage (src/orb/oauth.ts:150), which currently renders the
    enrollment secret into a cacheable HTML GET response.

Test Coverage Requirements

99%+ patch coverage, branch-counted; both arms of revoke-on-reissue, and a test that a superseded secret
no longer mints a token.

Links & Resources

maintainer-only — credential lifecycle.

Metadata

Metadata

Assignees

Labels

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

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions