From e0e0f15da570ff86b5bfbffc5e2a3f7dba4f5f10 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:18:30 -0700 Subject: [PATCH] =?UTF-8?q?feat(orb):=20token-broker=20=E2=80=94=20operato?= =?UTF-8?q?r-issued=20enrollment=20+=20token=20exchange?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Orb token-broker: a maintainer's self-hosted container exchanges a one-time enrollment secret for short-lived GitHub installation tokens, so it acts on its own repos WITHOUT ever holding the Orb App private key (gittensory holds it centrally, mints on demand). All flag-gated (404 until ORB_BROKER_ENABLED). Trust model = das-github-mirror: the OPERATOR is the authority. Rather than a maintainer-OAuth flow (whose admin-of-installation binding the red-team flagged as a privilege-escalation hole), an enrollment is issued only for an install the operator already opted in (registered=1), via the INTERNAL_JOB_TOKEN-gated POST /v1/internal/orb/enrollments. The secret is shown ONCE and stored only as a SHA-256 hash. POST /v1/orb/token (token-exempt; the secret IS the auth): the container presents the secret → - installation_id is read from the enrollment ROW (bound at issue time) — NEVER the request, so a stolen secret for install X can't mint a token for install Y; - the install must STILL be registered=1 and neither suspended nor removed (the gate is re-checked at mint); - createOrbInstallationToken mints on demand and is never cached; GitHub's real expiry is surfaced. Maintainer-OAuth self-enrollment (for external maintainers at scale) is a later layer on this secure core. Advances #1255 (the central Orb data layer). --- src/api/routes.ts | 27 +++++++ src/auth/rate-limit.ts | 1 + src/orb/broker.ts | 57 ++++++++++++++ test/integration/orb-broker.test.ts | 113 ++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 src/orb/broker.ts create mode 100644 test/integration/orb-broker.test.ts diff --git a/src/api/routes.ts b/src/api/routes.ts index 39ec749cf1..f0e8d49357 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -125,6 +125,7 @@ import { handleGitHubWebhook } from "../github/webhook"; import { handleOrbIngest, readOrbIngestBody } from "../orb/ingest"; import { handleOrbWebhook } from "../orb/webhook"; import { handleOrbOAuthCallback } from "../orb/oauth"; +import { brokerOrbToken, isOrbBrokerEnabled, issueOrbEnrollment } from "../orb/broker"; import { computeFleetAnalytics } from "../orb/analytics"; import { handleMcpRequest } from "../mcp/server"; import { buildOpenApiSpec } from "../openapi/spec"; @@ -2873,6 +2874,18 @@ export function createApp() { // Post-install / OAuth landing — the App's Callback URL. Token-exempt; GitHub drives the redirect after a // maintainer installs or updates the Orb App. Lands on a real page instead of a 401. app.get("/v1/orb/oauth/callback", handleOrbOAuthCallback); + // Token-broker exchange: a self-hosted container presents its enrollment secret (Bearer) → a short-lived + // GitHub installation token for the BOUND install. Token-exempt (the enrollment secret IS the auth); flag-gated + // (404 until ORB_BROKER_ENABLED); the installation_id is read server-side from the enrollment, never the request. + app.post("/v1/orb/token", async (c) => { + if (!isOrbBrokerEnabled(c.env)) return c.json({ error: "not_found" }, 404); + const auth = c.req.header("authorization") ?? ""; + const secret = auth.startsWith("Bearer ") ? auth.slice(7).trim() : ""; + if (!secret) return c.json({ error: "missing_enrollment_secret" }, 401); + const result = await brokerOrbToken(c.env, secret); + if ("error" in result) return c.json(result, result.error === "invalid_enrollment" ? 401 : 403); + return c.json(result); + }); // Gittensory Orb (#1255) — central fleet-calibration collector. Receives anonymized, reversal-aware // outcome batches from self-hosted instances. No auth required: all data is HMAC-anonymized by the sender; @@ -2960,6 +2973,19 @@ export function createApp() { return c.json({ installationId, registered: registered === 1 }); }); + // Operator-only: issue a one-time token-broker enrollment secret for a REGISTERED install, to hand to that + // maintainer's self-hosted container. The secret is returned ONCE (stored only hashed). Bearer-gated by the + // /v1/internal/* middleware (INTERNAL_JOB_TOKEN); flag-gated (404 until ORB_BROKER_ENABLED). + app.post("/v1/internal/orb/enrollments", async (c) => { + if (!isOrbBrokerEnabled(c.env)) return c.json({ error: "not_found" }, 404); + const payload = (await c.req.json().catch(() => null)) as { installationId?: unknown } | null; + const installationId = Number(payload?.installationId); + if (!Number.isInteger(installationId) || installationId <= 0) return c.json({ error: "installationId required" }, 400); + const result = await issueOrbEnrollment(c.env, installationId); + if ("error" in result) return c.json(result, result.error === "installation_not_found" ? 404 : 409); + return c.json(result); // { enrollId, secret } — secret shown exactly once + }); + // Convergence (ops / observability, flag GITTENSORY_REVIEW_OPS). Cross-repo review-OUTCOME aggregate (gate-block // ledger + recommendation/slop calibration) for an operator dashboard. Bearer-gated by the `/v1/internal/*` // middleware above (INTERNAL_JOB_TOKEN). Flag-OFF (default) → 404, so the endpoint does not exist and the @@ -4908,6 +4934,7 @@ function requiresApiToken(path: string): boolean { if (path === "/v1/github/webhook") return false; if (path === "/v1/orb/webhook") return false; if (path === "/v1/orb/oauth/callback") return false; + if (path === "/v1/orb/token") return false; if (path === "/v1/orb/ingest") return false; if (path.startsWith("/v1/internal/")) return false; return path.startsWith("/v1/"); diff --git a/src/auth/rate-limit.ts b/src/auth/rate-limit.ts index 15494e0094..ddcdea4c6b 100644 --- a/src/auth/rate-limit.ts +++ b/src/auth/rate-limit.ts @@ -100,6 +100,7 @@ export function routeClassForPath(path: string): RateLimitClass { // narrow IP range; the per-IP strict cap is proven for /v1/github/webhook and #1292 reserves headroom). if (path === "/v1/orb/webhook") return "strict"; if (path === "/v1/orb/oauth/callback") return "strict"; + if (path === "/v1/orb/token") return "strict"; // Orb telemetry ingest: unauthenticated + write, accepting anonymized batches from untrusted // self-host instances. Strict (10/min per IP) caps abuse — legitimate instances export hourly. if (path === "/v1/orb/ingest") return "strict"; diff --git a/src/orb/broker.ts b/src/orb/broker.ts new file mode 100644 index 0000000000..03ab99551f --- /dev/null +++ b/src/orb/broker.ts @@ -0,0 +1,57 @@ +// Gittensory Orb central GitHub App (#1255) — the token-broker. A maintainer's self-hosted container exchanges a +// one-time enrollment secret for short-lived GitHub installation tokens, so it can act on its own repos WITHOUT +// ever holding the Orb App private key (gittensory holds it centrally and mints on demand). +// +// Trust model (das-github-mirror): the OPERATOR is the authority. An enrollment is issued only for an install the +// operator has already opted in (registered=1) via the internal-token-gated POST /v1/internal/orb/enrollments; +// the secret is shown to the operator ONCE and stored only as a SHA-256 hash. The container then presents that +// secret to /v1/orb/token. The minted token's installation_id comes from the enrollment ROW (bound server-side at +// issue time) — never from the request — so a stolen secret for install X can never mint a token for install Y. +// Every path is inert (404) until ORB_BROKER_ENABLED is set. (Maintainer-OAuth self-enrollment is a later layer; +// the operator-issued path here avoids the OAuth privilege-escalation surface the red-team flagged.) +import { createOpaqueToken, hashToken } from "../auth/security"; +import { createOrbInstallationToken } from "./app-auth"; + +export function isOrbBrokerEnabled(env: Env): boolean { + return /^(1|true|yes|on)$/i.test(String(env.ORB_BROKER_ENABLED ?? "").trim()); +} + +export type IssueResult = { enrollId: string; secret: string } | { error: "installation_not_found" | "installation_not_registered" }; + +/** Operator-only: mint a one-time enrollment secret for a REGISTERED install. Returns the plaintext secret ONCE + * (stored only hashed) for the operator to hand to the container's config. */ +export async function issueOrbEnrollment(env: Env, installationId: number): Promise { + const install = await env.DB.prepare("SELECT registered FROM orb_github_installations WHERE installation_id = ?").bind(installationId).first<{ registered: number }>(); + if (!install) return { error: "installation_not_found" }; + if (install.registered !== 1) return { error: "installation_not_registered" }; + const enrollId = createOpaqueToken("orbenr"); + const secret = createOpaqueToken("orbsec"); + await env.DB.prepare( + `INSERT INTO orb_enrollments (enroll_id, installation_id, secret_hash, state, authorized_at, enrolled_at) + VALUES (?, ?, ?, 'enrolled', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, + ) + .bind(enrollId, installationId, await hashToken(secret)) + .run(); + return { enrollId, secret }; +} + +export type BrokerResult = { token: string; installationId: number; expiresAt: string } | { error: "invalid_enrollment" | "installation_not_eligible" }; + +/** The container's token-exchange: a valid enrollment secret → a short-lived installation token for the BOUND + * install. installation_id is read from the enrollment row, never the caller; the install must still be + * registered=1 and neither suspended nor removed at mint time (the gate is re-checked, not trusted from issue). */ +export async function brokerOrbToken(env: Env, secret: string): Promise { + const row = await env.DB + .prepare("SELECT enroll_id, installation_id, state, revoked_at FROM orb_enrollments WHERE secret_hash = ?") + .bind(await hashToken(secret)) + .first<{ enroll_id: string; installation_id: number; state: string; revoked_at: string | null }>(); + if (!row || row.state !== "enrolled" || row.revoked_at !== null) return { error: "invalid_enrollment" }; + const install = await env.DB + .prepare("SELECT registered, suspended_at, removed_at FROM orb_github_installations WHERE installation_id = ?") + .bind(row.installation_id) + .first<{ registered: number; suspended_at: string | null; removed_at: string | null }>(); + if (!install || install.registered !== 1 || install.suspended_at !== null || install.removed_at !== null) return { error: "installation_not_eligible" }; + const minted = await createOrbInstallationToken(env, row.installation_id); + await env.DB.prepare("UPDATE orb_enrollments SET last_token_at = CURRENT_TIMESTAMP WHERE enroll_id = ?").bind(row.enroll_id).run(); + return { token: minted.token, installationId: row.installation_id, expiresAt: minted.expiresAt }; +} diff --git a/test/integration/orb-broker.test.ts b/test/integration/orb-broker.test.ts new file mode 100644 index 0000000000..871addc79e --- /dev/null +++ b/test/integration/orb-broker.test.ts @@ -0,0 +1,113 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createApp } from "../../src/api/routes"; +import { brokerOrbToken, isOrbBrokerEnabled, issueOrbEnrollment } from "../../src/orb/broker"; +import { createTestEnv, type TestD1Database } from "../helpers/d1"; + +async function pkcs8Pem(): Promise { + const key = (await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["sign", "verify"])) as CryptoKeyPair; + const b64 = Buffer.from((await crypto.subtle.exportKey("pkcs8", key.privateKey)) as ArrayBuffer).toString("base64").replace(/(.{64})/g, "$1\n"); + return `-----BEGIN PRIVATE KEY-----\n${b64}\n-----END PRIVATE KEY-----`; +} +const db = (e: Env) => e.DB as unknown as TestD1Database; +const seedInstall = (e: Env, id: number, cols: Record = {}) => { + const all: Record = { installation_id: id, registered: 1, ...cols }; + const keys = Object.keys(all); + return db(e).prepare(`INSERT INTO orb_github_installations (${keys.join(", ")}) VALUES (${keys.map(() => "?").join(", ")})`).bind(...keys.map((k) => all[k] as string | number | null)).run(); +}; +const brokerEnv = async (over: Partial = {}): Promise => + createTestEnv({ ORB_BROKER_ENABLED: "true", ORB_GITHUB_APP_ID: "4139483", ORB_GITHUB_APP_PRIVATE_KEY: await pkcs8Pem(), INTERNAL_JOB_TOKEN: "dev-internal-token", ...over }); +const tokenFetch = (token = "ghs_broker", expires = "2026-06-25T08:00:00Z") => vi.stubGlobal("fetch", async () => Response.json({ token, expires_at: expires })); + +afterEach(() => vi.unstubAllGlobals()); + +describe("isOrbBrokerEnabled", () => { + it("is off by default, on for a truthy flag", () => { + expect(isOrbBrokerEnabled(createTestEnv())).toBe(false); + expect(isOrbBrokerEnabled(createTestEnv({ ORB_BROKER_ENABLED: "true" }))).toBe(true); + }); +}); + +describe("issueOrbEnrollment", () => { + it("404s an unknown install, rejects an unregistered one, issues a hashed secret for a registered one", async () => { + const e = await brokerEnv(); + expect(await issueOrbEnrollment(e, 999)).toEqual({ error: "installation_not_found" }); + await seedInstall(e, 200, { registered: 0 }); + expect(await issueOrbEnrollment(e, 200)).toEqual({ error: "installation_not_registered" }); + await seedInstall(e, 201, { registered: 1 }); + const issued = await issueOrbEnrollment(e, 201); + expect(issued).toMatchObject({ enrollId: expect.stringMatching(/^orbenr_/), secret: expect.stringMatching(/^orbsec_/) }); + const row = await db(e).prepare("SELECT state, installation_id, secret_hash FROM orb_enrollments WHERE installation_id=201").first<{ state: string; installation_id: number; secret_hash: string }>(); + expect(row).toMatchObject({ state: "enrolled", installation_id: 201 }); + expect(row?.secret_hash).not.toContain("orbsec"); // stored hashed, never plaintext + }); +}); + +describe("brokerOrbToken", () => { + it("mints a token for a valid enrollment on a registered install (id bound server-side)", async () => { + const e = await brokerEnv(); + await seedInstall(e, 300, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 300)) as { secret: string }; + tokenFetch("ghs_minted", "2026-06-25T08:00:00Z"); + expect(await brokerOrbToken(e, secret)).toEqual({ token: "ghs_minted", installationId: 300, expiresAt: "2026-06-25T08:00:00Z" }); + expect((await db(e).prepare("SELECT last_token_at FROM orb_enrollments WHERE installation_id=300").first<{ last_token_at: string | null }>())?.last_token_at).not.toBeNull(); + }); + + it("rejects an unknown or revoked enrollment", async () => { + const e = await brokerEnv(); + expect(await brokerOrbToken(e, "orbsec_bogus")).toEqual({ error: "invalid_enrollment" }); + await seedInstall(e, 301, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 301)) as { secret: string }; + await db(e).prepare("UPDATE orb_enrollments SET revoked_at=CURRENT_TIMESTAMP WHERE installation_id=301").run(); + expect(await brokerOrbToken(e, secret)).toEqual({ error: "invalid_enrollment" }); + }); + + it("re-checks the install gate at mint time (unregistered / suspended / removed → not eligible)", async () => { + const e = await brokerEnv(); + await seedInstall(e, 302, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 302)) as { secret: string }; + await db(e).prepare("UPDATE orb_github_installations SET suspended_at=CURRENT_TIMESTAMP WHERE installation_id=302").run(); + expect(await brokerOrbToken(e, secret)).toEqual({ error: "installation_not_eligible" }); + }); +}); + +describe("broker endpoints", () => { + const app = createApp(); + const auth = { authorization: "Bearer dev-internal-token" }; + + it("both routes 404 when the broker flag is off (byte-identical deploy)", async () => { + const off = createTestEnv({ INTERNAL_JOB_TOKEN: "dev-internal-token" }); + expect((await app.request("/v1/orb/token", { method: "POST" }, off)).status).toBe(404); + expect((await app.request("/v1/internal/orb/enrollments", { method: "POST", headers: auth, body: "{}" }, off)).status).toBe(404); + }); + + it("the full operator-issue → container-exchange flow over HTTP", async () => { + const e = await brokerEnv(); + await seedInstall(e, 400, { registered: 1 }); + const issueRes = await app.request("/v1/internal/orb/enrollments", { method: "POST", headers: auth, body: JSON.stringify({ installationId: 400 }) }, e); + expect(issueRes.status).toBe(200); + const { secret } = (await issueRes.json()) as { secret: string }; + tokenFetch("ghs_flow"); + const tokRes = await app.request("/v1/orb/token", { method: "POST", headers: { authorization: `Bearer ${secret}` } }, e); + expect(tokRes.status).toBe(200); + expect(await tokRes.json()).toMatchObject({ token: "ghs_flow", installationId: 400 }); + }); + + it("/v1/orb/token: 401 without a Bearer secret, 401 on a bad secret, 403 when the install became ineligible", async () => { + const e = await brokerEnv(); + expect((await app.request("/v1/orb/token", { method: "POST" }, e)).status).toBe(401); + expect((await app.request("/v1/orb/token", { method: "POST", headers: { authorization: "Bearer orbsec_bad" } }, e)).status).toBe(401); + await seedInstall(e, 401, { registered: 1 }); + const { secret } = (await issueOrbEnrollment(e, 401)) as { secret: string }; + await db(e).prepare("UPDATE orb_github_installations SET registered=0 WHERE installation_id=401").run(); + expect((await app.request("/v1/orb/token", { method: "POST", headers: { authorization: `Bearer ${secret}` } }, e)).status).toBe(403); + }); + + it("/v1/internal/orb/enrollments: 400 missing id, 409 unregistered, 404 unknown", async () => { + const e = await brokerEnv(); + await seedInstall(e, 402, { registered: 0 }); + expect((await app.request("/v1/internal/orb/enrollments", { method: "POST", headers: auth, body: "{}" }, e)).status).toBe(400); + expect((await app.request("/v1/internal/orb/enrollments", { method: "POST", headers: auth, body: "{bad" }, e)).status).toBe(400); // unparseable JSON → catch → null + expect((await app.request("/v1/internal/orb/enrollments", { method: "POST", headers: auth, body: JSON.stringify({ installationId: 402 }) }, e)).status).toBe(409); + expect((await app.request("/v1/internal/orb/enrollments", { method: "POST", headers: auth, body: JSON.stringify({ installationId: 999 }) }, e)).status).toBe(404); + }); +});