HTTP API
The broker’s API answers JSON. A refusal is {"code": "…", "error": "…"} with the HTTP status below, and a refusal that carries more lists its body under its route; the error reference lists every code. The broker’s own approval page, whose routes take a broker sign-in session, answers a person’s browser with HTML pages and redirects instead, listed by status.
Authentication
Section titled “Authentication”Each route takes one kind of credential, or none. These are the refusals the broker answers for the credential itself, before the route runs.
| Credential | Description | Refusals |
|---|---|---|
| None | A route anyone may call, with no credential. | none |
| Machine credential | A route for a machine’s launcher (agent-secrets-helper, or the Legion daemon), which signs each call’s Proof header with the key its machine login’s credential is bound to. | 503 DATABASE_UNAVAILABLE401 LAUNCHER_INVALID |
| Caller ServiceAccount token | A route for a service calling the broker with its own Kubernetes ServiceAccount token: Dispatch’s server, which lists what waits on a person, relays a person’s machine-login decisions and revokes, and links the broker’s own page for a secret request; and whatever resource server reads a token back through introspection. The token is verified against the broker’s Kubernetes issuer and its account must be one BROKER_API_ACCOUNTS lists, and the handler is given that entry’s name — the caller’s identity, which it logs when it acts. Each such route declares the power its caller must hold, and the caller’s name answers the powers it has (accountPowers), which authenticate checks before the handler runs: being listed reaches the routes of that caller’s own powers and no others. | 403 ACCOUNT_LACKS_POWER403 ACCOUNT_NOT_LISTED401 ACCOUNT_TOKENS_OFF401 ACCOUNT_TOKEN_EXPIRED401 ACCOUNT_TOKEN_INVALID401 ACCOUNT_TOKEN_TOO_LONG_LIVED429 RATE_LIMITED |
| Service login ServiceAccount token | A route for a service calling the broker with its own Kubernetes ServiceAccount token, the account BROKER_SERVICE_LOGINS lets log in as that service — a deployment’s Legion daemon, which registers the pods it starts. The token is verified and mapped to its service by the check a token login makes (machine.Service.VerifyServiceToken). The handler is given that check’s own answer, one machine.ServiceAccount: the service the caller logs in as, the account whose token it presented, and the pod that token was bound to. The account and the pod come with it because BROKER_SERVICE_LOGINS may name an account BROKER_SERVICES binds to a service: every pod of such an account logs that service in, so a caller here may be the very pod a request names, and the account and the pod uid are what tell the two apart. It is a class of its own rather than a power of accountAuth’s because the two lists are two authorities: BROKER_API_ACCOUNTS names the callers that relay a person’s decisions and read tokens back, and a daemon that registers its own pods is not one of them. A deployment already lists its per-project daemons in BROKER_SERVICE_LOGINS for their token logins, so these routes need no new setting and no new account. | 403 ACCOUNT_NOT_LISTED401 ACCOUNT_TOKENS_OFF401 ACCOUNT_TOKEN_EXPIRED401 ACCOUNT_TOKEN_INVALID401 ACCOUNT_TOKEN_TOO_LONG_LIVED429 RATE_LIMITED |
| Renewal session proof | Session authentication for the one route that gives an ended enrollment a distinct refusal. A skewed/malformed proof remains PROOF_INVALID; only proof.ErrNotLive is ENROLLMENT_NOT_LIVE so the pod renewer can stop rather than retry an enrollment that cannot recover. | 503 DATABASE_UNAVAILABLE401 ENROLLMENT_NOT_LIVE401 PROOF_INVALID |
| Session proof | A route for an enrolled agent session, which signs each call’s Proof header with its own key, and which acts only on its own enrollment, requests and grants. | 503 DATABASE_UNAVAILABLE401 PROOF_INVALID |
| Caller ServiceAccount token, checked by the handler | A route whose handler authenticates its own caller, because what it may answer is settled before any credential is read: the two decide routes read the record’s kind first and refuse a secret request 403 DECIDE_ON_BROKER_PAGE naming the broker’s own page, to every caller, token or none. Register checks nothing for such a row; the handler spends the account class’s per-address bucket and then authenticates a machine login’s decision exactly as accountAuth does (decideRecord), so one is still decided only on a listed service’s ServiceAccount token. The row declares the power that caller must hold all the same, and the handler is given it: authenticate cannot check it here, since it is the handler that reads the caller, so the check is decideRecord’s, against the same accountPowers and with the same 403 ACCOUNT_LACKS_POWER as every other row of that authority. | none |
| Broker sign-in session | A route of the broker’s own approval page, for a person in a browser signed in to the broker itself (OpenID Connect with the broker’s own client, BROKER_SIGNIN_*). The handler is given the session the request’s __Host-broker-session cookie names, or the zero session when it names none that is live, since what that means is the route’s to answer: the page starts a sign-in, a decision answers a page linking the record it names. Every route of the class spends its source address’s bucket first, the one the account class spends (authenticate), so no caller drives a record read, a token request to the sign-in provider or a session write unmetered. Mounted only when the broker builds its approval page, which every broker does. | 429 (an HTML page) 500 (an HTML page) |
Routes
Section titled “Routes”| Method | Path | Credential | What it does |
|---|---|---|---|
GET |
/.well-known/oauth-authorization-server |
None | The broker’s own OAuth 2.0 authorization server metadata (RFC 8414): its issuer, its token, device authorization, introspection and revocation endpoints, the grants it serves, how a client authenticates at the token endpoint, the algorithms a DPoP proof may be signed with, and the subject token types its RFC 8693 exchange takes. It names the device code and the device authorization endpoint only while the broker serves the device grant, which is while BROKER_MACHINE_LOGIN_URL is set. It needs no credential: it is what a client reads before it holds one, and every word of it is already public in the broker’s own reference. Mounted only when the broker runs its OAuth provider, since with none it would name endpoints that are not there. |
POST |
/v1/enrollments |
Machine credential | Enroll a session’s or pod’s signing key under the caller’s machine credential. |
DELETE |
/v1/enrollments/{id} |
Machine credential | Revoke an enrollment made under the caller’s machine credential’s operator, or a pod enrollment the caller’s own machine credential made. |
POST |
/v1/launcher-credentials/token |
None | Log a service in with its own projected Kubernetes ServiceAccount token, which no person approves: answers its launcher credential’s id and expiry. |
POST |
/v1/oauth/device_authorization |
None | Start a person’s machine login as an OAuth device authorization (RFC 8628): answers the device code the machine polls with and the code the person types. Rate-limited per source address and per person its login_hint names; mounted only when the broker runs its OAuth provider. |
POST |
/v1/oauth/token |
None | Exchange an approved device code, or a refresh token, for a DPoP-bound token pair naming the machine login’s launcher credential; mounted only when the broker runs its OAuth provider. |
POST |
/v1/oauth/introspect |
Caller ServiceAccount token | Read one of the broker’s access tokens back (RFC 7662): whether it is live and, when it is, the session, the person and the project it carries, and the key it is bound to. Mounted only when the broker runs its OAuth provider. |
POST |
/v1/oauth/revoke |
Caller ServiceAccount token | End one of the broker’s own access tokens (RFC 7009), an exchanged pod’s or workflow run’s included, which nothing else can end before its own expiry: the form names the token, the client it was issued to, and optionally which kind to look for first. It needs relaysDecisions rather than readsTokensBack because ending one of the broker’s tokens is the same authority class as ending a machine login or a grant, both already behind that power; a caller listed only to read a token back must not be able to end one. Mounted only when the broker runs its OAuth provider. |
POST |
/v1/oauth/exchange/pod-sessions |
Service login ServiceAccount token | Register that one pod may exchange under one session name: the row the token exchange reads before it mints a token for a pod of an account that fixes no session, written by the deployment’s own daemon as it starts that pod. Mounted only when the broker runs its OAuth provider. |
POST |
/v1/enrollments/{id}/renew |
Renewal session proof | Extend the calling session’s lease. |
GET |
/v1/enrollments/self |
Session proof | Describe the calling session’s enrollment and its live grants. |
POST |
/v1/requests |
Session proof | Request secrets: the policy grants or denies them at once, or a person must approve them. |
GET |
/v1/requests/{id} |
Session proof | Read one of the calling session’s requests. |
POST |
/v1/requests/{id}/cancel |
Session proof | Cancel one of the calling session’s pending requests. |
POST |
/v1/grants/{id}/values |
Session proof | Release a live grant’s values to the session that holds it. |
POST |
/v1/grants/{id}/revoke |
Session proof | End a grant the calling session holds. |
GET |
/v1/pending |
Caller ServiceAccount token | List the pending requests and machine logins the named person decides, each secret request with the broker’s own page where that person decides it. |
POST |
/v1/credential-requests/{record}/approve |
Caller ServiceAccount token, checked by the handler | Approve a pending machine login as the named person, who must type its code: the broker takes the person’s name on the caller’s word, from a listed service’s ServiceAccount token, which the handler verifies itself — and whose name must carry relaysDecisions, the power this row declares — after it has read the record’s kind. It reads the kind first because a secret request is refused before any caller is looked at: it is decided only on the broker’s own page by a person signed in to the broker, so this answers 403 DECIDE_ON_BROKER_PAGE naming that page to every caller, token or none, writing nothing. |
POST |
/v1/credential-requests/{record}/deny |
Caller ServiceAccount token, checked by the handler | Deny a pending machine login as the named person, who must type its code, on the caller’s word and behind the same authentication and the same power as approve. A secret request is refused the same way, writing nothing, with 403 DECIDE_ON_BROKER_PAGE naming the page where a person signed in to the broker denies it. |
POST |
/v1/machine-logins/lookup |
Caller ServiceAccount token | Find a pending machine login by the confirmation code its machine shows. |
GET |
/v1/launcher-credentials |
Caller ServiceAccount token | List the machine logins the named person may revoke, every service’s, such as the Legion daemon’s, whoever approved it, and the person’s own machines’, that are not revoked and are unexpired or expired with a session still running, each naming who approved it. The person is the one the caller names. |
POST |
/v1/launcher-credentials/{id}/revoke-by-approver |
Caller ServiceAccount token | End a machine login, expired or not: a service’s (its token login, or one a person approved before token logins) as anyone signed in, a person’s machine’s as the person who approved it. Its launcher proofs stop authenticating and every session it enrolled, pods included, ends with its grants and pending requests. The revoking person is the login the request names, which the broker still takes on the caller’s word: a revoke only takes power away. |
GET |
/v1/grants |
Caller ServiceAccount token | List the live grants of the named person’s sessions, automatic or approved, and those the person approved. The person is the one the caller names. |
POST |
/v1/grants/{id}/revoke-by-approver |
Caller ServiceAccount token | End a grant as its approver or as its enrollment’s operator. The operator’s revoke also withholds the secrets the grant’s request got automatically from that session, even when the grant had already ended: its other grants that got them automatically end too, and it asks before it gets them again. The revoking person is the login the request names, which the broker still takes on the caller’s word: a revoke only takes power away. |
GET |
/v1/person-signins |
Caller ServiceAccount token | List the sign-ins the named person made on their machines with agent-secrets login, by machine: one row per box key they signed in with and have not revoked, each naming the machine’s Google account, the box key’s thumbprint, and when the sign-in was made and last used. The person is the one the caller names, as it is for every list Dispatch’s server relays. |
POST |
/v1/person-signins/{id}/revoke |
Caller ServiceAccount token | End one of the named person’s sign-ins: that box key trades no token at the exchange again, and every access token already minted on that box under that sign-in ends in the revoke’s own transaction, so none of them outlives it. The person’s sign-ins on their other boxes keep working, and the box signs in again with a key of its own. The revoking person is the one the request names, which the broker takes on the caller’s word: a revoke only takes power away. |
GET |
/v1/grant-log |
Caller ServiceAccount token | Read the broker’s grant log for one closed period, for a quarterly access review: every audit row between ?since and ?until (RFC 3339, half-open), oldest first, each naming the session that wrote it, that session’s person, the service the broker verified, the secrets asked for and their decisions, the state, the approver and who decided. ?after resumes after an id and ?limit bounds the page, which is a thousand rows at most. ?until must be at least an hour before now, so that every row of the period has committed before the review reads it. It is the one route readsGrantLog reaches: the review reads the broker’s record and changes nothing in it. |
GET |
/v1/operator/machines |
Machine credential | List the logins of the calling credential’s operator’s own machines, as Dispatch’s machine-logins page lists them, without the service logins that page also lists. |
POST |
/v1/operator/machines/{id}/revoke |
Machine credential | End one of the operator’s own machines’ logins: no proof signed with it authenticates again and every session it enrolled ends. A service’s login is not found here, like an unknown id. Revoking the calling machine’s own login ends this machine’s access. |
POST |
/v1/operator/machines/{id}/retire |
Machine credential | Retire the calling machine login after its machine logged in again: it enrolls nothing more and cannot refresh, while the sessions it enrolled keep running and renewing with their own keys. {id} must be the calling credential. |
GET |
/v1/operator/grants |
Machine credential | List the live grants of the operator’s sessions and those the operator approved, exactly as Dispatch’s Live grants page lists them. |
POST |
/v1/operator/grants/{id}/revoke |
Machine credential | End a grant as the operator: the revocation Dispatch’s page offers, which also withholds an automatic grant’s secrets from its session. |
GET |
/v1/settings |
None | The namespace prefix, AWS account and region, and the agent-secrets key the broker serves: what the CLI needs so it and the broker never disagree about which secret a name means. |
POST |
/v1/secrets/{name}/reread |
None | Reread one secret from Secrets Manager now, as whoever just wrote it may ask: the change is served, or gone, within seconds instead of at the next five-minute reload. Open to any caller, and rate-limited both per source address and over every caller at once, since each reread holds the policy’s writer lock while it reads; a reread only moves the broker toward AWS’s own truth. |
GET |
/approve/{record} |
Broker sign-in session | A person’s approval page for one credential-request record, an HTML page on the broker’s own origin: the record’s secret names, the enrollment the broker verified asked, the session the request claims when it differs, the reason (empty only on a record an older build wrote before the broker required one wherever a person decides), the lifetime, the times, the approver and the state, one sentence saying what Approve gives and for how long and one saying what Deny and the expiry do, then every other pending secret request the person may decide, with Approve and Deny for each the person may decide and one “Approve all N shown”. It checks the id’s shape, then the session, then the record: a malformed id is a 400, a request with no live session, or with ?switch=1, redirects to the sign-in provider and returns here, and only a signed-in person’s unknown record id is a 404. A machine login’s record is printed with the note that it is decided by typing its code on Dispatch’s machine-login page, and offers no button. |
POST |
/approve |
Broker sign-in session | Decide records an approval page printed, as the signed-in person: a form from the broker’s own origin only, carrying the page’s token, which binds it to the session and to exactly the records the page printed with a button, for an hour. Each record is decided on its own, and the answer is a 303 back to the page, which says what became of each. |
GET |
/signin/callback |
Broker sign-in session | Finish a sign-in to the approval page, the provider’s redirect back with a code, checked against the sign-in this browser started (its __Host-broker-signin cookie, used once): state, PKCE, the ID token and the Workspace domain. It makes a new session, ending the one the browser held, and answers a 303 to the record’s page the sign-in started from. |
POST |
/signin/logout |
Broker sign-in session | Sign out of the approval page, from the broker’s own origin only: the session ends and its cookie is cleared, and the answer links back to the record. |
GET |
/signin/status |
None | Report the broker’s sign-in: whether its sign-in provider accepted the broker’s own client at the last hourly check and when, when a person last signed in, when a sign-in last failed and the coded reason it failed with, and when a decision was last made on the approval page. It needs no credential because a production check fetches it from a pod where no person is signed in, and because it names no login, no secret and no provider’s message: every word it answers is one of two closed sets this broker chose (internal/broker/store’s signin_status). It spends its source address’s bucket in its own handler, as the page’s routes spend it in authenticate, so this read of the broker’s own row is metered too. |
GET |
/approve/assets/approve.css |
None | The approval page’s one stylesheet, embedded in the broker; it needs no credential and spends no rate-limit bucket, since every page it dresses is read before anyone has signed in and the bytes are the binary’s. |
GET |
/healthz |
None | Report whether the broker can reach its database. From SIGTERM onward it answers 503 SHUTTING_DOWN instead (Deps.Draining) while the process goes on serving every other route, which is what takes a stopping pod out of its Service’s endpoints before it stops accepting connections. So this is a readiness probe and must never be used as a liveness probe: a liveness probe on it would have the kubelet kill the container in the middle of the drain, dropping the requests the pod was draining. |
GET /.well-known/oauth-authorization-server
Section titled “GET /.well-known/oauth-authorization-server”The broker’s own OAuth 2.0 authorization server metadata (RFC 8414): its issuer, its token, device authorization, introspection and revocation endpoints, the grants it serves, how a client authenticates at the token endpoint, the algorithms a DPoP proof may be signed with, and the subject token types its RFC 8693 exchange takes. It names the device code and the device authorization endpoint only while the broker serves the device grant, which is while BROKER_MACHINE_LOGIN_URL is set. It needs no credential: it is what a client reads before it holds one, and every word of it is already public in the broker’s own reference. Mounted only when the broker runs its OAuth provider, since with none it would name endpoints that are not there.
- Credential: None
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_metadata.go:87
Response body (JSON):
| Field | Type | Description |
|---|---|---|
issuer |
string |
The broker’s own issuer identifier (BROKER_PUBLIC_URL), which is the URL this document is published under and the audience every signed proof and request object names. RFC 8414 requires it. |
token_endpoint |
string |
The token endpoint: the device code grant, the refresh grant and the RFC 8693 token exchange. RFC 8414 requires it of a server that serves a grant using it, as all three do. |
device_authorization_endpoint |
string or absent |
The device authorization endpoint (RFC 8628), absent from a broker that serves no device grant, which is one started with no BROKER_MACHINE_LOGIN_URL: there is then no page for a person to type a code into, the route is not mounted, and grant_types_supported names no device code either. |
introspection_endpoint |
string |
The introspection endpoint (RFC 7662), where a resource server listed in BROKER_API_ACCOUNTS reads one of the broker’s access tokens back. |
revocation_endpoint |
string |
The revocation endpoint (RFC 7009), where a caller listed for that power ends one of the broker’s tokens. |
grant_types_supported |
array of strings |
The grant types this broker serves: always the refresh token grant and the RFC 8693 token exchange, and the RFC 8628 device code only while the broker serves the device grant. RFC 8414 makes the member optional with a default of the authorization code and implicit grants, neither of which this broker has, so leaving it out would say something untrue. |
response_types_supported |
array of strings |
The response types this broker supports, which is none: it has no authorization endpoint at all, since nobody signs in to it through a browser to get a token — a person’s part in a machine login is typing a code, and every other caller presents an identity it already holds. RFC 8414 requires the member, so it is written empty rather than omitted. |
token_endpoint_auth_methods_supported |
array of strings |
How a client authenticates at the token endpoint: “none” and nothing else (authMethodNone). |
dpop_signing_alg_values_supported |
array of strings |
The algorithms a DPoP proof (RFC 9449) may be signed with, the member RFC 9449 Section 5.1 defines. Every token this broker issues is DPoP-bound and a request with no acceptable proof gets none, so a client that cannot sign with one of these gets nothing at all. |
subject_token_types_supported |
array of strings |
The token types this broker’s RFC 8693 exchange takes as a subject token, which is what a caller puts in subject_token_type. RFC 8693 registers no metadata member for them and RFC 8414 names nothing of the kind, so this is the broker’s own member, named after RFC 8693’s own subject_token_type parameter and RFC 8414’s <thing>_supported convention. A caller that does not know it ignores it, as RFC 8414 says an unknown member must be; one that does learns which identities this deployment accepts without trying each in turn. |
POST /v1/enrollments
Section titled “POST /v1/enrollments”Enroll a session’s or pod’s signing key under the caller’s machine credential.
- Credential: Machine credential
- Success:
200,201 - Source:
packages/envoy/internal/broker/api/handlers_enroll.go:89
Request body (JSON):
| Field | Type | Description |
|---|---|---|
kind |
string |
What the session is: “session” (an agent session on a machine its person logged in, or a machine enrolling under the launcher credential its token exchange minted, which acts for that machine’s fleet rather than for a person) or “pod”. |
runtime_id |
string |
The session’s runtime: a pod’s UID, or whatever names the session on its machine (the helper’s host:pid:start time, a container’s id). A launcher enrolls one live session per runtime id (and slot). |
slot |
string |
A pod only, optional: which of the pod’s independent enrollments this is. |
operator |
string or null |
Optional: the operator the session runs for. The broker records the machine credential’s own operator whatever is sent, and refuses any other login. Absent or null for a pod. |
thumbprint |
string |
The RFC 7638 JWK thumbprint of the session’s P-256 signing key. |
session_id |
string or null |
Optional: the Envoy session the broker notifies when one of this session’s pending requests expires. |
pod_token |
string or null |
A pod only: its projected service-account token, which proves the runtime id is its UID. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
enrollment_id |
string |
The enrollment’s id, which the session’s Proof headers name. |
lease_expires_at |
string (RFC 3339 time) |
When the enrollment’s lease lapses unless the session renews it. |
slot |
string or null |
The pod slot it holds; null for every enrollment without one. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 409 | ALREADY_ENROLLED |
this runtime is already enrolled and live |
| 500 | INTERNAL |
create enrollment failed |
| 400 | INVALID_ENROLLMENT |
body must be one valid JSON object: (varies) |
| 400 | INVALID_KIND |
kind must be “session” or “pod” |
| 400 | INVALID_SLOT |
a slot is only for a pod enrollment and must match ^[a-z][a-z0-9-]{0,62}$ |
| 401 | LAUNCHER_INVALID |
the launcher credential is not valid |
| 403 | OPERATOR_MISMATCH |
this launcher credential enrols a different operator |
| 403 | POD_IDENTITY_MISMATCH |
the projected token does not identify this pod |
| 403 | POD_NOT_LAUNCHED |
this machine login does not launch the pods of that service account |
| 403 | POD_SELF_ENROLL_NO_SESSION |
a pod enrolling itself states no session name |
| 403 | POD_SELF_ENROLL_ONLY |
a pod’s own machine login enrols that pod alone |
| 403 | POD_TOKEN_EXPIRED |
the projected token has expired |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 403 | SESSION_HELD |
another identity holds this session name |
DELETE /v1/enrollments/{id}
Section titled “DELETE /v1/enrollments/{id}”Revoke an enrollment made under the caller’s machine credential’s operator, or a pod enrollment the caller’s own machine credential made.
- Credential: Machine credential
- Path parameters:
id - Success:
204, with no body - Source:
packages/envoy/internal/broker/api/handlers_enroll.go:171
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | ENROLLMENT_ID_INPUT |
enrollment ids are UUIDs |
| 500 | INTERNAL |
revoke enrollment failed |
| 403 | NOT_ENROLLING_CREDENTIAL |
only the machine login that made an enrollment carrying no operator may end it |
| 403 | OPERATOR_MISMATCH |
this launcher credential enrols a different operator |
POST /v1/launcher-credentials/token
Section titled “POST /v1/launcher-credentials/token”Log a service in with its own projected Kubernetes ServiceAccount token, which no person approves: answers its launcher credential’s id and expiry.
- Credential: None
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_launcher.go:59
Request body (JSON):
| Field | Type | Description |
|---|---|---|
token |
string |
The service’s projected Kubernetes ServiceAccount token, whose audience is the broker’s (BROKER_K8S_OIDC_AUDIENCE) and whose account BROKER_SERVICE_LOGINS names. |
request |
string |
A machine request object signed by the key the credential is bound to: a compact ES256 JWS carrying that public key, with one launcher_credential detail naming the account’s service. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
credential_id |
string |
The launcher credential’s id, which every launcher proof the key signs names. |
expires_at |
string (RFC 3339 time) |
When the credential expires: the token’s own expiry, at most an hour after the login. The service logs in again before then with its current token, and no person approves it. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 403 | ACCOUNT_NOT_LISTED |
this ServiceAccount does not log in as this service |
| 500 | INTERNAL |
token login failed |
| 400 | INVALID_REQUEST |
body must be one valid JSON object: (varies) |
| 409 | KEY_HOLDS_LIVE_CREDENTIAL |
this machine login’s key already holds a live launcher credential |
| 429 | RATE_LIMITED |
too many launcher credential requests; try again later too many token logins for this ServiceAccount; try again later |
| 400 | REQUEST_INVALID |
request object invalid |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 401 | TOKEN_EXPIRED |
the ServiceAccount token has expired |
| 401 | TOKEN_INVALID |
the ServiceAccount token does not verify |
| 404 | TOKEN_LOGIN_OFF |
this broker trusts no Kubernetes token issuer, so no service logs in with a token |
| 401 | TOKEN_TOO_LONG_LIVED |
the ServiceAccount token lives longer than an hour from its issue |
POST /v1/oauth/device_authorization
Section titled “POST /v1/oauth/device_authorization”Start a person’s machine login as an OAuth device authorization (RFC 8628): answers the device code the machine polls with and the code the person types. Rate-limited per source address and per person its login_hint names; mounted only when the broker runs its OAuth provider.
- Credential: None
- Source:
packages/envoy/internal/broker/api/handlers_oauth.go:27
POST /v1/oauth/token
Section titled “POST /v1/oauth/token”Exchange an approved device code, or a refresh token, for a DPoP-bound token pair naming the machine login’s launcher credential; mounted only when the broker runs its OAuth provider.
- Credential: None
- Source:
packages/envoy/internal/broker/api/handlers_oauth.go:96
POST /v1/oauth/introspect
Section titled “POST /v1/oauth/introspect”Read one of the broker’s access tokens back (RFC 7662): whether it is live and, when it is, the session, the person and the project it carries, and the key it is bound to. Mounted only when the broker runs its OAuth provider.
- Credential: Caller ServiceAccount token
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_introspect.go:35
Response body (JSON):
| Field | Type | Description |
|---|---|---|
active |
boolean |
Active is whether the token is live: one of the broker’s access tokens, not revoked, not expired. Everything else is set only when it is. |
session |
string or absent |
Session is the session the token’s writes are recorded as. |
person |
string or absent |
Person is the person the session acts for. |
service |
string or absent |
Service is the service a token that names no person was minted for: the fleet its machine belongs to, as BROKER_VM_SERVICES names it. A token names a person or a service and never both, so a reader that records who is answerable for a write reads Person, and one that decides what a machine may reach reads this. |
machine |
string or absent |
Machine is the machine a person signed in on acted through: the Google account of the actor token the exchange was made with, as BROKER_VM_SERVICES lists it. It is set only beside Person, and only for a token a person’s sign-in on a machine minted; every other token names no machine. The token’s own RFC 8693 ‘act’ claim records the same delegation as the actor token’s issuer spells it, which is a Google subject rather than an account, so this is the name a reader can act on. |
project_scope |
string or absent |
ProjectScope is what Project means: ProjectScopeNamed or ProjectScopePerson. A machine login’s own token, which no exchange decided for, carries neither this nor Project. |
project |
string or absent |
Project is the one project key a ProjectScopeNamed token may write. |
scope |
string or absent |
Scope is the OAuth scope the token was granted, space-separated as RFC 7662 spells it. |
exp |
number or absent |
ExpiresAt is when the token expires, in seconds since the epoch (RFC 7662’s ‘exp’), so a caller that remembers an affirmative answer can keep it no longer than the token lives. |
client_id |
string or absent |
ClientID is the OAuth client the token was issued to. |
jkt |
string or absent |
Thumbprint is the JWK thumbprint of the key the token is DPoP-bound to (RFC 9449’s ‘jkt’). It is what makes a stolen token useless: whoever reads this answer checks the caller’s proof against it. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 503 | DATABASE_UNAVAILABLE |
introspect token failed: a dependency the broker needs is unavailable |
| 400 | INVALID_REQUEST |
the form must parse and be at most … bytes |
| 400 | TOKEN_REQUIRED |
token is required |
POST /v1/oauth/revoke
Section titled “POST /v1/oauth/revoke”End one of the broker’s own access tokens (RFC 7009), an exchanged pod’s or workflow run’s included, which nothing else can end before its own expiry: the form names the token, the client it was issued to, and optionally which kind to look for first. It needs relaysDecisions rather than readsTokensBack because ending one of the broker’s tokens is the same authority class as ending a machine login or a grant, both already behind that power; a caller listed only to read a token back must not be able to end one. Mounted only when the broker runs its OAuth provider.
- Credential: Caller ServiceAccount token
- Source:
packages/envoy/internal/broker/api/handlers_revoke.go:26
POST /v1/oauth/exchange/pod-sessions
Section titled “POST /v1/oauth/exchange/pod-sessions”Register that one pod may exchange under one session name: the row the token exchange reads before it mints a token for a pod of an account that fixes no session, written by the deployment’s own daemon as it starts that pod. Mounted only when the broker runs its OAuth provider.
- Credential: Service login ServiceAccount token
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_pod_sessions.go:66
Request body (JSON):
| Field | Type | Description |
|---|---|---|
pod_token |
string |
The pod’s own projected Kubernetes ServiceAccount token, whose audience is the broker’s (BROKER_K8S_OIDC_AUDIENCE): the very token the registrar hands POST /v1/enrollments as it enrolls that pod. It is what names the pod — its account is the token’s subject and its uid the kubernetes.io pod uid — so a registrar cannot register a pod whose token it does not hold, and nothing is taken on the registrar’s word but the session name. |
session |
string |
The session name that pod may exchange under. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
pod_uid |
string |
The uid of the pod the registration is for, read from the token the body carried. |
session |
string |
The session name that pod may exchange under. |
expires_at |
string (RFC 3339 time) |
When the broker stops honouring the registration unless it is written again. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
register the pod’s session name failed |
| 400 | INVALID_REQUEST |
body must be one valid JSON object: (varies) |
| 403 | POD_ACCOUNT_NOT_LISTED |
exchange: no entry lists this identity |
| 403 | POD_CALLER_IS_THE_POD |
this is the caller’s own pod, … of …, and a registrar registers a pod other than the one it runs in |
| 400 | POD_NOT_BOUND |
exchange: the subject token is bound to no pod |
| 401 | POD_TOKEN_EXPIRED |
the pod’s ServiceAccount token has expired |
| 401 | POD_TOKEN_INVALID |
the pod’s ServiceAccount token does not verify |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 409 | SESSION_FIXED |
exchange: the entry fixes the session name it writes under |
| 400 | SESSION_INVALID |
exchange: the request names an unusable session |
POST /v1/enrollments/{id}/renew
Section titled “POST /v1/enrollments/{id}/renew”Extend the calling session’s lease.
- Credential: Renewal session proof
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_enroll.go:209
Request body (JSON):
| Field | Type | Description |
|---|---|---|
pod_token |
string or null |
A pod only: its current projected service-account token, which must name the pod UID and service account the enrollment was made for. Required once the enrollment has renewed with it (enroll.Service.Renew). |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
lease_expires_at |
string (RFC 3339 time) |
When the renewed lease lapses. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | ENROLLMENT_ID_INPUT |
enrollment ids are UUIDs |
| 401 | ENROLLMENT_NOT_LIVE |
enrollment is not live the machine login that enrolled this session has ended |
| 500 | INTERNAL |
renew enrollment failed |
| 400 | INVALID_RENEWAL |
body must be one valid JSON object: (varies) |
| 403 | NOT_YOURS |
a session may only renew its own enrollment |
| 403 | POD_IDENTITY_MISMATCH |
the projected token does not identify this pod |
| 403 | POD_TOKEN_EXPIRED |
the projected token has expired |
| 403 | POD_TOKEN_REQUIRED |
a pod enrollment’s renewal must carry its projected token |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
GET /v1/enrollments/self
Section titled “GET /v1/enrollments/self”Describe the calling session’s enrollment and its live grants.
- Credential: Session proof
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_enroll.go:251
Response body (JSON):
| Field | Type | Description |
|---|---|---|
enrollment_id |
string |
The session’s enrollment id. |
grants |
array of objects |
The session’s live grants, soonest to expire first. |
kind |
string |
“session” or “pod”. |
lease_expires_at |
string (RFC 3339 time) |
When the session’s lease lapses unless it is renewed. |
operator |
string or null |
The person the session runs for; null for a pod. |
slot |
string or null |
The pod slot it holds; null for every enrollment without one. |
Fields of grants[]:
| Field | Type | Description |
|---|---|---|
grant_id |
string |
The grant’s id, which the values and revoke routes take. |
request_id |
string |
The request it answered. |
approver |
string or null |
The login that approved it; null for a grant the policy gave automatically. |
expires_at |
string (RFC 3339 time) or null |
When it expires; null for a grant that lasts until its session ends. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
read enrollment failed read live grants failed |
| 401 | PROOF_INVALID |
enrollment is not live |
POST /v1/requests
Section titled “POST /v1/requests”Request secrets: the policy grants or denies them at once, or a person must approve them.
- Credential: Session proof
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_requests.go:72
Request body (JSON):
| Field | Type | Description |
|---|---|---|
request |
string |
The session’s signed request: a compact ES256 JWS, signed with the session’s key and addressed to the broker’s public URL, naming the secrets (authorization_details) and the reason. |
session_id |
string or null |
Optional: the Envoy session the broker notifies if this request expires undecided. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
request_id |
string |
The request’s id, which GET /v1/requests/{id} and its cancel route take. |
state |
string |
“granted”, “pending” (a person must decide it) or “denied”. |
secrets |
array of objects |
How the policy decided each name asked for. |
grant_id |
string or null |
Once granted, the grant to read the values from; null otherwise. |
record_id |
string or null |
The credential-request record the approver decides; null when nobody needs to. |
approver |
string or null |
Who decides it, as its record names them: a person’s login (their lowercase email), or “anyone” for a shared secret, which anyone signed in to the broker’s approval page may decide. While it is pending it is listed in that person’s Dispatch Inbox, or in everyone’s for “anyone”, linking approve_url. Null when the policy decided it at once, so no record names one. |
lifetime_seconds |
number or null |
How many seconds its grant lasts, as the signed request asked; or, when it asked for none, 12 hours; null when every name is tagged default-grant=session and the grant lasts until the session ends. A reused or coalesced request answers its own, which equals the one asked for. A client that asked for a lifetime and reads null, or no such field from a broker before it, was not given that lifetime. |
coalesced |
boolean or absent |
True when the request joined an identical one this session already had pending. |
detail |
string or null |
The broker’s one sentence about why it denied the request: the secret that refused it and the rule that did, as “GCLOUD_SAMI: requester-not-listed”, several joined with “; “. Null when the broker recorded none — every request it did not deny, and a denial whose rule it cannot name. A session reads it to learn that asking for that secret again will not help. |
approve_url |
string or absent |
While it is pending, its record’s page on the broker, <BROKER_PUBLIC_URL>/approve/<record>, where a person signed in to the broker decides it; absent when the policy decided it at once. |
Fields of secrets[]:
| Field | Type | Description |
|---|---|---|
name |
string |
The secret’s name. |
decision |
string |
“automatic”, “approval” or “deny”. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | GRANT_REQUIRES_LIFETIME |
a requested secret requires a shorter grant lifetime |
| 500 | INTERNAL |
create request failed |
| 400 | INVALID_REQUEST |
body must be one valid JSON object: (varies) |
| 400 | MIXED_APPROVERS |
the requested secrets need different approvers; request them separately |
| 401 | PROOF_INVALID |
enrollment is not live |
| 400 | REQUEST_INVALID |
request object invalid |
| 409 | REQUEST_PENDING |
a request for one or more of these secrets is already pending |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 400 | SESSION_ID_INPUT |
session id must be at most 128 characters with no whitespace or control character |
| 400 | UNKNOWN_SECRET |
no agent secret has this name |
GET /v1/requests/{id}
Section titled “GET /v1/requests/{id}”Read one of the calling session’s requests.
- Credential: Session proof
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_requests.go:245
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
“pending”, “granted”, “denied”, “expired” or “cancelled”. |
grant_id |
string or null |
Once granted, the grant to read the values from; null otherwise. |
record_id |
string or null |
The credential-request record the approver decides; null when nobody needs to. |
approver |
string or null |
Who decides it, as its record names them: a person’s login (their lowercase email), or “anyone” for a shared secret, which anyone signed in to the broker’s approval page may decide. The record fixes it when the request is made, so a later change to the secret’s owner does not move it, and it stays once the request is decided. Null when the policy decided it at once, so no record names one. |
lifetime_seconds |
number or null |
How many seconds its grant lasts: as the signed request asked, else 15 minutes when any name is human tier and 12 hours otherwise; null for a grant that lasts until the session ends (every name tagged default-grant=session). |
decided_at |
string (RFC 3339 time) or null |
When it left “pending”; null while pending. |
decision |
object or null |
Who decided it and when; null while pending, when the policy decided it at once, and when it expired. |
detail |
string or null |
The broker’s one sentence about why it denied the request, as POST /v1/requests answers it: the secret that refused it and the rule that did, as “GCLOUD_SAMI: requester-not-listed”. Null when the broker recorded none, a request it did not deny among them. |
approve_url |
string or absent |
While it is pending, its record’s page on the broker, <BROKER_PUBLIC_URL>/approve/<record>, where a person signed in to the broker decides it; absent once it is no longer pending, and when the policy decided it at once. |
Fields of decision:
| Field | Type | Description |
|---|---|---|
by |
string |
Who decided it: the approver’s login, “session:<enrollment id>” (the session cancelled it), “launcher:<credential id>” (its launcher ended the session, or its operator ended its machine login from one of their machines) or “broker” (the session’s lease lapsed). |
at |
string (RFC 3339 time) |
When it was decided. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
read request failed read request owner failed |
| 404 | NOT_FOUND |
no such request |
| 403 | NOT_YOURS |
this request belongs to another session |
| 400 | REQUEST_ID_INPUT |
request ids are UUIDs |
POST /v1/requests/{id}/cancel
Section titled “POST /v1/requests/{id}/cancel”Cancel one of the calling session’s pending requests.
- Credential: Session proof
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_requests.go:280
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
The state the call left the request, record or grant in: “denied”, “cancelled” or “revoked”. Every writer of this body states one of the three as a literal (a denial, a cancel, a grant’s revoke and a launcher credential’s). |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
cancel request failed |
| 404 | NOT_FOUND |
no such request |
| 403 | NOT_YOURS |
this request or grant belongs to another session |
| 400 | REQUEST_ID_INPUT |
request ids are UUIDs |
| 409 | REQUEST_TERMINAL |
request is already decided |
POST /v1/grants/{id}/values
Section titled “POST /v1/grants/{id}/values”Release a live grant’s values to the session that holds it.
- Credential: Session proof
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_requests.go:303
Request body (JSON):
| Field | Type | Description |
|---|---|---|
github_app |
object or null |
Optional: narrows the token of every github-app secret the grant holds to one account’s installation and, within it, to named repositories, to read access, or to both. A grant that holds no github-app secret refuses it (NARROWING_NOT_APPLICABLE), one whose github-app secret does not list the account refuses it (NARROWING_OWNER_NOT_LISTED), and one holding a name behind the airlock refuses it (NARROWING_BEHIND_AIRLOCK), since the proxy spends that grant’s values and mints them owner-wide. It narrows this release alone: the grant’s next release without it is owner-wide. |
Fields of github_app:
| Field | Type | Description |
|---|---|---|
owner |
string |
The one GitHub account whose installation the token is minted for: a login the secret’s github-owners lists, compared without regard to case. The answer names it lowercased. |
repositories |
array of strings or absent |
Optional: names of the account’s repositories, without the account, at least one and at most 100: the token reaches those repositories alone, but keeps every permission the App holds, its organization permissions among them, unless read_only is set too. The answer names them sorted, each once. |
read_only |
boolean or absent |
Optional: true limits the token to contents: read, metadata: read, pull_requests: read and actions: read, less the Actions grant the installation does not hold. A machine that must only read needs it, with or without repositories. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
expires_at |
string (RFC 3339 time) or null |
When the grant expires; the session can read the values again until then. Null for a grant that lasts until the session ends. |
values |
object of string values |
Each granted name’s value, read fresh from the secret store. |
github_app |
object or null or absent |
The narrowing this release applied to its GitHub App tokens, absent when it applied none. A client that asked for one and finds none here is answered by a broker from before narrowing, which released the owner-wide tokens. |
airlock |
object or null or absent |
How to reach the values this release did not hand over: the names the airlock holds back are not in “values” at all, and this says where the proxy that holds them is. Absent when no granted name is behind the airlock, which is every release of a broker with no rules file. |
Fields of github_app:
| Field | Type | Description |
|---|---|---|
owner |
string |
The one GitHub account whose installation the token is minted for: a login the secret’s github-owners lists, compared without regard to case. The answer names it lowercased. |
repositories |
array of strings or absent |
Optional: names of the account’s repositories, without the account, at least one and at most 100: the token reaches those repositories alone, but keeps every permission the App holds, its organization permissions among them, unless read_only is set too. The answer names them sorted, each once. |
read_only |
boolean or absent |
Optional: true limits the token to contents: read, metadata: read, pull_requests: read and actions: read, less the Actions grant the installation does not hold. A machine that must only read needs it, with or without repositories. |
Fields of airlock:
| Field | Type | Description |
|---|---|---|
proxy_url |
string |
The URL the command points HTTPS_PROXY at (BROKER_AIRLOCK_PROXY_URL). |
ca_certificate |
string |
The PEM certificate the command must trust to reach the proxy, which signs a certificate for each host it carries a request to. |
proxy_password |
string |
The password every proxy connection authenticates with: one of this grant’s own placeholders, the first (name, slot) pair in sorted order. |
names |
object of object values |
Each name the airlock holds back, by the name the command knows it as. |
Fields of airlock.names:
| Field | Type | Description |
|---|---|---|
value |
string |
What the command’s environment variable of that name holds: the released shape of the secret with a placeholder wherever its value would be. |
slots |
object of string values |
Each slot of the secret and the placeholder standing for it, so a command that takes one value of the secret apart knows which placeholder is which. |
hosts |
array of strings |
The hosts the secret’s airlock entry allows, which the command keeps off its NO_PROXY so their requests reach the proxy. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 403 | GRANT_CHAIN_INVALID |
this grant’s approval chain no longer verifies |
| 400 | GRANT_ID_INPUT |
grant ids are UUIDs |
| 403 | GRANT_NOT_LIVE |
grant is expired, revoked, or its session ended |
| 500 | INTERNAL |
read grant values failed |
| 400 | INVALID_NARROWING |
body must be one valid JSON object: (varies) github_app.… … |
| 502 | MINT_FAILED |
the broker could not mint a token from this secret |
| 409 | NARROWING_BEHIND_AIRLOCK |
this grant holds a name behind the airlock, which takes no narrowing |
| 400 | NARROWING_NOT_APPLICABLE |
this grant holds no github-app secret to narrow |
| 403 | NARROWING_OWNER_NOT_LISTED |
the narrowing names an account a github-app secret of this grant does not serve |
| 422 | NARROWING_REFUSED |
the narrowing cannot be served |
| 403 | NOT_YOURS |
this request or grant belongs to another session |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 404 | SECRET_NOT_IN_STORE |
secret is not in the secrets store |
POST /v1/grants/{id}/revoke
Section titled “POST /v1/grants/{id}/revoke”End a grant the calling session holds.
- Credential: Session proof
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_requests.go:390
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
The state the call left the request, record or grant in: “denied”, “cancelled” or “revoked”. Every writer of this body states one of the three as a literal (a denial, a cancel, a grant’s revoke and a launcher credential’s). |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | GRANT_ID_INPUT |
grant ids are UUIDs |
| 500 | INTERNAL |
revoke grant failed |
| 404 | NOT_FOUND |
no such grant |
| 403 | NOT_YOURS |
this request or grant belongs to another session |
GET /v1/pending
Section titled “GET /v1/pending”List the pending requests and machine logins the named person decides, each secret request with the broker’s own page where that person decides it.
- Credential: Caller ServiceAccount token
- Query parameters:
approver - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_pending.go:19
Response body (JSON):
| Field | Type | Description |
|---|---|---|
pending |
array of objects |
The undecided records the named person decides, newest first. |
Fields of pending[]:
| Field | Type | Description |
|---|---|---|
record_id |
string |
The credential-request record’s id, which the approval page and the machine login’s approve and deny routes take. |
kind |
string |
“agent_secret” (a secret request) or “launcher_credential” (a machine login). |
identifiers |
array of strings |
The secrets a secret request asks for, or the machine a machine login names. |
requested_at |
string (RFC 3339 time) |
When it was asked. |
session |
object |
The session this request names, read independently of each other (LEGION-587); see RecordSessionResp, this row’s exact shape here too. |
approve_url |
string or absent |
A secret request’s page on the broker, <BROKER_PUBLIC_URL>/approve/<record>, where a person signed in to the broker decides it; absent for a machine login, which is decided with its typed code. |
Fields of pending[].session:
| Field | Type | Description |
|---|---|---|
request |
string or null |
The id the request itself stated as an unsigned override in its body; null when it named none. |
enrollment |
string or null |
The id the requesting session’s own enrollment stated when it enrolled; null when it named none, or for a machine login, which has no requesting enrollment at all. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | APPROVER_REQUIRED |
approver is required |
| 500 | INTERNAL |
list pending requests failed |
POST /v1/credential-requests/{record}/approve
Section titled “POST /v1/credential-requests/{record}/approve”Approve a pending machine login as the named person, who must type its code: the broker takes the person’s name on the caller’s word, from a listed service’s ServiceAccount token, which the handler verifies itself — and whose name must carry relaysDecisions, the power this row declares — after it has read the record’s kind. It reads the kind first because a secret request is refused before any caller is looked at: it is decided only on the broker’s own page by a person signed in to the broker, so this answers 403 DECIDE_ON_BROKER_PAGE naming that page to every caller, token or none, writing nothing.
- Credential: Caller ServiceAccount token, checked by the handler
- Path parameters:
record - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_records.go:77
Request body (JSON):
| Field | Type | Description |
|---|---|---|
approver |
string |
The login of the person deciding, which the record’s rule must admit: its approver, or anyone signed in for the denial of a service’s machine login. |
code |
string or null |
The machine login’s confirmation code, typed again. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
credential_id |
string or null |
The machine credential the approval minted. |
state |
string |
“approved”: decideMachineLogin writes this body for an approval alone (handlers_ui_records.go), and a denial answers StateResponse instead. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 403 | ACCOUNT_LACKS_POWER |
the caller … may not call … |
| 403 | ACCOUNT_NOT_LISTED |
this account is not in BROKER_API_ACCOUNTS |
| 401 | ACCOUNT_TOKENS_OFF |
this broker trusts no token issuer for its API callers, so no caller authenticates with an identity token |
| 401 | ACCOUNT_TOKEN_EXPIRED |
the caller’s identity token has expired |
| 401 | ACCOUNT_TOKEN_INVALID |
the Google identity token names no verified account, so it names no caller (ask for it with format=full) the caller’s identity token does not verify the caller’s token names no issuer this broker trusts for its API callers |
| 401 | ACCOUNT_TOKEN_TOO_LONG_LIVED |
the caller’s identity token lives longer than an hour from its issue the caller’s identity token names no issue time (iat) |
| 400 | APPROVER_REQUIRED |
approver is required |
| 403 | CODE_MISMATCH |
confirmation code does not match |
| 400 | CODE_REQUIRED |
code is required to decide a machine login |
| 403 | DECIDE_ON_BROKER_PAGE |
a secret request is decided only on the secrets broker’s own page, by a person signed in to the broker: … |
| 500 | INTERNAL |
decide machine login failed read credential request kind failed |
| 400 | INVALID_DECISION |
body must be one valid JSON object: (varies) |
| 409 | KEY_HOLDS_LIVE_CREDENTIAL |
this machine login’s key already holds a live launcher credential |
| 403 | NOT_APPROVER |
only the record’s approver may decide it |
| 404 | NOT_FOUND |
no such credential request |
| 429 | RATE_LIMITED |
too many requests with a ServiceAccount token; try again later |
| 400 | RECORD_ID_INPUT |
record ids are lowercase-hex sha256 hashes |
| 409 | RECORD_TERMINAL |
this machine login expired before its approver acted on it this machine login has already been decided |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 400 | SERVICE_LOGS_IN_WITH_TOKEN |
a service logs in with its own projected Kubernetes ServiceAccount token at POST /v1/launcher-credentials/token, never through a code a person approves |
The DECIDE_ON_BROKER_PAGE refusal’s body (JSON):
| Field | Type | Description |
|---|---|---|
code |
string |
The refusal’s code. |
error |
string |
What was refused, naming the page. |
approve_url |
string |
The record’s page on the broker’s own approval page, <BROKER_PUBLIC_URL>/approve/<record>. |
POST /v1/credential-requests/{record}/deny
Section titled “POST /v1/credential-requests/{record}/deny”Deny a pending machine login as the named person, who must type its code, on the caller’s word and behind the same authentication and the same power as approve. A secret request is refused the same way, writing nothing, with 403 DECIDE_ON_BROKER_PAGE naming the page where a person signed in to the broker denies it.
- Credential: Caller ServiceAccount token, checked by the handler
- Path parameters:
record - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_records.go:81
Request body (JSON):
| Field | Type | Description |
|---|---|---|
approver |
string |
The login of the person deciding, which the record’s rule must admit: its approver, or anyone signed in for the denial of a service’s machine login. |
code |
string or null |
The machine login’s confirmation code, typed again. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
The state the call left the request, record or grant in: “denied”, “cancelled” or “revoked”. Every writer of this body states one of the three as a literal (a denial, a cancel, a grant’s revoke and a launcher credential’s). |
Refusals:
| Status | Code | Message |
|---|---|---|
| 403 | ACCOUNT_LACKS_POWER |
the caller … may not call … |
| 403 | ACCOUNT_NOT_LISTED |
this account is not in BROKER_API_ACCOUNTS |
| 401 | ACCOUNT_TOKENS_OFF |
this broker trusts no token issuer for its API callers, so no caller authenticates with an identity token |
| 401 | ACCOUNT_TOKEN_EXPIRED |
the caller’s identity token has expired |
| 401 | ACCOUNT_TOKEN_INVALID |
the Google identity token names no verified account, so it names no caller (ask for it with format=full) the caller’s identity token does not verify the caller’s token names no issuer this broker trusts for its API callers |
| 401 | ACCOUNT_TOKEN_TOO_LONG_LIVED |
the caller’s identity token lives longer than an hour from its issue the caller’s identity token names no issue time (iat) |
| 400 | APPROVER_REQUIRED |
approver is required |
| 403 | CODE_MISMATCH |
confirmation code does not match |
| 400 | CODE_REQUIRED |
code is required to decide a machine login |
| 403 | DECIDE_ON_BROKER_PAGE |
a secret request is decided only on the secrets broker’s own page, by a person signed in to the broker: … |
| 500 | INTERNAL |
decide machine login failed read credential request kind failed |
| 400 | INVALID_DECISION |
body must be one valid JSON object: (varies) |
| 409 | KEY_HOLDS_LIVE_CREDENTIAL |
this machine login’s key already holds a live launcher credential |
| 403 | NOT_APPROVER |
only the record’s approver may decide it |
| 404 | NOT_FOUND |
no such credential request |
| 429 | RATE_LIMITED |
too many requests with a ServiceAccount token; try again later |
| 400 | RECORD_ID_INPUT |
record ids are lowercase-hex sha256 hashes |
| 409 | RECORD_TERMINAL |
this machine login expired before its approver acted on it this machine login has already been decided |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 400 | SERVICE_LOGS_IN_WITH_TOKEN |
a service logs in with its own projected Kubernetes ServiceAccount token at POST /v1/launcher-credentials/token, never through a code a person approves |
The DECIDE_ON_BROKER_PAGE refusal’s body (JSON):
| Field | Type | Description |
|---|---|---|
code |
string |
The refusal’s code. |
error |
string |
What was refused, naming the page. |
approve_url |
string |
The record’s page on the broker’s own approval page, <BROKER_PUBLIC_URL>/approve/<record>. |
POST /v1/machine-logins/lookup
Section titled “POST /v1/machine-logins/lookup”Find a pending machine login by the confirmation code its machine shows.
- Credential: Caller ServiceAccount token
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_machinelogin.go:30
Request body (JSON):
| Field | Type | Description |
|---|---|---|
code |
string |
The XXXX-XXXX confirmation code the machine shows, as the approver typed it. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
record_id |
string |
The record’s id: the SHA-256 of its text. |
kind |
string |
“agent_secret” (a secret request) or “launcher_credential” (a machine login). |
state |
string |
“pending”, or how it ended: “approved”, “denied”, “expired”, “cancelled” or “revoked”. |
approver |
string |
The one login that may decide it, or “anyone”: any signed-in person may decide a request for a shared human-tier secret, and may deny a service’s machine login (which only a broker before token logins opened, and which nobody approves any more), whatever person it names. |
enrollment |
object or null |
The session asking; null for a machine login. |
session |
object |
The session this record names, read independently of each other; null fields where that id was never set (LEGION-587). |
identifiers |
array of strings |
The secrets asked for, or the machine logging in. |
service |
string or null |
The service a machine login is for, when it logs a service in rather than a person’s machine; null otherwise. |
reason |
string |
The reason the session gave, verbatim. |
lifetime_seconds |
number or null |
How many seconds the grant or credential lasts once approved; null for a grant that lasts until its session ends. A machine login’s credential always names one. |
rules_version |
string |
The version of the secret policy it was decided under (the SHA-256 of every agent secret’s name, owner, tier and ARN); for a machine login, the version current when it was asked. |
expires_at |
string (RFC 3339 time) |
When it expires undecided. |
requested_at |
string (RFC 3339 time) |
When it was asked. |
decided |
object or null |
How it was decided; null while pending. |
Fields of enrollment:
| Field | Type | Description |
|---|---|---|
kind |
string |
“session” (an agent session on a machine its person logged in) or “pod”. |
runtime_id |
string |
The session’s runtime: whatever names the session on its machine (the helper’s host:pid:start time, a container’s id), or a pod’s UID. |
operator |
string |
The person the session runs for; empty for a pod. |
slot |
string or null |
The pod slot it holds; null for every enrollment without one. |
Fields of session:
| Field | Type | Description |
|---|---|---|
request |
string or null |
The id the request itself stated as an unsigned override in its body; null when it named none. |
enrollment |
string or null |
The id the requesting session’s own enrollment stated when it enrolled; null when it named none, or for a machine login, which has no requesting enrollment at all. |
Fields of decided:
| Field | Type | Description |
|---|---|---|
event |
string |
“approved”, “denied”, “expired” or “cancelled”. |
at |
string (RFC 3339 time) |
When it was decided. |
credential_id |
string or null |
The machine credential an approved machine login minted; null otherwise. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | CODE_REQUIRED |
code is required |
| 500 | INTERNAL |
lookup machine login failed read credential request failed |
| 400 | INVALID_LOOKUP |
body must be one valid JSON object: (varies) |
| 404 | NO_SUCH_CODE |
no pending machine login has this code |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
GET /v1/launcher-credentials
Section titled “GET /v1/launcher-credentials”List the machine logins the named person may revoke, every service’s, such as the Legion daemon’s, whoever approved it, and the person’s own machines’, that are not revoked and are unexpired or expired with a session still running, each naming who approved it. The person is the one the caller names.
- Credential: Caller ServiceAccount token
- Query parameters:
approver - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_machinelogin.go:64
Response body (JSON):
| Field | Type | Description |
|---|---|---|
credentials |
array of objects |
The machine logins that are not revoked and are either unexpired or expired with a session still running, newest first: every service’s and the named person’s own machines’ on Dispatch’s page, the calling operator’s own machines’ alone on the operator route. |
Fields of credentials[]:
| Field | Type | Description |
|---|---|---|
credential_id |
string |
The launcher credential’s id, which the revoke route takes. |
host |
string |
The machine’s host name, as a person’s machine login named it; for a token login, its ServiceAccount’s <namespace>/<name>, or a machine’s <project>/<zone>/<instance name>. |
service |
string or null |
The service a service’s login is for (legion-daemon), whose sessions are its pods, or the fleet a machine belongs to; null for a person’s own machine. |
subject |
string or null |
What a token login proved: its ServiceAccount (system:serviceaccount:<namespace>:<name>), or a machine as the token exchange names it (exchange.MachineSubject); null for every login a person approved. |
approved_by |
string |
The login of the person who approved the login (anyone signed in may have approved a service’s login before token logins); “” for a service’s token login, which no person approves. |
issued_at |
string (RFC 3339 time) |
When the login was approved, or a token login’s first login. |
expires_at |
string (RFC 3339 time) |
When the credential expires. Each refresh of a person’s device login moves it to BROKER_LAUNCHER_CREDENTIAL_SECONDS from then; a login through the earlier route never refreshes; a token login moves it later at each login, at most an hour away. |
expired |
boolean |
True once expires_at has passed: the login enrolls nothing more, but sessions it enrolled still run (each renews with its own key), and revoking it ends them. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | APPROVER_REQUIRED |
approver is required |
| 500 | INTERNAL |
list launcher credentials failed |
POST /v1/launcher-credentials/{id}/revoke-by-approver
Section titled “POST /v1/launcher-credentials/{id}/revoke-by-approver”End a machine login, expired or not: a service’s (its token login, or one a person approved before token logins) as anyone signed in, a person’s machine’s as the person who approved it. Its launcher proofs stop authenticating and every session it enrolled, pods included, ends with its grants and pending requests. The revoking person is the login the request names, which the broker still takes on the caller’s word: a revoke only takes power away.
- Credential: Caller ServiceAccount token
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_machinelogin.go:96
Request body (JSON):
| Field | Type | Description |
|---|---|---|
approver |
string |
The login of the person revoking: for a grant, its approver or its session’s operator; for a person’s machine login, the person who approved it; for a service’s machine login, anyone signed in. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
The state the call left the request, record or grant in: “denied”, “cancelled” or “revoked”. Every writer of this body states one of the three as a literal (a denial, a cancel, a grant’s revoke and a launcher credential’s). |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | APPROVER_REQUIRED |
approver is required |
| 400 | CREDENTIAL_ID_INPUT |
launcher credential ids are UUIDs |
| 500 | INTERNAL |
revoke launcher credential failed |
| 400 | INVALID_REVOKE |
body must be one valid JSON object: (varies) |
| 403 | NOT_APPROVER |
only the person who approved a person’s machine login may revoke it |
| 404 | NOT_FOUND |
no such machine login |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
GET /v1/grants
Section titled “GET /v1/grants”List the live grants of the named person’s sessions, automatic or approved, and those the person approved. The person is the one the caller names.
- Credential: Caller ServiceAccount token
- Query parameters:
approver - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_grants.go:26
Response body (JSON):
| Field | Type | Description |
|---|---|---|
grants |
array of objects |
The live grants of the named person’s sessions, and those the person approved, newest first. |
Fields of grants[]:
| Field | Type | Description |
|---|---|---|
grant_id |
string |
The grant’s id, which the revoke route takes. |
granted |
string |
How the session got it: “automatic” (the policy gave it without asking) or “approval”. GrantsForApprover writes one of the two (machine_read.go, policy.Automatic and policy.Approval), so it is a closed set. |
record_id |
string or null |
The credential-request record its approval rests on; null for an automatic grant. |
enrollment |
object |
The session holding it. |
names |
array of strings |
The secrets it covers. |
approver |
string or null |
The person who approved it; null for an automatic grant. It is who the grant belongs to, which is what the revoke rule reads (requests.Machine.RevokeByApprover’s mayRevoke), and for a grant the model judge approved it is the record’s own approver — the owner, or “anyone” for a shared secret — in whose stead the judge decided. Who decided it is DecidedBy, which for such a grant is the judge and not this person. |
decided_by |
string or null |
Who decided the request this grant rests on (requests.decided_by): the approving person’s own login, or “judge:<model id>” where the model judge approved it. Null for an automatic grant, which nobody decided. A page that says who decided a grant says this, so that a judged grant names the judge rather than a person who never saw the request. |
expires_at |
string (RFC 3339 time) or null |
When it expires; null for a grant that lasts until its session ends. |
created_at |
string (RFC 3339 time) |
When it was granted. |
Fields of grants[].enrollment:
| Field | Type | Description |
|---|---|---|
kind |
string |
“session” (an agent session on a machine its person logged in) or “pod”. |
runtime_id |
string |
The session’s runtime: whatever names the session on its machine (the helper’s host:pid:start time, a container’s id), or a pod’s UID. |
operator |
string |
The person the session runs for; empty for a pod. |
slot |
string or null |
The pod slot it holds; null for every enrollment without one. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | APPROVER_REQUIRED |
approver is required |
| 500 | INTERNAL |
list grants for approver failed |
POST /v1/grants/{id}/revoke-by-approver
Section titled “POST /v1/grants/{id}/revoke-by-approver”End a grant as its approver or as its enrollment’s operator. The operator’s revoke also withholds the secrets the grant’s request got automatically from that session, even when the grant had already ended: its other grants that got them automatically end too, and it asks before it gets them again. The revoking person is the login the request names, which the broker still takes on the caller’s word: a revoke only takes power away.
- Credential: Caller ServiceAccount token
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_grants.go:73
Request body (JSON):
| Field | Type | Description |
|---|---|---|
approver |
string |
The login of the person revoking: for a grant, its approver or its session’s operator; for a person’s machine login, the person who approved it; for a service’s machine login, anyone signed in. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
The state the call left the request, record or grant in: “denied”, “cancelled” or “revoked”. Every writer of this body states one of the three as a literal (a denial, a cancel, a grant’s revoke and a launcher credential’s). |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | APPROVER_REQUIRED |
approver is required |
| 400 | GRANT_ID_INPUT |
grant ids are UUIDs |
| 500 | INTERNAL |
revoke grant failed |
| 400 | INVALID_REVOKE |
body must be one valid JSON object: (varies) |
| 403 | NOT_APPROVER |
only the grant’s approver or its enrollment’s operator may revoke it |
| 404 | NOT_FOUND |
no such grant |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
GET /v1/person-signins
Section titled “GET /v1/person-signins”List the sign-ins the named person made on their machines with agent-secrets login, by machine: one row per box key they signed in with and have not revoked, each naming the machine’s Google account, the box key’s thumbprint, and when the sign-in was made and last used. The person is the one the caller names, as it is for every list Dispatch’s server relays.
- Credential: Caller ServiceAccount token
- Query parameters:
person - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_personsignin.go:36
Response body (JSON):
| Field | Type | Description |
|---|---|---|
signins |
array of objects |
The named person’s live sign-ins, newest first: one per box key they signed in with and have not revoked. A revoked sign-in is not listed, and neither is anyone else’s. |
Fields of signins[]:
| Field | Type | Description |
|---|---|---|
signin_id |
string |
The sign-in’s id, which the revoke route takes. |
machine |
string |
The Google account of the machine the person signed in on, as BROKER_VM_SERVICES lists it: the fleet’s account, which is what names the box on the person’s own list. |
thumbprint |
string |
The RFC 7638 thumbprint of the box key the sign-in is bound to, which is what tells this sign-in from the person’s others on the same fleet. It is public: it is what the box’s DPoP proofs already carry. |
created_at |
string (RFC 3339 time) |
When the person first signed in on that box. |
last_used_at |
string (RFC 3339 time) |
When that box last traded a token under this sign-in. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
list person sign-ins failed |
| 400 | PERSON_REQUIRED |
person is required |
POST /v1/person-signins/{id}/revoke
Section titled “POST /v1/person-signins/{id}/revoke”End one of the named person’s sign-ins: that box key trades no token at the exchange again, and every access token already minted on that box under that sign-in ends in the revoke’s own transaction, so none of them outlives it. The person’s sign-ins on their other boxes keep working, and the box signs in again with a key of its own. The revoking person is the one the request names, which the broker takes on the caller’s word: a revoke only takes power away.
- Credential: Caller ServiceAccount token
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_ui_personsignin.go:66
Request body (JSON):
| Field | Type | Description |
|---|---|---|
person |
string |
The login of the person whose sign-in it is. A sign-in of anyone else is answered as an unknown id. |
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
Always “revoked”: the sign-in no longer trades, whether this call ended it or an earlier one did. |
tokens_ended |
number |
How many of that person’s live access tokens this call ended with it, the ones minted on that box under that sign-in. It is 0 for a sign-in revoked already, and for one whose tokens had all expired. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
revoke person sign-in failed |
| 400 | INVALID_REVOKE |
body must be one valid JSON object: (varies) |
| 404 | NOT_FOUND |
no sign-in of that person has that id |
| 400 | PERSON_REQUIRED |
person is required |
| 413 | REQUEST_TOO_LARGE |
request body exceeds … bytes |
| 400 | SIGNIN_ID_INPUT |
sign-in ids are UUIDs |
GET /v1/grant-log
Section titled “GET /v1/grant-log”Read the broker’s grant log for one closed period, for a quarterly access review: every audit row between ?since and ?until (RFC 3339, half-open), oldest first, each naming the session that wrote it, that session’s person, the service the broker verified, the secrets asked for and their decisions, the state, the approver and who decided. ?after resumes after an id and ?limit bounds the page, which is a thousand rows at most. ?until must be at least an hour before now, so that every row of the period has committed before the review reads it. It is the one route readsGrantLog reaches: the review reads the broker’s record and changes nothing in it.
- Credential: Caller ServiceAccount token
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_grantlog.go:59
Response body (JSON):
| Field | Type | Description |
|---|---|---|
events |
array of objects |
The period’s audit rows this page holds, oldest first (audit id order, which is write order), each as store.GrantLogEvent’s own JSON tags name it; an empty array when the page holds none. |
next_after |
number |
The id to send as ?after= for the next page: the last event’s id, or the request’s own ?after= when this page is empty, so a caller that has reached the end of the period and asks again reads the same empty page rather than starting the period over. |
Fields of events[]:
| Field | Type | Description |
|---|---|---|
id |
number |
ID is the audit row’s own id, which is also the paging cursor: GrantLogQuery.After. |
at |
string (RFC 3339 time) |
At is when the row was written. |
kind |
string |
Kind is the audit row’s kind, request.created and grant.revoked among them. |
actor |
string |
Actor is who the broker recorded as having done it: session:<enrollment id>, human:<login>, launcher:<credential id>, or the broker itself. |
enrollment_id |
string |
EnrollmentID is the enrollment the row names, empty for a row that names none (every launcher_credential.* kind). |
request_id |
string |
RequestID is the request the row names, empty for a row that names none (a grant.moved row, and every enrollment and launcher-credential kind). |
grant_id |
string |
GrantID is the grant the row names, empty for a row that names none. |
session |
object |
Session is the enrollment’s own identity: its kind, its session id and its runtime id. |
person |
string |
Person is the operator the enrollment acts for (enrollments.operator), empty for a pod and for a machine’s own session. |
service |
string |
Service is the service the broker VERIFIED for the request, read from the request’s own request.created audit row: the service account the enrollment proved still named this service when the request was decided. It is what the policy decided under. |
claimed_service |
string |
ClaimedService is the service the enrollment’s launcher credential claims (launcher_credentials.service), which the policy does NOT trust: a credential may name a service whose account no longer proves it, and then Service is empty while this is not. The review reads the pair to see a claim the broker refused. |
secrets |
array of objects |
Secrets are the request’s secrets and each one’s decision, in name order; empty for a row that names no request. |
state |
string |
State is the request’s state now: pending, granted, denied, cancelled or expired. |
approver |
string |
Approver is the login the request’s record allows to decide it (requests.allowed_approver), or record.AnyoneApprover for a shared secret; empty when the policy decided the request itself. |
decided_by |
string |
DecidedBy is who the request records as having decided it: the approving login, the broker for an expiry, requests.PolicyDeniedActor for a denial the policy made under an approval. |
lifetime_seconds |
number |
LifetimeSeconds is how long the request’s grant lasts; zero when the request records none, which is a grant that lasts until its session ends. |
reason |
string |
Reason is the reason the session signed into its request object, the sentence a person reads on the approval page. |
detail |
string |
Detail is the request’s decision_detail: why it was denied, or why an approval granted nothing. |
raw |
any JSON value |
Raw is the audit row’s own detail object, verbatim, so a kind this type says nothing about is still readable. |
Fields of events[].session:
| Field | Type | Description |
|---|---|---|
kind |
string |
Kind is the enrollment’s kind, session or pod. |
session_id |
string |
SessionID is the Envoy session the enrollment recorded, empty for an enrollment that names none (a pod enrolled before its session had a name, say). |
runtime_id |
string |
RuntimeID is what the enrollment proved it runs on: the box’s id for a session, the pod’s UID for a pod. |
Fields of events[].secrets[]:
| Field | Type | Description |
|---|---|---|
name |
string |
Name is the secret’s name as the session asked for it. |
decision |
string |
Decision is the request_secrets row’s decision: automatic, approval or deny. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
read the grant log failed |
| 400 | PAGE_INVALID |
after is not a whole number of zero or more limit is not a whole number of zero or more |
| 400 | PERIOD_INVALID |
since is not an RFC 3339 time, such as 2026-01-01T00:00:00Z until is not an RFC 3339 time, such as 2026-01-01T00:00:00Z |
| 400 | PERIOD_NOT_CLOSED |
until must be at least an hour before now, so that every row of the period has committed before the review reads it |
| 400 | PERIOD_REQUIRED |
since is required: an RFC 3339 time, such as 2026-01-01T00:00:00Z until is required: an RFC 3339 time, such as 2026-01-01T00:00:00Z |
GET /v1/operator/machines
Section titled “GET /v1/operator/machines”List the logins of the calling credential’s operator’s own machines, as Dispatch’s machine-logins page lists them, without the service logins that page also lists.
- Credential: Machine credential
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_operator.go:33
Response body (JSON):
| Field | Type | Description |
|---|---|---|
credentials |
array of objects |
The machine logins that are not revoked and are either unexpired or expired with a session still running, newest first: every service’s and the named person’s own machines’ on Dispatch’s page, the calling operator’s own machines’ alone on the operator route. |
Fields of credentials[]:
| Field | Type | Description |
|---|---|---|
credential_id |
string |
The launcher credential’s id, which the revoke route takes. |
host |
string |
The machine’s host name, as a person’s machine login named it; for a token login, its ServiceAccount’s <namespace>/<name>, or a machine’s <project>/<zone>/<instance name>. |
service |
string or null |
The service a service’s login is for (legion-daemon), whose sessions are its pods, or the fleet a machine belongs to; null for a person’s own machine. |
subject |
string or null |
What a token login proved: its ServiceAccount (system:serviceaccount:<namespace>:<name>), or a machine as the token exchange names it (exchange.MachineSubject); null for every login a person approved. |
approved_by |
string |
The login of the person who approved the login (anyone signed in may have approved a service’s login before token logins); “” for a service’s token login, which no person approves. |
issued_at |
string (RFC 3339 time) |
When the login was approved, or a token login’s first login. |
expires_at |
string (RFC 3339 time) |
When the credential expires. Each refresh of a person’s device login moves it to BROKER_LAUNCHER_CREDENTIAL_SECONDS from then; a login through the earlier route never refreshes; a token login moves it later at each login, at most an hour away. |
expired |
boolean |
True once expires_at has passed: the login enrolls nothing more, but sessions it enrolled still run (each renews with its own key), and revoking it ends them. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
list launcher credentials failed |
| 403 | SERVICE_CREDENTIAL |
a service’s machine login has no operator; machine and grant commands are a person’s |
POST /v1/operator/machines/{id}/revoke
Section titled “POST /v1/operator/machines/{id}/revoke”End one of the operator’s own machines’ logins: no proof signed with it authenticates again and every session it enrolled ends. A service’s login is not found here, like an unknown id. Revoking the calling machine’s own login ends this machine’s access.
- Credential: Machine credential
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_operator.go:45
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
The state the call left the request, record or grant in: “denied”, “cancelled” or “revoked”. Every writer of this body states one of the three as a literal (a denial, a cancel, a grant’s revoke and a launcher credential’s). |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | CREDENTIAL_ID_INPUT |
launcher credential ids are UUIDs |
| 500 | INTERNAL |
revoke launcher credential failed |
| 403 | NOT_APPROVER |
only the person who approved a person’s machine login may revoke it |
| 404 | NOT_FOUND |
no such machine login |
| 403 | SERVICE_CREDENTIAL |
a service’s machine login has no operator; machine and grant commands are a person’s |
POST /v1/operator/machines/{id}/retire
Section titled “POST /v1/operator/machines/{id}/retire”Retire the calling machine login after its machine logged in again: it enrolls nothing more and cannot refresh, while the sessions it enrolled keep running and renewing with their own keys. {id} must be the calling credential.
- Credential: Machine credential
- Path parameters:
id - Success:
204, with no body - Source:
packages/envoy/internal/broker/api/handlers_operator.go:62
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | CREDENTIAL_ID_INPUT |
launcher credential ids are UUIDs |
| 500 | INTERNAL |
retire launcher credential failed |
| 403 | NOT_CALLING_CREDENTIAL |
a machine login retires only itself: sign with the credential the path names |
GET /v1/operator/grants
Section titled “GET /v1/operator/grants”List the live grants of the operator’s sessions and those the operator approved, exactly as Dispatch’s Live grants page lists them.
- Credential: Machine credential
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_operator.go:80
Response body (JSON):
| Field | Type | Description |
|---|---|---|
grants |
array of objects |
The live grants of the named person’s sessions, and those the person approved, newest first. |
Fields of grants[]:
| Field | Type | Description |
|---|---|---|
grant_id |
string |
The grant’s id, which the revoke route takes. |
granted |
string |
How the session got it: “automatic” (the policy gave it without asking) or “approval”. GrantsForApprover writes one of the two (machine_read.go, policy.Automatic and policy.Approval), so it is a closed set. |
record_id |
string or null |
The credential-request record its approval rests on; null for an automatic grant. |
enrollment |
object |
The session holding it. |
names |
array of strings |
The secrets it covers. |
approver |
string or null |
The person who approved it; null for an automatic grant. It is who the grant belongs to, which is what the revoke rule reads (requests.Machine.RevokeByApprover’s mayRevoke), and for a grant the model judge approved it is the record’s own approver — the owner, or “anyone” for a shared secret — in whose stead the judge decided. Who decided it is DecidedBy, which for such a grant is the judge and not this person. |
decided_by |
string or null |
Who decided the request this grant rests on (requests.decided_by): the approving person’s own login, or “judge:<model id>” where the model judge approved it. Null for an automatic grant, which nobody decided. A page that says who decided a grant says this, so that a judged grant names the judge rather than a person who never saw the request. |
expires_at |
string (RFC 3339 time) or null |
When it expires; null for a grant that lasts until its session ends. |
created_at |
string (RFC 3339 time) |
When it was granted. |
Fields of grants[].enrollment:
| Field | Type | Description |
|---|---|---|
kind |
string |
“session” (an agent session on a machine its person logged in) or “pod”. |
runtime_id |
string |
The session’s runtime: whatever names the session on its machine (the helper’s host:pid:start time, a container’s id), or a pod’s UID. |
operator |
string |
The person the session runs for; empty for a pod. |
slot |
string or null |
The pod slot it holds; null for every enrollment without one. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
list grants for approver failed |
| 403 | SERVICE_CREDENTIAL |
a service’s machine login has no operator; machine and grant commands are a person’s |
POST /v1/operator/grants/{id}/revoke
Section titled “POST /v1/operator/grants/{id}/revoke”End a grant as the operator: the revocation Dispatch’s page offers, which also withholds an automatic grant’s secrets from its session.
- Credential: Machine credential
- Path parameters:
id - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_operator.go:92
Response body (JSON):
| Field | Type | Description |
|---|---|---|
state |
string |
The state the call left the request, record or grant in: “denied”, “cancelled” or “revoked”. Every writer of this body states one of the three as a literal (a denial, a cancel, a grant’s revoke and a launcher credential’s). |
Refusals:
| Status | Code | Message |
|---|---|---|
| 400 | GRANT_ID_INPUT |
grant ids are UUIDs |
| 500 | INTERNAL |
revoke grant failed |
| 403 | NOT_APPROVER |
only the grant’s approver or its enrollment’s operator may revoke it |
| 404 | NOT_FOUND |
no such grant |
| 403 | SERVICE_CREDENTIAL |
a service’s machine login has no operator; machine and grant commands are a person’s |
GET /v1/settings
Section titled “GET /v1/settings”The namespace prefix, AWS account and region, and the agent-secrets key the broker serves: what the CLI needs so it and the broker never disagree about which secret a name means.
- Credential: None
- Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_secrets.go:42
Response body (JSON):
| Field | Type | Description |
|---|---|---|
store |
string |
Which secret store holds the agent secrets: aws-secrets-manager or gcp-secret-manager. A broker from before this field answered none, and held them in AWS Secrets Manager. |
secrets_prefix |
string |
The name prefix every agent secret sits under: with aws-secrets-manager a Secrets Manager name prefix ending in “/”, with gcp-secret-manager projects/<project number>/secrets/<secret-id prefix>-. The secret a session asks for as DEEL_API_KEY is <prefix>deel-api-key. |
kms_key_arn |
string or absent |
With aws-secrets-manager, the ARN of the agent-secrets KMS key, which every agent secret must be encrypted with; absent otherwise. |
aws_account_id |
string or absent |
With aws-secrets-manager, the AWS account the agent-secrets key, and so every agent secret, is in; absent otherwise. |
aws_region |
string or absent |
With aws-secrets-manager, the AWS region the agent-secrets key, and so every agent secret, is in; absent otherwise. |
kms_key_name |
string or absent |
With gcp-secret-manager, the resource name of the agent-secrets Cloud KMS key, which every agent secret must be encrypted with; absent otherwise. |
gcp_project |
string or absent |
With gcp-secret-manager, the number of the project every agent secret is in, the one the prefix names; absent otherwise. |
POST /v1/secrets/{name}/reread
Section titled “POST /v1/secrets/{name}/reread”Reread one secret from Secrets Manager now, as whoever just wrote it may ask: the change is served, or gone, within seconds instead of at the next five-minute reload. Open to any caller, and rate-limited both per source address and over every caller at once, since each reread holds the policy’s writer lock while it reads; a reread only moves the broker toward AWS’s own truth.
- Credential: None
- Path parameters:
name - Success:
200 - Source:
packages/envoy/internal/broker/api/handlers_secrets.go:82
Response body (JSON):
| Field | Type | Description |
|---|---|---|
name |
string |
The name reread, as the path gave it. |
served |
boolean |
True when the broker now serves the secret. |
reason |
string or absent |
Why the broker does not serve it, absent when it does: “absent” (no secret under the prefix carries the name, or the one that does is scheduled for deletion) or the reason the broker refused the secret, the one its policy-refused log line names (such as owner-tag-missing or no-current-value). |
kind |
string or absent |
The kind the broker serves the secret as, while it serves it: github-app or google-login, or absent for a stored secret, whose grants release its stored value. A person who retags a secret to a kind reads it here before storing the key the kind mints from: a stale store read can still serve the secret as stored. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 500 | INTERNAL |
reread secret failed |
| 429 | RATE_LIMITED |
too many secret rereads; try again later |
| 503 | REQUEST_ENDED |
the request ended before the reread finished; send it again |
| 400 | SECRET_NAME_INVALID |
not an agent secret name |
GET /approve/{record}
Section titled “GET /approve/{record}”A person’s approval page for one credential-request record, an HTML page on the broker’s own origin: the record’s secret names, the enrollment the broker verified asked, the session the request claims when it differs, the reason (empty only on a record an older build wrote before the broker required one wherever a person decides), the lifetime, the times, the approver and the state, one sentence saying what Approve gives and for how long and one saying what Deny and the expiry do, then every other pending secret request the person may decide, with Approve and Deny for each the person may decide and one “Approve all N shown”. It checks the id’s shape, then the session, then the record: a malformed id is a 400, a request with no live session, or with ?switch=1, redirects to the sign-in provider and returns here, and only a signed-in person’s unknown record id is a 404. A machine login’s record is printed with the note that it is decided by typing its code on Dispatch’s machine-login page, and offers no button.
- Credential: Broker sign-in session
- Path parameters:
record - Query parameters:
decided,switch - Answers (an HTML page, or a redirect):
200,302,400,404,500,503 - Source:
packages/envoy/internal/broker/approval/page.go:113
POST /approve
Section titled “POST /approve”Decide records an approval page printed, as the signed-in person: a form from the broker’s own origin only, carrying the page’s token, which binds it to the session and to exactly the records the page printed with a button, for an hour. Each record is decided on its own, and the answer is a 303 back to the page, which says what became of each.
- Credential: Broker sign-in session
- Answers (an HTML page, or a redirect):
200,303,400,403,500 - Source:
packages/envoy/internal/broker/approval/page.go:413
GET /signin/callback
Section titled “GET /signin/callback”Finish a sign-in to the approval page, the provider’s redirect back with a code, checked against the sign-in this browser started (its __Host-broker-signin cookie, used once): state, PKCE, the ID token and the Workspace domain. It makes a new session, ending the one the browser held, and answers a 303 to the record’s page the sign-in started from.
- Credential: Broker sign-in session
- Answers (an HTML page, or a redirect):
303,400,403,500,503 - Source:
packages/envoy/internal/broker/approval/signin.go:101
POST /signin/logout
Section titled “POST /signin/logout”Sign out of the approval page, from the broker’s own origin only: the session ends and its cookie is cleared, and the answer links back to the record.
- Credential: Broker sign-in session
- Answers (an HTML page, or a redirect):
200,400,403,500,503 - Source:
packages/envoy/internal/broker/approval/signin.go:184
GET /signin/status
Section titled “GET /signin/status”Report the broker’s sign-in: whether its sign-in provider accepted the broker’s own client at the last hourly check and when, when a person last signed in, when a sign-in last failed and the coded reason it failed with, and when a decision was last made on the approval page. It needs no credential because a production check fetches it from a pod where no person is signed in, and because it names no login, no secret and no provider’s message: every word it answers is one of two closed sets this broker chose (internal/broker/store’s signin_status). It spends its source address’s bucket in its own handler, as the page’s routes spend it in authenticate, so this read of the broker’s own row is metered too.
- Credential: None
- Success:
200 - Source:
packages/envoy/internal/broker/api/server.go:684
Response body (JSON):
| Field | Type | Description |
|---|---|---|
client_check |
string or null |
What the last check of the broker’s client at its sign-in provider found: “ready” (the provider accepted the client), “client_rejected” (it refused it, so nobody can sign in) or “issuer_unreachable” (it could not be asked, which says nothing about the client). |
client_checked_at |
string (RFC 3339 time) or null |
When that check was taken (RFC 3339). |
last_sign_in_at |
string (RFC 3339 time) or null |
When a sign-in last completed. |
last_failed_sign_in_at |
string (RFC 3339 time) or null |
When a sign-in last failed at the token exchange or at the ID-token check. |
last_failed_sign_in_reason |
string or null |
Why that sign-in failed, one of invalid_client, unauthorized_client, redirect_uri_mismatch, invalid_grant, issuer_unreachable, bad_issuer, bad_audience, bad_signature, bad_nonce, not_in_domain and email_not_verified. |
last_decision_at |
string (RFC 3339 time) or null |
When a decision was last made on the approval page. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 503 | DATABASE_UNAVAILABLE |
read the sign-in status failed: a dependency the broker needs is unavailable |
| 429 | RATE_LIMITED |
too many requests from this address; try again later |
GET /approve/assets/approve.css
Section titled “GET /approve/assets/approve.css”The approval page’s one stylesheet, embedded in the broker; it needs no credential and spends no rate-limit bucket, since every page it dresses is read before anyone has signed in and the bytes are the binary’s.
- Credential: None
- Source:
packages/envoy/internal/broker/api/server.go:706
GET /healthz
Section titled “GET /healthz”Report whether the broker can reach its database. From SIGTERM onward it answers 503 SHUTTING_DOWN instead (Deps.Draining) while the process goes on serving every other route, which is what takes a stopping pod out of its Service’s endpoints before it stops accepting connections. So this is a readiness probe and must never be used as a liveness probe: a liveness probe on it would have the kubelet kill the container in the middle of the drain, dropping the requests the pod was draining.
- Credential: None
- Success:
200 - Source:
packages/envoy/internal/broker/api/server.go:636
Response body (JSON):
| Field | Type | Description |
|---|---|---|
status |
string |
“ok”. |
Refusals:
| Status | Code | Message |
|---|---|---|
| 503 | DATABASE_UNAVAILABLE |
ping Postgres failed: a dependency the broker needs is unavailable |
| 503 | SHUTTING_DOWN |
the broker is shutting down and is no longer taking new traffic |