Skip to main content
Skip to main content

Secrets and the credentials proxy

A sandbox gets a credential in one of two ways. A secret is delivered into the sandbox as an environment variable, so the code inside holds the real value. An integration keeps the credential outside: the sandbox calls the credentials proxy, which adds the credential to the request on its way out. Both take their values from the environment variables of the workspace service.

Use the proxy wherever the credential is an HTTP header sent to an API, so an agent can't read or leak it. Use secrets when a tool needs the value itself, for example a database URL.

Where secrets come from​

Credentials live in the environment variables of the Box Workspace service in Zerops: your own service variables (use secret variables for sensitive values), the project variables the service sees, and references to other services' variables. You add and edit them in the Zerops GUI or in the import YAML, like for any service, see environment variables.

The workspace notices changes to these variables without a restart (workspace environment variables). Sandboxes and integrations refer to the variables by name only. The workspace keeps no copy of a value, only a fingerprint of what each sandbox was given.

Variables whose names start with ZBOX_, such as the workspace API key, are never delivered to a sandbox or injected by the proxy.

Secrets​

Choose the variables a sandbox gets when you create it. Each --secret names one variable exactly, there are no patterns:

# deliver two workspace variables into the sandbox
zbox new agent --secret DATABASE_URL --secret STRIPE_KEY

A name the workspace doesn't have is skipped without an error. If you add the variable later, the sandbox becomes stale.

To deliver a variable under another name inside the sandbox, write GUEST=NAME. This lets each sandbox get its own value under the name its code expects:

# the workspace variable DB_URL_SB42 becomes DATABASE_URL in the sandbox
zbox new agent --secret DATABASE_URL=DB_URL_SB42

--secrets all delivers every variable of the service except the ZBOX_ ones, including the project variables and the Zerops system variables it sees. Any change to one of them makes the sandbox stale, so prefer the list of names.

In the sandbox's policy, the selection is the secrets object. Its mode is NONE, ALLOWLIST (the variables in names) or ALL_ENVS (--secrets all). aliases maps the sandbox's name to the workspace variable, for example {"DATABASE_URL": "DB_URL_SB42"}. An alias wins over a workspace variable of the same name, and a change of the aliased variable makes the sandbox stale like any other. Aliases are delivered with ALLOWLIST and ALL_ENVS, and NONE refuses them.

The secrets ceiling of the workspace policy can limit the modes and the variables a sandbox may select or alias, and refuses anything else with policyViolation. The selection is part of the sandbox's policy, so you can change it later with zbox policy <name> -f or in the Secrets section of the console's Policy tab. The change only takes effect when the secrets are redelivered. Templates don't carry a selection: you choose the secrets for each sandbox, including sandboxes created from a template.

How secrets reach the sandbox​

Secrets are set as environment variables, under the same names or their aliases, for the sandbox's main process and every command you start in it: zbox exec, shells, SSH sessions and detached processes (running commands). They aren't written to the sandbox's disk, except a delivered CODEX_AUTH_JSON, which is written to /root/.codex/auth.json where Codex reads it. When a sandbox is parked with memory, they are part of its memory image, which is stored on the workspace's encrypted disk.

They are delivered when the sandbox is created (from a base or from a warm template) and on every boot from its disk: a wake after a park without memory, a resize, a restore from a disk snapshot and a wake with --redeliver-secrets. A wake that resumes the memory image keeps the values the sandbox already had.

The API never answers a secret's value. The sandbox's policy holds only the names, and the environment the API shows has names only: the launch environment as envNames of the sandbox's launchContract, and the environment of a session's command as envNames of its command. Delivered secrets are in neither list.

If you deliver a connector's own variable, for example --secret GH_TOKEN, the sandbox holds the real credential and the proxy isn't involved (connectors).

Secret-bearing sandboxes​

A sandbox that received at least one secret is secret-bearing (secretBearing in the API, delivered, no fork in zbox get). The mark stays for the sandbox's life: a later boot without secrets doesn't clear it, because a delivered file stays on the disk.

A secret-bearing sandbox can't be forked or snapshotted with memory, and neither it nor any of its snapshots can be exported. All of these fail with secretBearing, so its secrets can't spread into other sandboxes.

Disk snapshots work. A snapshot records in its own secretBearing field whether the sandbox was secret-bearing when it was taken, and a sandbox restored from such a snapshot starts secret-bearing. A restored sandbox boots cold and is given the current values of its selection, like any cold boot.

Stale secrets​

A sandbox is stale when its selection would now pick something different from what it was given: a selected variable changed its value, was added or removed, or you changed the selection. A first selection on a sandbox that had none counts too. zbox get shows STALE, the API shows secretsStale: true.

A running stale sandbox keeps working with the values it has. A parked one isn't woken implicitly: commands, shells, SSH and requests to its ports fail with secretsStale (the router answers 503 SECRETS_STALE), and so does a plain zbox wake. Wake it explicitly with the current values:

zbox wake agent --redeliver-secrets

In the console, it's the Wake and redeliver secrets button on the sandbox page.

Redelivery boots the sandbox from its disk: the memory image is discarded, so running processes start again. On a running sandbox, zbox wake --redeliver-secrets does the same in one operation: the disk is synced, the sandbox is shut down and booted again with the current values. A resize of a running sandbox boots it again the same way, so it delivers the current values too, stale or not.

Credentials proxy​

The credentials proxy runs in the workspace and calls an API on a sandbox's behalf. The sandbox sends a plain HTTP request without any credential. The proxy checks that the sandbox has the integration attached, reads the credential from the workspace's environment variable, adds it to the request and forwards it to the target. The credential never enters the sandbox: it isn't in its environment or on its disk.

Integrations​

An integration is a named target that the proxy can call: a target URL, the name of the workspace variable that holds the credential, and how the proxy sends it. Integrations belong to the workspace, and you attach them to the sandboxes that may use them.

zbox integration create stripe --target-url https://api.stripe.com --credential-env STRIPE_KEY --inject bearer
zbox integration list
--injectWhat the proxy sends
bearerAuthorization: Bearer <value>
basicAuthorization: Basic from a user:password value. A value without a colon is used as the password of the user x-access-token, which GitHub accepts for tokens.
header:<name>The value as is in the header <name>, for example header:X-Api-Key

The target is an http or https URL, on the internet or a service in your project, for example http://api:3000. The request path is appended to the target URL's path. With --strip-prefix /api, a request for /api/v1/items is forwarded as /v1/items.

The proxy reads the credential on every request. A new value of the variable, or a change made with zbox integration set, applies to the next request, without touching any sandbox. An integration can't be deleted while a sandbox has it attached.

The name of an integration is a lowercase DNS label: letters, digits and hyphens.

Coming soon

Rate limits per integration, so the proxy caps how many requests sandboxes send to a target.

Attach an integration​

A sandbox uses an integration through an attachment under a label, which is the integration's name unless you pick another one. Attach integrations when you create the sandbox, or later:

# at create: the label is the integration name, or label=integration
zbox new agent --integration stripe --integration pay=stripe
# later, on a running sandbox
zbox attach agent stripe
zbox attach agent billing --integration stripe
zbox attachments agent
zbox detach agent billing

In the console, the same is the sandbox's Integrations tab, and the workspace's integrations are on the Integrations page.

Inside the sandbox, an attached integration is the host <label>.int.workspace over plain HTTP, also when the target is an https URL:

curl http://stripe.int.workspace/v1/customers

Any Authorization, Proxy-Authorization or X-Api-Key header the sandbox sends is removed before the credential is added. Responses are streamed back as they arrive and aren't filtered, so a target that echoes request headers back shows the credential to the sandbox.

Attaching and detaching on a running sandbox applies at once. A label works only in the sandbox it's attached to. Forks and restored sandboxes get the attachments of their parent, and terminating a sandbox removes its attachments.

Caution

A label attached while the sandbox is parked with memory doesn't resolve after the wake. Attach to running sandboxes, or detach and attach the label again once the sandbox runs.

The proxy makes the request from the workspace, so it works in every network mode except airgapped. A sandbox with project-only network can still reach an internet API through an integration.

When the proxy can't serve a request, it answers with a JSON error:

StatusCodeMeaning
404notAttachedThe sandbox has no attachment with this label.
502credentialMissingThe workspace has no value in the integration's credential variable.
502upstreamFailedThe target didn't answer.
Coming soon

Peer attachments: a label that points at a port of another sandbox, so sandboxes can call each other through the proxy.

Connectors​

A connector is a ready-made provider login (Claude Code, GitHub, OpenAI Codex): it stores the credential in a workspace variable and manages the integrations that the proxy injects it for, and it also sets up the provider's tools inside the sandbox. See connectors.