Recipes

Legion machine Google-identity sign-in for the secrets broker

Discovered from README cross-links · today

Security, Science & HardwareSoftware & DevOps

Machines trade a Google identity token for DPoP-bound secrets; people sign

What they did

The author reworked how Legion's secrets broker authenticates machines and people. Previously a machine could only reach the broker through a device grant — a code a person typed — and that login died 30 days after its last refresh, so a rebuilt machine needed a human again. Sessions on a machine also had no way to get their own Dispatch token, so people copied personal tokens onto machines (INFRA-958). The change lets a machine trade its Google identity token for a DPoP-bound access token that names its fleet's service, lasts an hour or less, and carries no refresh token; the same trade mints the launcher credential its sessions enrol under, so the old VM token login and the `vm` enrollment kind are gone. A person now runs `agent-secrets login` once on a box; that sign-in is listed and revocable on Dispatch's credentials page, and revoking it ends that box's tokens at once while the person's other machines keep working. Secret authority follows the access token: a machine reads its service's secrets and shared agent-tier ones and no person's, and a person reaches their own only through their own sign-in on that box. A token naming a service and no person may read Dispatch but is refused every write with `SIGN_IN_REQUIRED`, whose message names the sign-in command; the Envoy listener accepts such a token too but refuses one naming another session. One token client, in Go and TypeScript, gets and renews tokens for `agent-secrets`, the Legion controller, Oh My Pi, Claude Code and the `dispatch` command, both held to one wire corpus. `BROKER_MACHINE_LOGIN_URL` became optional: unset, the broker starts, serves no device grant, says so in its RFC 8414 metadata, and Dispatch serves no machine-code page. The author added a dev stack — real broker, Dispatch, listener, NATS and OIDC issuer on real Postgres — and drove two proofs: a machine's trade reading a fleet token (with a negative control refused for a person-owned secret) and a person's `agent-secrets login` with a typed code, then reading their own secret.

How it went

The change stands at head f767a511 with 43 PR threads answered and resolved, and a `ce-simplify-code` pass at b80a24af that applied seven findings and caught three real defects (a missed Dispatch client construction, an uncanonicalized sign-in list query, and a silently wrong credential expiry) plus two per-request costs. The dev-stack E2E proved both the machine trade and the person sign-in, each with a negative control. CodeQL reports 14 alerts, one a false positive in new code.

Worth knowing

A TypeScript session cannot trade under a fleet or pod entry that fixes a session, by design; no deployment sets such an entry with the broker route today. A copied `identity-key.pem` keeps signing launcher proofs after a machine-login revoke until the key is removed and the helper restarted, and a copied sign-in record outlives a Dispatch revoke until the Google grant is revoked. The new refusals close nothing today because no deployment sets `AGENT_SECRETS_MACHINE_IDENTITY` or the broker exchange settings yet. Socket tests for `cmd/agent-secrets`, `cmd/agent-secrets-helper` and `internal/broker/helper` cannot run in a gVisor pod, so CI is their only run. Migration 0024 has no down migration; a box re-enrols rather than being restored.

Wiring

Order: first the broker's OAuth exchange (`packages/envoy/internal/broker/oauth/exchange.go`) trades a Google identity token for a DPoP-bound access token, with `personDeclinesNothing` and `personCredential` handling a sign-in's two halves so a revoke ends a token from a trade that commits while the revoke waits. The trade also mints the launcher credential sessions enrol under, retiring the VM token route and the `vm` kind; migration 0024 revokes live `vm` enrollments, grants and pending requests. Next the shared token client — Go and TypeScript — gets and renews tokens for `agent-secrets`, the Legion controller, Oh My Pi, Claude Code and `dispatch`, both implementations checked against one wire corpus. The TypeScript session rule lives in `packages/broker-client/src/environment.ts` (`tradeSession`), caller-first, refusing a fleet or pod entry that fixes a session. The identity key sits at `~/.config/agent-secrets/identity-key.pem`, with a three-step cutoff in `guides/revoke-a-session.md`. Dispatch gains the sign-in list and revoke on its credentials page and the `SIGN_IN_REQUIRED` refusal on writes; the Envoy listener gains the 403 on eight routes for a token naming another session, and ignores the header when told of no broker. Config: `BROKER_MACHINE_LOGIN_URL` optional (unset means no device grant, stated in RFC 8414 metadata, and no machine-code page); `AGENT_SECRETS_MACHINE_IDENTITY` on clients plus `BROKER_EXCHANGE_ACCOUNTS` and `BROKER_GOOGLE_OIDC_AUDIENCE` at the broker turn the new paths on. The dev stack `packages/envoy/scripts/dev-stack.sh` boots the real broker, Dispatch, listener, NATS and OIDC issuer against real Postgres with real migrations.

Try it yourself

In this repo, replace the broker's device-grant machine login with a Google-identity trade. A machine should exchange its Google identity token for a DPoP-bound access token that names its fleet's service, lasts an hour or less, and carries no refresh token; the same trade should mint the launcher credential its sessions enrol under. Remove the VM token login route, the `vm` enrollment kind, and `agent-secrets enroll --vm` / `renew --vm`. Add `agent-secrets login` so a person signs in once per box, list that sign-in on Dispatch's credentials page, and make a revoke end that box's tokens at once without touching the person's other machines. Make secret authority follow the access token: a machine reads its service's secrets and shared agent-tier ones and no person's. Refuse every Dispatch write by a service-only token with `SIGN_IN_REQUIRED` naming the sign-in command, and have the Envoy listener refuse a broker token that names another session. Write one token client in Go and one in TypeScript, held to a shared wire corpus, and make `BROKER_MACHINE_LOGIN_URL` optional so an unset broker serves no device grant and advertises that in RFC 8414 metadata. Add a dev stack with the real broker, Dispatch, listener, NATS and OIDC issuer on Postgres, and prove both a machine trade and a person sign-in with negative controls.

Source

Tools in this stack