Guides
Credential broker
On this page
Brig keeps an agent's API keys on the host. A route in a policy says which requests may use them: the methods, the paths and, for git, the repositories. The agent holds a session token, and the broker adds the real key to each request it allows. A route on the aws preset is the exception: it hands the sandbox temporary AWS keys. See AWS.
How it works #
- A policy declares a route: an upstream such as
https://api.anthropic.com, the secret that holds its key, and the requests it allows. - The sandbox gets the route's URL and a session token, in the variables the tool already reads, such as
ANTHROPIC_BASE_URL. It never gets the key. - The tool sends its request to the route at
brig.internal, with the session token. brig-broker, on the host, checks the token and the route'srequest.allow. A request the route does not allow gets a403, before the key is read.- The broker adds the key, sends the request upstream on a connection of its own, and redacts the key from the response.
egress: default: deny keeps the agent from going around the broker: the upstream's own address is not reachable from the sandbox.
brig.internal is served over TLS with a CA that Brig creates on the host. The CA is valid for brig.internal alone. The sandbox gets it as /brig/ca/brig.pem, and /brig/ca/bundle.pem holds the image's own roots followed by it. SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE and GIT_SSL_CAINFO point at them. The CA key stays on the host.
Brig starts brig-broker on the first run with a route, and brig info names it in the BROKER row:
BROKER transport brig.internal, routes anthropic; brig-broker pid 4242 on 127.0.0.1:52546; kept on the host: claude-credentials; modes endpointBefore boot, Brig checks that the store holds each route's secret. It reads no value. A missing secret, or a store that does not open, refuses the run with exit code 6, and the message names the command that stores the secret.
Claude Code, Messages API only #
Import your Claude Code login once. Brig stores it as claude-credentials:
brig secret import claude-codeSave this policy as ~/.config/brig/policies/claude-messages.yaml:
apiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
name: claude-messages
description: Claude Code through the broker, Messages API only
spec:
egress:
default: deny
routes:
- name: anthropic
preset: claude-code
credential:
secretRef: { name: claude-credentials }
request:
allow:
methods: [POST]
paths: ["/v1/messages", "/v1/messages/count_tokens"]Attach it to one session, check it, and run the agent:
brig policy attach claude-messages claude-code -n messages
brig policy check claude-code -n messages
brig run claude@messagesThe claude-code preset fills in the rest of the route: the upstream, the header the key goes in, and the variables Claude Code reads. The profile's own copy of the login is not delivered: brig info claude@messages lists claude-credentials as kept on the host.
Request rules #
request.allow takes three lists. Every list you set must match. A list you leave out allows anything.
| List | Matches | Example |
|---|---|---|
methods |
The HTTP method | [GET], [GET, POST, PUT] |
paths |
The path after the route prefix, without the query. * matches one path segment |
["/v1/messages", "/anything/orders/*"] |
repos |
The owner/name of a git request. Only the github-token preset takes it |
[brig-sh/brig, brig-sh/*] |
A request that misses gets a 403 whose body names the rule:
brig broker refused this request
policy: claude-messages
route: anthropic
rule: spec.routes[anthropic].request.allow.methods
reason: method "GET" is not in the allow list [POST]
sandbox: brig-claude-code-messagesPaths are globs, not regular expressions. /anything/orders/* allows /anything/orders/42 and refuses /anything/orders/42/items. The methods apply to the whole route: to allow GET on one path and POST on another, write two routes.
The broker matches the path as it receives it. It refuses with a 400 any path that the upstream could read as a different path. These are a . or .. segment, an empty segment, an escaped /, \, . or %, a backslash, and the Unicode characters that normalize to these.
Session tokens #
Brig mints a session token for each route on every exec. The token lasts 12 hours, and it is valid for one sandbox and one route. The sandbox cannot mint a token.
The tool sends the token in one place: Authorization: Bearer, the basic-auth password on a basicAuth route, or the header the route puts the key in, such as x-api-key. A request that carries a live token of the sandbox anywhere else gets a 403, and the upstream is never dialed:
brig: refused: the session token was sent outside its carrier (route github, in body)The broker looks in the method, the path, the query, the headers and the first 8 MiB of the body. It finds the token raw, percent-escaped, JSON-escaped, in hex and in base64, also when it is split across lines or the body is gzip or deflate. A conversation whose history holds the token is refused on every later turn, until the token expires. A new conversation works. The check stops a leak by accident or by an injected prompt. An agent that means to hide its token can.
Two APIs with their own rules #
A route needs no preset. Name the upstream, the header the key goes in, and the variables the sandbox gets. This policy gives one API read-only access, and another three methods under one glob. Each route also sends a static header beside its key.
Store a key for each API:
printf '%s' "$INVENTORY_KEY" | brig secret create inventory-key
printf '%s' "$ORDERS_KEY" | brig secret create orders-keySave the policy as ~/.config/brig/policies/two-apis.yaml:
apiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
name: two-apis
description: two APIs, each with its own verbs, paths and headers
spec:
egress:
default: deny
routes:
- name: inventory
upstream: https://postman-echo.com
credential:
secretRef: { name: inventory-key }
inject:
headers:
set:
- name: Authorization
value: "Bearer {value}"
- name: X-Tenant
value: acme
guest:
env:
INVENTORY_URL: "{route.url}"
INVENTORY_TOKEN: "{session.token}"
request:
allow:
methods: [GET]
paths: ["/get", "/headers", "/status/200"]
- name: orders
upstream: https://httpbingo.org
credential:
secretRef: { name: orders-key }
inject:
headers:
set:
- name: X-Api-Key
value: "{value}"
- name: X-Request-Source
value: brig-demo
guest:
env:
ORDERS_URL: "{route.url}"
ORDERS_TOKEN: "{session.token}"
request:
allow:
methods: [GET, POST, PUT]
paths: ["/anything/orders/*", "/post", "/put"]{value} is the key. {route.url} and {session.token} are what the sandbox gets in place of the upstream and the key. A tool in the sandbox sends the session token as Authorization: Bearer, whatever header the route puts the key in:
brig policy attach two-apis claude-code -n apis
brig sh claude@apis
# inside the sandbox
inv() { curl -s -H "Authorization: Bearer $INVENTORY_TOKEN" "$@"; echo; }
ord() { curl -s -H "Authorization: Bearer $ORDERS_TOKEN" "$@"; echo; }
inv $INVENTORY_URL/get
ord -X PUT -d '{"qty":3}' $ORDERS_URL/anything/orders/42A session token works on its own route only. The orders route answers the inventory token with a 401.
postman-echo.com and httpbingo.org are third-party services, used here because they echo a request back. Replace them with your own APIs.
git, fetch only #
The github-token preset sends a token to https://github.com as a basic-auth password. git in the sandbox rewrites https://github.com/ and git@github.com: to the route, and its credential helper answers with the session token.
gh auth token | brig secret create gh-tokenapiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
name: github-fetch-only
description: git to github.com, fetch-only, one repository
spec:
egress:
default: deny
routes:
- name: github
preset: github-token
credential:
secretRef: { name: gh-token }
request:
allow:
repos: [brig-sh/homebrew-brig]
paths: ["/*/*/info/refs", "/*/*/git-upload-pack"]A fetch uses info/refs and git-upload-pack. A push uses info/refs and git-receive-pack. The query that tells the two info/refs requests apart is not matched, so the push is refused at git-receive-pack, before the token is read.
git sends a credential only after a challenge. So on a basicAuth route, a 401 for a missing, forged or expired session token carries WWW-Authenticate: Basic realm="brig", and git then asks its helper. A route that carries the token in a header gets no challenge.
AWS #
AWS signs each request with the secret key, so the broker cannot add a key to a request on its way out. A route on the aws preset has no upstream. It answers the sandbox's AWS SDK as the AWS container credential provider, with temporary keys, and the SDK calls AWS with them itself. The sandbox holds real AWS keys, and they work outside the sandbox too until they expire. The aws route states what that hands the sandbox.
The route's secret holds a pointer to an AWS profile on your host, and no key. Declare it with an aws source in a profile of your own (see Sources):
secrets:
- name: aws-creds
sources:
- from: awsImport it, and name the role the sandbox assumes:
brig secret import <profile> aws-creds --aws-profile dev \
--role-arn arn:aws:iam::111122223333:role/brig-coding-agent--identity self gives the sandbox your own identity in that profile instead, so it can do what you can. import refuses a line that names neither mode, or both. An AWS profile behind a route has the flags and the checks import makes.
apiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
name: aws-sandbox
spec:
egress:
default: deny
allow:
- host: "*.amazonaws.com"
routes:
- name: aws
preset: aws
credential:
secretRef: { name: aws-creds }The SDK calls AWS directly, so a default: deny policy needs an allow rule for the endpoints the agent uses. A host rule does not make the access read-only. *.amazonaws.com allows every call the keys allow, writes included, and resources in other accounts too. The role's policy is what limits the calls.
The route itself needs no allow rule. The sandbox reaches it in plain HTTP at 169.254.170.2, the address of the AWS container credential provider, which the runtime diverts to the broker. The AWS SDKs take plain HTTP for that URL only to loopback and a few link-local addresses, and they do not trust the brig CA. The broker serves only a route that vends on that address. Any other route gets a 404 there, before its token is read. On hull, the divert needs hull v0.1.0 or later, and Brig refuses an older one with exit code 7.
The sandbox gets:
AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.2/<sandbox>/r/aws
AWS_CONTAINER_AUTHORIZATION_TOKEN=<session token>
AWS_REGION=<the region of the AWS profile>
AWS_DEFAULT_REGION=<the region of the AWS profile>The SDK sends the token as the whole Authorization value, with no scheme. Only a route that vends takes it that way. The route serves its own URL alone, and the preset allows GET.
Each run and exec prints the identity the route hands out. -q hides the line:
brig: route aws: AWS role arn:aws:iam::111122223333:role/brig-coding-agent, assumed from your profile dev
brig: route aws: AWS as your own identity (profile dev); the sandbox can do what you canThe broker fetches the keys by running the host's aws CLI, with every AWS_ variable removed from its environment: aws sts assume-role in role mode, and aws configure export-credentials in self mode. The role session is named after the sandbox, so CloudTrail shows which sandbox made a call. It lasts 30 minutes by default. The broker holds the keys in memory, and fetches new ones when those it holds have less than 20 minutes left. The audit line names the access key id it handed out, and never the secret key or the session token.
An AWS SDK reads its environment and ~/.aws before the container credential provider. Brig refuses a run with an aws route when the profile or BRIG_FORWARD_ENV hands the sandbox another AWS credential: AWS_ACCESS_KEY_ID, AWS_PROFILE, an AWS_CONTAINER_* variable or any other AWS credential variable. It also refuses the run when ~/.aws would be in the guest home. Brig does not check the image.
A new run checks the route before boot, and fetches keys. When aws can get no credentials, for instance after the SSO login lapsed, Brig refuses the run with exit code 6 and names aws sso login --profile <profile>. A running sandbox that needs new keys then gets a 502 with the same fix. brig stop revokes the sandbox's session tokens, so it fetches no more keys. Keys already handed out stay valid until they expire.
GCP #
A route on the gcp preset forwards the sandbox's requests to several Google APIs with one session token. Each API is a service of the route, with its own request.allow. The broker replaces the session token with a Google access token on the way out, so the guest never holds a Google token.
The route's secret holds a pointer to a gcloud configuration on your host, and no token. Declare it with a gcloud source in a profile of your own:
secrets:
- name: gcp-creds
sources:
- from: gcloudImport it, and name the service account the sandbox acts as:
brig secret import <profile> gcp-creds --gcloud-configuration default \
--impersonate-service-account brig-readonly@my-project.iam.gserviceaccount.comThe service account's IAM roles limit what the sandbox can do. Your account needs roles/iam.serviceAccountTokenCreator on it. --identity self gives the sandbox your own identity instead, so it can do what you can. import refuses a line that names neither mode, or both. It mints one token before it stores the pointer, so a lapsed login is refused there, and it prints the identity the sandbox will act as. A gcloud login behind a route has the flags and the checks.
The secret can also be an access token, stored as it is. It is your own identity, and it expires in about an hour, after which you store a new one:
gcloud auth print-access-token | brig secret create gcp-credsapiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
name: gcp-readonly
spec:
egress:
default: deny
routes:
- name: gcp
preset: gcp
credential:
secretRef: { name: gcp-creds }
services:
- name: storage
request:
allow:
methods: [GET]
paths: [/storage/v1/b, /storage/v1/b/*, /storage/v1/b/*/o, /storage/v1/b/*/o/*]
- name: cloudresourcemanager
request:
allow:
methods: [GET]
paths: [/v1/projects/*, /v3/projects/*]
- name: logging
request:
allow:
methods: [POST]
paths: [/v2/entries:list]A service is one of these:
name |
Upstream | gcloud endpoint override ends in |
|---|---|---|
storage |
https://storage.googleapis.com |
/storage/v1/ |
cloudresourcemanager |
https://cloudresourcemanager.googleapis.com |
/ |
compute |
https://compute.googleapis.com |
/compute/v1/ |
logging |
https://logging.googleapis.com |
/ |
iam |
https://iam.googleapis.com |
/ |
The sandbox reaches a service at https://brig.internal/<sandbox>/r/gcp/<service>/<rest>. The broker matches <rest> against the service's request.allow, and sends it to the service's host. A service with no request allows every request to it. For storage that includes the XML API, whose paths do not look like the JSON ones, so give each service paths. In a path, * also matches a :verb suffix: methods: [GET, POST] with /v1/projects/* also allows POST /v1/projects/x:setIamPolicy. A service that allows POST lists each path with its verb, such as /v2/entries:list.
The broker calls Google from the host, so the policy needs no egress rule for Google. The sandbox gets:
BRIG_GCP_URL=https://brig.internal/<sandbox>/r/gcp
BRIG_GCP_TOKEN=<session token>
CLOUDSDK_API_ENDPOINT_OVERRIDES_<SERVICE>=<BRIG_GCP_URL>/<service><override end>, for each service
CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE=/brig/ca/bundle.pemgcloud reads none of the other CA variables, so Brig sets its own. Brig writes the gcloud variables itself, and refuses a guest.env entry that names one. gcloud takes the session token as its access token from a file. The guest writes the file and points gcloud at it:
# inside the sandbox
printf '%s' "$BRIG_GCP_TOKEN" > /tmp/gcp-token
export CLOUDSDK_AUTH_ACCESS_TOKEN_FILE=/tmp/gcp-tokenThe broker refuses these requests before it reads the credential:
| Request | Answer | check= |
|---|---|---|
| A service the route does not list | 403 |
request.services |
A header X-HTTP-Method-Override, X-HTTP-Method or X-Method-Override, or a query parameter $httpMethod or httpMethod in any case |
403 |
request.method-override |
A batch request: batch as the first segment after the service |
403 |
request.batch |
| A service segment with an escape in it | 400 |
path |
Google APIs serve a request as the method an override names, so a GET that methods allowed would run as another method. A batch carries many requests in one body, and request.allow sees only the outer one.
For a pointer, the broker mints the token on the host. It runs gcloud auth print-access-token with the configuration, and with the service account or, in self mode, your account. gcloud does not print when the token expires, so the broker POSTs the token to Google's tokeninfo endpoint, from the host. It reads the expiry, and refuses a token that belongs to another identity. The broker holds the tokens in memory, per sandbox and route, and writes none to the store, a file or a log. A request gets the token held while it has at least 2 minutes left. Below that, the broker mints a new one. A token lives as long as gcloud makes it, about an hour.
Each run and exec mints a token at its start when the one held has less than 2 minutes left. When gcloud can mint no token, for instance after the login lapsed, Brig refuses the run with exit code 6 and names gcloud auth login --configuration <c>, with your account in self mode. A running sandbox that needs a new token then gets a 502 with the same fix. brig stop revokes the sandbox's session tokens, so the broker forwards none of its requests after that. The gcp route states what the route hands the sandbox.
Client certificates #
An upstream that asks for a client certificate is served by a route with inject.tls. The broker presents the certificate and signs the handshake with its key. The key never leaves the broker, and the sandbox gets the route URL and a session token, as on any route.
Store the certificate and the CA that the upstream's certificate chains to:
brig secret create inventory-client --type tls -f inventory-client.pem
brig secret create example-root-ca --type ca -f example-root-ca.pem--type tls checks that the value holds a certificate, its chain if any, and one private key that matches the certificate. It also checks that the certificate is valid now. --type ca checks that the value holds at least one CA certificate. Certificates and CA bundles lists each refusal.
apiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
name: inventory-mtls
description: read-only access to the inventory service, which wants a client certificate
spec:
routes:
- name: inventory
upstream: https://inventory.internal.example.com
inject:
tls:
clientCertificate: { secretRef: { name: inventory-client } }
ca: { secretRef: { name: example-root-ca } }
guest:
env:
INVENTORY_URL: "{route.url}"
INVENTORY_TOKEN: "{session.token}"
request:
allow:
methods: [GET]
paths: ["/v2/items/*", "/v2/search"]This route has no credential: the certificate identifies the caller. A route can also carry a key in headers.set or basicAuth beside tls. A route on a preset cannot take tls.
ca replaces the system roots on this route. A certificate that chains only to a system root is then refused. Without ca, the route verifies the upstream with the system roots. Other routes keep the system roots.
brig policy check prints a tls row under the route. It names the secrets and reads no value:
$ brig policy check inventory-mtls
inventory-mtls
route inventory preset (none) mode endpoint
tls clientCertificate inventory-client ca example-root-ca
...The broker reads both secrets on each request, as it reads a key. A files: or env: binding of either in the profile is dropped for the run. A route with inject.tls has a connection pool of its own for each sandbox, so two sandboxes never share a TLS connection. When either secret changes, the next request builds a new pool with the new certificate and CA, and needs no restart. A request already in flight finishes on its old connection.
A user keeps one client certificate per route. Brig does not choose between certificates, so every sandbox that uses the route presents the same one.
Brig warns when a certificate stored with --type tls is within three days of its expiry, and once it has expired. The warning comes once per brig plan, and once when brig run or brig sh starts or attaches to a sandbox. It names the command that stores the renewed copy, brig secret update <name> --type tls.
A request that fails a certificate check gets a 502, and the audit line names the check:
check= |
When |
|---|---|
tls.expired |
The client certificate has expired |
tls.certificate |
The client certificate does not parse, is not yet valid, or its key does not match |
tls.ca |
The ca secret cannot be read, or holds no CA certificate |
tls.handshake |
The upstream's certificate does not verify, or the upstream refused the client certificate |
All but tls.handshake fail before the broker dials the upstream.
Route fields #
| Field | Required | What it is |
|---|---|---|
name |
yes | The path segment of the route URL, unique across the policies bound to a run. Lowercase letters, digits and dashes, starting with a letter |
preset |
no | A route that Brig ships. See Presets |
mode |
no | endpoint, the default and the only mode this build serves. A route in another mode is refused before boot, with exit code 7 |
upstream |
unless a preset sets it | A bare https:// origin, such as https://api.example.com:8443. No path, query or user name |
credential.secretRef.name |
unless the route is certificate-only | A secret in Brig's store. It never reaches the sandbox |
credential.field |
no | For a document-shaped secret, the field that is injected |
inject.headers.set, .remove, .allow |
set, unless a preset sets it, the route sets basicAuth, or the route is certificate-only |
See Headers |
inject.basicAuth.username |
no | Sends the key as a basic-auth password |
inject.tls.clientCertificate.secretRef.name |
no | A client certificate and its key, presented to the upstream. See Client certificates |
inject.tls.ca.secretRef.name |
no | The CA certificates of the upstream. They replace the system roots on this route |
services |
on the gcp preset |
The Google APIs the route reaches. Each is {name, request.allow}, and is listed once. Only the gcp preset takes it, and Brig refuses a request.allow on the route itself. See GCP |
guest.env |
unless a preset sets it, or guest.git is set |
{NAME: placeholder} |
guest.git.insteadOf |
no | URL prefixes that git in the sandbox rewrites to the route |
request.allow |
no | See Request rules. An empty list, such as repos: [], is refused. Leave the key out to allow any |
A route is certificate-only when inject sets tls.clientCertificate and neither headers.set nor basicAuth. A route with a credential carries it in headers.set or basicAuth.
Presets #
A preset is a route that Brig ships. A route that names one takes every field it does not set from the preset.
| Preset | Upstream | Key goes in | The sandbox gets |
|---|---|---|---|
claude-code |
https://api.anthropic.com |
Authorization: Bearer, from your Claude Code login. The broker refreshes the login on the host |
ANTHROPIC_BASE_URL, CLAUDE_CODE_OAUTH_TOKEN |
anthropic-api-key |
https://api.anthropic.com |
x-api-key |
ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY |
github-token |
https://github.com |
basic auth, as x-access-token |
a git rewrite of https://github.com/ and git@github.com: |
aws |
none. The route answers the sandbox itself | none. The broker fetches temporary keys through the host's aws CLI |
AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_REGION, AWS_DEFAULT_REGION |
gcp |
none. Each service goes to its own googleapis.com host |
Authorization: Bearer, an access token you store or one the broker mints through the host's gcloud |
BRIG_GCP_URL, BRIG_GCP_TOKEN and the gcloud variables. See GCP |
The two Anthropic presets also keep a reviewed list of Anthropic-Beta values and drop the rest. Print a preset in full with brig policy show --preset claude-code.
A field the route sets overrides the preset's. guest.env merges name by name, and inject.headers.remove is the union of both lists. Any other list is replaced whole. A preset that refreshes its credential, such as claude-code, keeps its upstream and credential.field, and Brig refuses a route that changes either.
Headers #
inject.headers.set takes any number of headers. One of them carries {value}, the key. The others are static, such as X-Tenant: acme above. inject.headers.remove drops headers the tool sends, and Authorization is always dropped, because it carries the session token. A route with inject.basicAuth sends the key as a basic-auth password instead, and takes no headers.set. inject.headers.allow keeps only the listed values of a header the tool sends, and drops the rest.
The broker also drops every header whose name holds an underscore, Forwarded, Via, the X-Forwarded- headers, and the headers that override a method or a URL, such as X-Http-Method-Override. It does not follow a redirect: a 3xx goes back to the tool as the upstream sent it.
The sandbox cannot set a variable of its own through a route: a guest.env value is {route.url}, {session.token} or, on an aws route, {route.region}, never a literal.
The audit log #
The broker writes one line per request to ~/.brig/host/broker/audit.log. A refused request names its rule:
msg=broker workload=spiffe://brig.local/brig/profile/claude-code/session/apis sandbox=brig-claude-code-apis route=orders method=DELETE path=/anything/orders/42 status=403 dur=132.166µs check=request.allow.methods arrived=brig-claude-code-apis policy=two-apis rule=spec.routes[orders].request.allow.methods reason="method \"DELETE\" is not in the allow list [GET, POST, PUT]"A route that vends adds vended=, the access key id it handed out. A route with a client certificate adds cert=, the certificate's fingerprint, and cert.notAfter=, its expiry. The line never holds a key, a token, a header value or a body.
Limits #
- Paths are globs. There are no regular expressions, no query matching and no deny lists.
- The methods apply to a whole route, not to one path.
- One key per route, and one client certificate.
- The broker does not read request bodies for rules. It cannot limit which model or which tool a request names, and it does not refuse a method override in a form-encoded body on a
gcproute. - The broker verifies the upstream with the system's trusted CAs, unless the route's
inject.tls.careplaces them. - An
awsroute hands the sandbox real keys, which work outside the sandbox until they expire. The role's policy limits what they can do. - A sandbox takes one route with
services. Brig refuses a second one across the policies bound to a run, because the guest has one set of gcloud endpoint overrides. - A Cloud Storage object whose name holds a
/cannot be read or written through agcproute. The JSON API sends the name aso/dir%2Ffile, and the broker refuses an encoded/in any path. Listing with a prefix works. - The
gcppreset serves HTTP/1.1 REST APIs only. A gRPC client, such as Pub/Sub or Firestore, does not work through it. - On a
gcproute, Brig does not yet write the token file for gcloud. It does not yet refuse before boot a guest variable that picks another Google identity, such asGOOGLE_APPLICATION_CREDENTIALS. A run does not yet print which Google identity the sandbox acts as.
brig stop and brig rm revoke a sandbox's session tokens. The broker exits when the last sandbox is removed.