Skip to content

Configuration

The broker reads its configuration from the environment and refuses to start, naming the variable, when one is missing, malformed or out of range.

Variable Default Accepted values What it sets
BROKER_ACCESS_TOKEN_SECONDS 900 whole number from 1 to 3600 How long an access token the broker’s OAuth provider issues lasts, a machine login’s and an exchanged one’s alike. A caller renews within it rather than holding one token for a whole run, so it trades how often every caller renews against how long a revoked credential’s last token stays spendable.
BROKER_AIRLOCK_CA_CERT_FILE — — The PEM certificate of the airlock proxy’s certificate authority, the one every release hands the command to trust. Required with BROKER_AIRLOCK_RULES_FILE, and read once at start as half of that authority, with BROKER_AIRLOCK_CA_KEY_FILE; a pair the authority refuses refuses the start. What a release carries is the authority’s own certificate rather than this file’s bytes, so a file that holds more than the certificate hands a command nothing more. Both broker pods mount the same file from one Kubernetes Secret, so a command’s trust does not depend on which pod released its values.
BROKER_AIRLOCK_CA_KEY_FILE — — The PEM private key of BROKER_AIRLOCK_CA_CERT_FILE’s certificate authority, which the proxy port signs each host’s certificate with. Required with BROKER_AIRLOCK_RULES_FILE, and mounted from the same Kubernetes Secret, which no container but the broker’s mounts: it is the key that lets its holder be any host a command trusts.
BROKER_AIRLOCK_IDLE_SECONDS 300 whole number from 10 to 3600 How long one proxy connection may go with no byte in either direction before the proxy closes it, default 300, between 10 and 3600. A connection carries a grant’s placeholders, so an abandoned one is state the broker holds for a command that is gone; a short bound costs a long-polling client its connection, which it opens again.
BROKER_AIRLOCK_LISTEN_ADDR 127.0.0.1:13381 — The address the airlock’s proxy port binds, default 127.0.0.1:13381. It is a second port of the same process, so it is bound and drained with the API’s. Read only when BROKER_AIRLOCK_RULES_FILE names a file; a deployment with no airlock binds no proxy port.
BROKER_AIRLOCK_PROXY_URL — — The URL a command reaches the airlock’s proxy port at, which every release of a name behind the airlock carries, so the command points its HTTPS_PROXY at it. Required with BROKER_AIRLOCK_RULES_FILE, and refused at start unless it is an absolute http or https URL with a host: a value with no scheme or no host would boot and then fail every airlock run one command at a time. It is a setting of its own rather than BROKER_PUBLIC_URL with another port, because the Service port a pod dials need not be the container port the broker binds.
BROKER_AIRLOCK_RULES_FILE — — The path of the airlock’s rules file, airlock.json, which says for each secret which hosts its value may reach and in which headers. The broker reads it once at start, so a change to it rolls the pods; it refuses to start on a file it cannot read, cannot parse, or whose entries it refuses, naming the file and the entry. A secret the file names is never released as a value: a session is given a placeholder, and the broker swaps the real value into the headers the entry allows. Empty (the default) is a deployment with no airlock, where every granted secret is released as its value.
BROKER_AIRLOCK_TEST_DIAL_ALLOW — — Whitespace-separated <host>:<port> addresses the airlock proxy may dial although the address they resolve to is one it never dials - a loopback, cluster, link-local or private address. It is a list rather than a switch for loopback because a local stack needs both at once: its stand-in services on loopback reachable, and the broker’s own loopback port still refused. For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_AIRLOCK_TEST_GITHUB_API_BASE — — The base URL a github-app secret is minted against, in place of https://api.github.com, so a release of one mints at a stand-in GitHub (internal/broker/airlock/airlocktest, cmd/dev-airlock-stands). For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_AIRLOCK_TEST_GOOGLE_TOKEN_URL — — The token endpoint a google-login secret is refreshed at, in place of https://oauth2.googleapis.com/token, so a release of one mints at a stand-in Google. For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_AIRLOCK_TEST_KEEP_VALUES — — With it true, the airlock proxy keeps a grant’s values across the reread of one of its names and past the five-minute margin before a minted token’s expiry, rather than reading and minting again. It is the negative control of the check that both halves belong to. For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_AIRLOCK_TEST_NO_KIND — — With it true, the airlock proxy reads every rules entry as having no kind, so an azure entry’s exchange and an aws entry’s signing are off and each takes the plain header swap. It is the negative control of the check those two kinds belong to. For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_AIRLOCK_TEST_RESOLVE — — Whitespace-separated <host>=<address> entries, each an IP address the airlock proxy reads that host as having and dials it at, in place of what the resolver answers. It is how a client that resolves a real name of its own - az, the AWS client - reaches a stand-in service through the proxy. A host named twice is refused. For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_AIRLOCK_TEST_UPSTREAM_CA_FILE — — A PEM file of extra roots the airlock proxy verifies a service’s certificate against on its own calls upstream, beside the host’s own roots, so it trusts a stand-in service’s certificate. Read once at start, and a file it cannot read or that holds no certificate refuses the start. For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_API_ACCOUNTS — — The accounts that call the broker’s named-caller routes — the approval routes Dispatch’s server calls on a person’s behalf, the introspection route a resource server reads one of the broker’s access tokens back through, and the grant-log route a quarterly access review reads the broker’s audit trail through — whitespace-separated <caller>=<account> entries. An account is a Kubernetes service account, written as the subject a projected token carries (dispatch=system:serviceaccount:example-ns:example-dispatch), or a Google service account, written as google: and the account’s address (listener=google:example-listener@example-project.iam.gserviceaccount.com). The two kinds are told apart by the entry’s own text and never by guessing at its shape: a Kubernetes subject names its kind already, and the google: tag names the other, so a mistyped address is refused as the kind it was written as rather than read as the other. A Kubernetes caller presents its own projected token, for BROKER_K8S_OIDC_AUDIENCE, verified against BROKER_K8S_OIDC_ISSUER as every other ServiceAccount token the broker takes is, and is looked up by the token’s sub; so a Kubernetes entry needs that pair. A Google caller presents its machine’s own Google identity token, for BROKER_GOOGLE_OIDC_AUDIENCE — the audience every machine of the deployment already holds one for — verified against BROKER_GOOGLE_OIDC_ISSUER, and is looked up by the token’s email; so a Google entry needs BROKER_GOOGLE_OIDC_AUDIENCE. Either token is refused if it lives longer than an hour from its issue, as a service’s login token is. A project’s default Google accounts and Google-managed service agents are refused here as they are in BROKER_VM_SERVICES, since a token from one proves nothing about which machine is asking. The caller’s name decides what the account may do, and the broker knows three: dispatch reaches every one of those routes, the ten Dispatch’s server relays a person’s decisions and reads through as well as introspection; listener reaches introspection alone, since the Envoy listener reads a token back and relays no decision; and access-review reaches GET /v1/grant-log alone, which answers every audit row of a closed period, so whoever runs the quarterly review reads the whole trail and can decide nothing, release no secret value and write no row. Dispatch’s server is deliberately not given that last one: it already holds approval authority over every person’s secrets, and the review is a reader, so the two stay separate names and separate accounts. A name the broker does not know refuses the boot naming it and the names it knows, so a typo never quietly produces a caller that reaches nothing, and a new caller is a deliberate edit to the broker’s own table (api.AccountFor). A listed caller reaching a route its name does not carry is 403 ACCOUNT_LACKS_POWER, which is a different fact from an account that is not listed at all (403 ACCOUNT_NOT_LISTED). A namespace’s default account is refused, as is an account given twice, since each account names one caller. APIAccounts is keyed by ServiceAccount subject and GoogleAPIAccounts by Google account address, each answering its caller and that caller’s powers. Unset, no caller reaches those routes.
BROKER_AZURE_LOGIN_SECRET — — The one agent-secret name allowed to release the broker’s Azure projected token, for example AZURE_LOGIN. Empty (the default) disables azure-login releases. Must be an uppercase agent-secret name, not its store id or slug. That secret must be human tier in the policy snapshot checked for each release, with a person’s approval recorded for that name in its grant. Another secret with kind=azure-login, the named secret retagged to agent tier, or an automatic grant releases no token.
BROKER_AZURE_TOKEN_FILE — — The path of the broker pod’s projected ServiceAccount token for azure-login releases, default /var/run/secrets/azure/tokens/azure-identity-token. Read only on release, not at boot, and read anew each time so the rotated token is used. The token’s exp claim supplies the released expires_at; an unreadable, malformed or expired token refuses that release. Mount the projection read-only in the broker container alone, with audience api://AzureADTokenExchange and a one-hour lifetime. Only one Entra identity may trust this ServiceAccount subject: a released token can sign in to any identity that does.
BROKER_DATABASE_URL — — The Postgres connection URL, the one variable that says which database the broker uses and how it signs in. Required. The broker applies its own migrations at startup. A URL that names a user and no password (neither in the URL nor from PGPASSWORD or a passfile) and whose host is an Amazon RDS endpoint (one ending in .rds.amazonaws.com) signs in by RDS IAM authentication: each new connection signs in with an auth token the broker mints for that user and host from the AWS SDK’s default credentials, in the region AWS_REGION, AWS_DEFAULT_REGION or the shared AWS config names, which need rds-db:connect on the database user. Such a URL must name that one host, with sslmode=verify-full and an sslrootcert file (the broker image ships the RDS CA bundle at /etc/ssl/rds/global-bundle.pem), or the broker refuses to start, since a token is a password to the database for 15 minutes; sslrootcert=system is refused too, since the system trust store holds no RDS CA. Any other URL, one carrying its own password or a passwordless one to a local Postgres that trusts its clients, connects as given. Nothing is substituted into it: a URL naming the removed ${BROKER_DATABASE_PASSWORD} placeholder is refused, as BROKER_DATABASE_PASSWORD itself is.
BROKER_DRAIN_SECONDS 5 whole number from 0 to 120 How long the broker goes on serving after SIGTERM while answering GET /healthz with 503, so Kubernetes has stopped sending it new requests before it stops accepting them. It is configuration rather than a compiled constant because the right value is the cluster’s EndpointSlice propagation time — how long a pod the control plane has marked for deletion keeps receiving traffic — which lives in the deployment’s own repository, not this one, and must be tunable there without a new image. Removal from a Service’s EndpointSlice starts when the pod’s deletion timestamp appears, concurrently with the kubelet beginning termination, so what the drain must outlast is that propagation and not a readiness probe’s periodSeconds × failureThreshold, which no longer stands between the pod and the endpoint controller by then; 5 seconds is what this repository’s own Dispatch deployment sizes its preStop wait at, for the same reason. 0 is for a local run or a dev stack, where nothing propagates and the wait is pure delay.
BROKER_ENVOY_TOKEN
BROKER_ENVOY_TOKEN_FILE
— — The bearer token for BROKER_ENVOY_URL, given directly or as a file whose trimmed contents win over the variable; read only when BROKER_ENVOY_URL is set.
BROKER_ENVOY_URL — — An Envoy listener the broker notifies, best effort, when a session’s pending request expires undecided. Unset, the broker sends nothing.
BROKER_EXCHANGE_ACCOUNTS — — The Kubernetes service accounts whose pods exchange their projected token, whitespace-separated <service account>=<project>=<person>[=<session>] entries, such as system:serviceaccount:example-ns:example-worker=EXAMPLE=ada@example.com. The token minted for a pod of the account may write that one project, acts for that person, and writes under the session the entry fixes; an entry that fixes none requires the pod to name its own session. The session is everything after the third “=”, so it alone may carry one. A namespace’s default account is refused, as is an account listed twice. Needs BROKER_K8S_OIDC_ISSUER and BROKER_EXCHANGE_K8S_AUDIENCE, whose tokens its pods present. Unset, no account exchanges.
BROKER_EXCHANGE_GITHUB_AUDIENCE — — The audience a workflow run’s token must carry, the one the run asks GitHub to mint it for.
BROKER_EXCHANGE_GITHUB_ISSUER — — The issuer of the token a continuous-integration workflow run presents to the exchange, https://token.actions.githubusercontent.com for runs on GitHub’s own hosts. Set it with BROKER_EXCHANGE_GITHUB_AUDIENCE, or neither, in which case the broker accepts no workflow token at all.
BROKER_EXCHANGE_K8S_AUDIENCE — — The audience a pod’s projected service-account token must carry to be exchanged (RFC 8693) for a Dispatch access token. Its issuer is the one cluster the broker is told about, BROKER_K8S_OIDC_ISSUER, which this needs; the audience is its own because an exchanging pod projects a token for it beside the one it enrolls with (BROKER_K8S_OIDC_AUDIENCE), so neither token is spendable where the other is. Unset, no pod exchanges.
BROKER_EXCHANGE_WORKFLOWS — — The GitHub workflows whose runs exchange their token, whitespace-separated <repository>=<workflow path>=<ref>=<project>=<person>[=<session>] entries, such as example-org/example-repo=.github/workflows/release.yaml=refs/heads/main=EXAMPLE=ada@example.com. Each field is compared exactly and nothing is wildcarded: a run of another workflow, or of this one on another ref, is not this entry. The session is everything after the fifth “=”, so it alone may carry one, and a workflow listed twice is refused. Needs BROKER_EXCHANGE_GITHUB_ISSUER and BROKER_EXCHANGE_GITHUB_AUDIENCE, whose tokens its runs present. Unset, no workflow exchanges.
BROKER_FAKE_SECRETS_FILE — — For local development only, a JSON file standing in for the secret store, in Secrets Manager’s shape whatever BROKER_SECRETS_STORE says, {“secrets”: [{“name”, “kms_key_id”, “tags”, “value”, “deleted_at”}]}: the broker lists the agent secrets and reads their values from it instead of from the store, reading the file again on each of those calls, so an edit to it is a write as Secrets Manager would see one. A secret with no “value” is one created without a value, which the broker refuses as no-current-value; one with a “deleted_at” (RFC 3339) is scheduled for deletion, which the broker no longer lists. Its “kms_key_id” names the agent-secrets key as the selected store configures it: BROKER_SECRETS_KMS_KEY_ARN, or BROKER_SECRETS_KMS_KEY_NAME under gcp-secret-manager.
BROKER_GOOGLE_OIDC_AUDIENCE — — The audience a machine’s Google identity token must carry, at the RFC 8693 token exchange, where such a token is a subject token (urn:envoy:params:oauth:token-type:google-identity). Its issuer is BROKER_GOOGLE_OIDC_ISSUER, Google’s own unless a dev stack says otherwise. Needs BROKER_VM_SERVICES, since an audience no VM service is listed for could accept no token. Unset, the broker accepts no Google identity token, and BROKER_VM_SERVICES, BROKER_VM_PROJECTS, BROKER_VM_INSTANCES and BROKER_VM_EXCHANGE_ACCOUNTS are refused.
BROKER_GOOGLE_OIDC_ISSUER — — The issuer of the Google identity tokens machines present, https://accounts.google.com when unset, which is where every real one comes from. It is a variable because a dev stack has no Google: cmd/dev-oidc-issuer stands in for it on a loopback port, so a broker, its machines and Dispatch can be driven end to end on one machine with no cloud. Read only where BROKER_GOOGLE_OIDC_AUDIENCE is set, which is what turns Google identity tokens on at all.
BROKER_JUDGE_DISPATCH_TOKEN_FILE — — The file holding the broker’s own projected ServiceAccount token for the Dispatch at BROKER_JUDGE_DISPATCH_URL. Read when the judge calls Dispatch and not at boot, through the reader every other projected token in this binary goes through (brokertoken.AccountToken), which reads the file again once what it last read is 30 seconds old, so a rotation reaches the judge without a restart. Required with the rest of JudgeSettings.
BROKER_JUDGE_DISPATCH_URL — — The base URL of the Dispatch the judge reads an issue from, an absolute http(s) URL. It makes two GETs there — one for the issue a request’s reason names, which answers that issue’s key, title and status, and one for the whole markdown of the issue’s spec document — and writes nothing: no ask, no comment, no status. Required with the rest of JudgeSettings.
BROKER_JUDGE_MODEL — — The id of the model the judge asks, as the endpoint at BROKER_JUDGE_MODEL_URL names it. Required with the rest of JudgeSettings.
BROKER_JUDGE_MODEL_AUDIENCE — — The audience of the Google identity token the broker presents to the endpoint at BROKER_JUDGE_MODEL_URL, minted for the broker’s own machine identity. Required with the rest of JudgeSettings.
BROKER_JUDGE_MODEL_PROTOCOL anthropic-messages — The wire protocol that endpoint speaks, one of anthropic-messages (the default), openai-responses, openai-chat and azure-anthropic, which are agentloop’s four Run kinds; any other value refuses the start naming the setting, the value and the four it takes. It is the one judge setting with a default, so it is never among the ones a half-configured judge is refused for; and because it says nothing on its own, setting it with the other five unset is refused as the same mistake, naming the five a judge needs, rather than read as a deployment that meant no judge.
BROKER_JUDGE_MODEL_URL — — The base URL the broker’s model judge calls its model at, an absolute http(s) URL. Required with the rest of JudgeSettings, which are all-or-none: with none of the five set there is no judge, and every human-tier request waits for the person who decides it today.
BROKER_JUDGE_TEST_SKIP_CHECKS — — With it true, the judge’s checks before the model are off, so a request one of them would hold back is asked about anyway. It is the negative control of the check the judge belongs to. For the broker’s local stack alone: a production start, which is one with BROKER_FAKE_SECRETS_FILE unset, is refused naming it.
BROKER_K8S_OIDC_AUDIENCE — — The audience a pod’s projected service-account token must carry.
BROKER_K8S_OIDC_ISSUER — — The issuer of the Kubernetes service-account tokens pods enroll with. Set it with BROKER_K8S_OIDC_AUDIENCE, or neither, in which case no pod can enroll.
BROKER_LAUNCHER_CREDENTIAL_SECONDS 2592000 whole number from 1 to 2592000 How long a machine login lasts without a refresh. An approval mints its credential for this long, and each refresh moves its expiry to this long from then; past it the machine logs in again, with a new key, a new code and a new approval.
BROKER_LEASE_SECONDS 900 whole number from 1 to 3600 An enrollment’s lease. A session renews it while it runs; one whose lease runs out is no longer enrolled and its calls are refused. Time while no broker was serving is not counted against it: a lease that ran out then stays live until a broker serves again, which gives it that time back. A gap of up to three BROKER_SWEEP_SECONDS still counts as a broker serving and is not given back, so a lease must outlive such a gap and the retries its renewer then takes to renew, counting each renewal the outage refuses as failing at whatever instant costs the renewer most, up to the 30-second client timeout: 91 s at a sweep of 1 or 5 seconds, 105 s at 10, 225 s at 30 and 360 s at 60. The broker refuses a shorter pair at boot and names the shortest lease that survives that sweep.
BROKER_LISTEN_ADDR 127.0.0.1:13380 — The host:port the broker listens on.
BROKER_MACHINE_LOGIN_URL — — The absolute http(s) URL of the page where a person types a machine login’s code (Dispatch’s /credentials/machine). Optional. The device grant’s verification_uri names it, so a machine shows its person where to go, and with no page to show there is no device grant: the broker composes none of its handlers, POST /v1/oauth/device_authorization is not mounted at all (404), and the authorization server metadata leaves the device code out of grant_types_supported. Everything else the OAuth provider serves — the token endpoint, the RFC 8693 exchange, introspection and revocation — is served either way. Unset is the deployment where machines hold a cloud identity and no person ever types a code.
BROKER_PROOF_SKEW_SECONDS 60 whole number from 1 to 300 How far the issue time of a signed proof or request object may differ from the broker’s clock.
BROKER_PUBLIC_URL — — The broker’s own address as its callers reach it, an absolute URL with no path. Required. Every signed proof and request object names it, so a client’s AGENT_SECRETS_URL must be exactly this.
BROKER_SECRETS_KMS_KEY_ARN
BROKER_SECRETS_KMS_KEY_NAME
— — The agent-secrets key every agent secret is encrypted with, one variable per store, each required with its store and refused with the other. With aws-secrets-manager, BROKER_SECRETS_KMS_KEY_ARN, the AWS KMS key’s ARN; with gcp-secret-manager, BROKER_SECRETS_KMS_KEY_NAME, the Cloud KMS key’s resource name projects/<project>/locations/global/keyRings/<ring>/cryptoKeys/<key> (global, since a secret with automatic replication takes a global key). A secret under the prefix encrypted with any other key, the provider’s default one included, is refused.
BROKER_SECRETS_PREFIX — — The namespace of the agent secrets. Required. With aws-secrets-manager, a Secrets Manager name prefix ending in “/” (for example production/agent-secrets/); with gcp-secret-manager, projects/<project number>/secrets/<secret-id prefix>- (for example projects/000000000000/secrets/agent-secrets-), the project by its number, since Secret Manager answers every resource name with the number. The broker serves every secret under it that carries an owner and a tier tag (Secret Manager: annotation) and is encrypted with the agent-secrets key, and rereads them every five minutes.
BROKER_SECRETS_STORE — — Which secret store the agent secrets live in: aws-secrets-manager (the default when unset, AWS Secrets Manager with the agent-secrets key in AWS KMS) or gcp-secret-manager (Google Cloud Secret Manager with the agent-secrets key in Cloud KMS, read under Application Default Credentials: on GKE, Workload Identity for the broker’s ServiceAccount, with no key file). It decides BROKER_SECRETS_PREFIX’s form and which key variable is required.
BROKER_SERVICES — — The registered services, whitespace-separated name=<service account> entries, such as legion-daemon=system:serviceaccount:legion:legion-worker. A name (lowercase letters, digits and hyphens, at most 64, not shared) is what a secret’s owner tag may name; the service account (system:serviceaccount:<namespace>:<name>) is one the service’s pods run as. A service may be named for several accounts, one entry each, but each account is given once and proves exactly one service. A session is the service’s when a launcher logged in as the service enrolled it and it is a pod whose projected token proved it runs as one of that service’s accounts. A machine login’s service name is the machine’s own claim, approved by whoever its login names, so the pod’s verified service account is what proves the service, and an account bound to two names would prove neither. A namespace’s default account (system:serviceaccount:<namespace>:default) is refused: every pod in the namespace whose spec names no account runs as it and can mount a token for the broker’s audience, so it proves nothing about which pods are the service’s. Keyed by account, each answering its service. A secret owned by a listed service goes at once to those pods and is refused to every other session. Unset, no service is registered, and a secret whose owner tag names one is refused as owner-tag-malformed.
BROKER_SERVICE_ACCOUNT_SECRETS — — The one Google service account each google-service-account agent secret mints a token for, whitespace-separated <SECRET NAME>=<account email> entries, such as GCP_ADMIN=gcp-admin@example-project.iam.gserviceaccount.com. The name is the uppercase agent-secret name a session asks for (DEV_ADITYA, GCP_ADMIN), not that secret’s store id or its slug, and each secret is named at most once; the account is a user-managed Google service account, <account id>@<project id>.iam.gserviceaccount.com in lowercase, held to secrets.ValidServiceAccountEmail, the minter’s own rule, so no binding can ever name an address the minter would refuse. A Google-managed service agent is refused here as it is in BROKER_VM_SERVICES: Google’s own services run as those accounts and the broker must not be able to impersonate one. One account may be bound to two secrets, since only the secret’s name is the key. This setting, and not a secret’s stored value, is what chooses the account a google-service-account secret mints for (secrets.GoogleServiceAccount.Accounts, which the broker fills from it): the broker holds roles/iam.serviceAccountTokenCreator on every account bound here, so a stored value alone choosing the account would let whoever may create an agent secret store another person’s service account in a secret of their own, list themselves among its requesters and mint that person’s token. A mint happens only where this binding and the stored value name the same account; either missing or the two disagreeing is refused before any call reaches Google. Unset, no secret is bound and every google-service-account mint is refused with secrets.ErrMintNotBound, logged class=binding. That is the window a deployment is in between this build and the apply that writes its bindings, and it is deliberately the safe side of the change: a broker that went on minting on the stored value alone until the setting arrived is the very thing the setting exists to stop. Nothing else needs it: a broker with no binding still boots and still serves every other kind. The policy loader reads it as well (policy.Loader.ServiceAccounts), for one rule — a secret bound to a dev-<name> account is one person’s own, so it may list no registered service among its requesters.
BROKER_SERVICE_LOGINS — — The ServiceAccounts that log in as a service with their own projected token (POST /v1/launcher-credentials/token), and that authenticate the routes a service calls as itself (serviceAuth, today POST /v1/oauth/exchange/pod-sessions, where a deployment’s daemon registers a pod it starts under a session name), whitespace-separated <service>=<service account> entries, such as legion-daemon=system:serviceaccount:legion-system:legion-daemon-legion. Each account (system:serviceaccount:<namespace>:<name>, named exactly: there is no wildcard) logs in as the one service its entry names and is given once; one service may have several accounts, one per machine. A namespace’s default account is refused, since every pod there that names no account runs as it. An account BROKER_SERVICES binds for a service’s pods may be listed here as well, which is a pod of that account enrolling itself: every such pod holds a token for the account, so each of them logs the service in, and what keeps one of them from enrolling another is enroll.Service.podUnderTokenLogin, which gives such a login the one pod whose token made it and answers ErrPodSelfEnrollOnly for any other pod UID. Keyed by account, each answering its service. A token whose subject is not listed is refused, and a credential an account’s login issued stops authenticating once its entry is gone. Needs BROKER_K8S_OIDC_ISSUER, whose tokens it trusts. Unset, no service logs in with a token.
BROKER_SESSION_CARRY_SECONDS 43200 whole number from 0 to 86400 How long what a session holds — its grants and its pending requests — waits on the enrollment of a box the session has left, for the next enrollment of that session name under the same principal to take it. An enrollment that ends while it holds either is left dormant instead of having them revoked (enroll.endEnrollment), and the sweep ends it for good once this long has passed since it went dormant (enroll.Service.EndCarried). 12 hours is requests.DefaultGrantLifetime, the life the broker already gives a grant whose request named none, so the default carry never outlives the grant it carries; and it is what an overnight approval takes, a request filed in the morning, the box replaced an hour later and the approval granted later that day. 0 turns the carry off: the next sweep ends every dormant enrollment it finds, which is what every end did before the carry. The maximum is one day, because a dormant enrollment holds a grant no session is proving and the approval behind it was given for one day’s work: past a day the person who approved yesterday has not approved today, and the session asking again is the right outcome.
BROKER_SIGNIN_ISSUER
BROKER_SIGNIN_CLIENT_ID
BROKER_SIGNIN_CLIENT_SECRET_FILE
BROKER_SIGNIN_DOMAIN
BROKER_SIGNIN_SESSION_SECONDS
86400 whole number from 1 to 86400 The broker’s own OpenID Connect client, through which a person signs in to the broker’s approval page to approve a secret request: the provider’s issuer (an absolute https URL, or http only to a loopback host, for a run on one machine), the client’s id, a file holding the client’s secret (read at startup and trimmed; no variable carries the secret itself, so BROKER_SIGNIN_CLIENT_SECRET is refused), the Google Workspace domain a person’s ID token must name in its hd claim, and how many seconds a sign-in lasts (an absolute expiry nothing extends, at most 86400, 24 hours, the default). The first four are required: a broker missing any of them refuses to start, naming the ones missing, since nobody could approve a secret request on a page nobody can sign in to.
BROKER_SIGNIN_DEVICE_AUDIENCE — — The audience a person’s own Google identity token must carry to be traded at the RFC 8693 token exchange, where such a token is a subject token (urn:envoy:params:oauth:token-type:person-google-identity) and the machine the person signed in on presents its own as the actor token (urn:envoy:params:oauth:token-type:machine-google-identity). It is the OAuth client id of the device-flow client agent-secrets login runs Google’s device authorization grant as, since Google puts the client’s id in an ID token’s aud. Its issuer is BROKER_GOOGLE_OIDC_ISSUER, Google’s own unless a dev stack says otherwise, and the person it names is held to BROKER_SIGNIN_DOMAIN through the hd claim, exactly as a person signing in to the broker’s own approval page is. Needs BROKER_GOOGLE_OIDC_AUDIENCE, whose machines are the boxes a person may sign in on. Unset, the broker accepts no person’s sign-in and registers neither token type, so a caller presenting either is refused by name. It is not one of the four SignInSettings, which are required together: those are what a person approves a secret request through and no broker serves without them, where this one is optional, as the whole Google exchange is.
BROKER_SWEEP_SECONDS 5 whole number from 1 to 60 How often the broker ends the enrollments whose lease lapsed and expires the pending requests and machine logins nobody decided in time. It is also how often a serving broker records that it serves: a gap of more than three sweeps between two such records is time no broker was serving, which the broker gives back to every lease still running when the gap began. A shorter gap is given back to nobody, so a long sweep demands a long BROKER_LEASE_SECONDS.
BROKER_TRUSTED_PROXY_HEADER — — The request header (e.g. X-Forwarded-For) whose last entry the machine-login and secret-reread rate limiters take as the caller’s address. Unset, they use the connection’s own address, which is right only when nothing proxies the broker. Set it only when every request passes through your own reverse proxy, which appends that entry; otherwise a caller can forge the header and pick its own rate-limit bucket.
BROKER_VM_EXCHANGE_ACCOUNTS — — The person and the Dispatch project each fleet writes for, whitespace-separated <service account email>=<project>=<person>[=<session>] entries, the grammar BROKER_EXCHANGE_ACCOUNTS uses for a pod, such as example-fleet@example-project.iam.gserviceaccount.com=EXAMPLE=ada@example.com. A token the exchange mints for a machine of that account may write that one project, acts for that person, and writes under the session the entry fixes; an entry that fixes none requires the machine to name its own session. The session is everything after the third “=”, so it alone may carry one. Each account is named at most once and must be one BROKER_VM_SERVICES lists. An account BROKER_VM_SERVICES lists and this setting does not gets a token that names its service and no person at all, which a resource server holds to what a service may do. Needs BROKER_GOOGLE_OIDC_AUDIENCE. Unset, every listed account’s machines get such a token. The entries this answers are the whole of what the exchange decides a machine’s token on: each one carries its account’s service (BROKER_VM_SERVICES), the pins (BROKER_VM_PROJECTS and BROKER_VM_INSTANCES) and, where this setting names them, the person, the project and the session.
BROKER_VM_INSTANCES — — The names each pinned account’s machines may have, whitespace-separated <service account email>=<instance-name glob> entries, at most one per account BROKER_VM_SERVICES lists and none for any other account. The glob is Go’s path.Match syntax (, ?, and […] classes, negated with ^) over the characters of an instance name, such as example-vm-. An account named here is named in BROKER_VM_PROJECTS too, or the boot is refused. Keyed by account. Needs BROKER_GOOGLE_OIDC_AUDIENCE. Unset, it is empty, so no account is pinned.
BROKER_VM_PROJECTS — — The Google Cloud project each pinned account’s machines run in, whitespace-separated <service account email>=<project id> entries, at most one per account BROKER_VM_SERVICES lists and none for any other account. An account named here is named in BROKER_VM_INSTANCES too, or the boot is refused: half a pin says nothing clear about which machines an entry means. Keyed by account. Needs BROKER_GOOGLE_OIDC_AUDIENCE. Unset, it is empty, so every listed account takes a machine wherever it runs — including one whose token carries no Compute Engine claims at all, which is what a token minted through a workload identity pool and a token from a Kubernetes pod’s metadata server both carry.
BROKER_VM_SERVICES — — The VM services, whitespace-separated name=<service account email> entries, such as example-service=example-fleet@example-project.iam.gserviceaccount.com. A name is lowercase letters, digits and hyphens, at most 64, not shared; the account is a user-managed Google service account (<account id>@<project id>.iam.gserviceaccount.com, in lowercase, as a token carries it) that the service’s machines run as, so every machine of that account is one identity. A service may be named for several accounts, one entry each, but each account is given once and proves exactly one service. A project’s default service accounts (Compute Engine’s and App Engine’s) and Google-managed service agents are refused: a project’s workloads run as the first by default and Google’s own services as the second, so neither proves which VMs are the service’s. An account may be pinned to one Compute Engine project and one instance-name pattern (BROKER_VM_PROJECTS and BROKER_VM_INSTANCES), or to neither, in which case its machines are taken wherever they run. Keyed by account, each answering its service. Needs BROKER_GOOGLE_OIDC_AUDIENCE. Unset, no VM service is listed.

The broker no longer reads these, and refuses to start while one is set, so a deployment that still sets one fails loudly instead of running on configuration that means nothing.

Variable Why
BROKER_ASK_POLL_SECONDS The broker opens no Dispatch ask and issues nothing in Dispatch; its model judge reads one thing there, whether an issue is open, at BROKER_JUDGE_DISPATCH_URL and with the broker’s own projected ServiceAccount token (BROKER_JUDGE_DISPATCH_TOKEN_FILE), and writes nothing
BROKER_DATABASE_PASSWORD The broker substitutes no password into BROKER_DATABASE_URL: on Amazon RDS it signs in by IAM token (https://cybermonklabs.github.io/legion/broker/operate/#signing-in-to-rds-by-iam-token), and any other database’s password goes in the URL itself
BROKER_DISPATCH_PROJECT The broker opens no Dispatch ask and issues nothing in Dispatch; its model judge reads one thing there, whether an issue is open, at BROKER_JUDGE_DISPATCH_URL and with the broker’s own projected ServiceAccount token (BROKER_JUDGE_DISPATCH_TOKEN_FILE), and writes nothing
BROKER_DISPATCH_TOKEN The broker opens no Dispatch ask and issues nothing in Dispatch; its model judge reads one thing there, whether an issue is open, at BROKER_JUDGE_DISPATCH_URL and with the broker’s own projected ServiceAccount token (BROKER_JUDGE_DISPATCH_TOKEN_FILE), and writes nothing
BROKER_DISPATCH_TOKEN_FILE The broker opens no Dispatch ask and issues nothing in Dispatch; its model judge reads one thing there, whether an issue is open, at BROKER_JUDGE_DISPATCH_URL and with the broker’s own projected ServiceAccount token (BROKER_JUDGE_DISPATCH_TOKEN_FILE), and writes nothing
BROKER_DISPATCH_URL The broker opens no Dispatch ask and issues nothing in Dispatch; its model judge reads one thing there, whether an issue is open, at BROKER_JUDGE_DISPATCH_URL and with the broker’s own projected ServiceAccount token (BROKER_JUDGE_DISPATCH_TOKEN_FILE), and writes nothing
BROKER_MAX_GRANT_SECONDS A request’s –ttl chooses a shorter grant lifetime, otherwise its grant lasts 12 hours, or until the requesting session ends for a secret tagged default-grant=session
BROKER_MINT_TARGETS The broker no longer reads one list of mint targets; each secret’s kind tag names the minter it is served by
BROKER_RULES_FILE The broker reads each secret’s owner and tier from the secret’s own tags under BROKER_SECRETS_PREFIX; where a secret’s value may go is BROKER_AIRLOCK_RULES_FILE, the airlock’s rules file, and no variable names any other rules file
BROKER_RULES_RELOAD_SECONDS The broker reads each secret’s owner and tier from the secret’s own tags under BROKER_SECRETS_PREFIX; where a secret’s value may go is BROKER_AIRLOCK_RULES_FILE, the airlock’s rules file, and no variable names any other rules file
BROKER_RULES_S3_URI The broker reads each secret’s owner and tier from the secret’s own tags under BROKER_SECRETS_PREFIX; where a secret’s value may go is BROKER_AIRLOCK_RULES_FILE, the airlock’s rules file, and no variable names any other rules file
BROKER_SIGNIN_CLIENT_SECRET The broker reads its sign-in client’s secret only from the file BROKER_SIGNIN_CLIENT_SECRET_FILE names, so the secret is never in its environment
BROKER_UI_ORIGIN A person approves on the broker’s own page, signed in to the broker (BROKER_SIGNIN_*), so the broker checks no WebAuthn origin
BROKER_UI_TOKEN The broker’s approval routes take the calling service’s own Kubernetes ServiceAccount token, verified against BROKER_K8S_OIDC_ISSUER, so list that account in BROKER_API_ACCOUNTS instead of sharing a bearer with Dispatch’s server
BROKER_UI_TOKEN_FILE The broker’s approval routes take the calling service’s own Kubernetes ServiceAccount token, verified against BROKER_K8S_OIDC_ISSUER, so list that account in BROKER_API_ACCOUNTS instead of sharing a bearer with Dispatch’s server