# 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](#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](/features/env-variables). The workspace notices changes to these variables without a restart ([workspace environment variables](/zbox/workspace-service#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](/zbox/access#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: ```bash # 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](#stale-secrets). 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: ```bash # 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](/zbox/workspace-settings#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 -f` or in the Secrets section of the console's Policy tab. The change only takes effect when the secrets are [redelivered](#stale-secrets). 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](/zbox/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](/zbox/lifecycle#resize-a-sandbox), 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](/zbox/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](/zbox/lifecycle#fork-a-sandbox) or snapshotted with memory, and neither it nor any of its snapshots can be [exported](/zbox/export). 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: ```bash 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](/zbox/lifecycle#resize-a-sandbox) 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. ```bash zbox integration create stripe --target-url https://api.stripe.com --credential-env STRIPE_KEY --inject bearer zbox integration list ``` | `--inject` | What the proxy sends | |---|---| | `bearer` | `Authorization: Bearer ` | | `basic` | `Authorization: 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:` | The value as is in the header ``, 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. :::note 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: ```bash # 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 `