> ## Documentation Index
> Fetch the complete documentation index at: https://instabox.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Org Secrets

> Credentials the outbound proxy adds to HTTPS headers, so the real value never lands in the machine.

Org Secrets are encrypted `key=value` pairs owned by an org. A machine sees each one as an environment
variable whose value is an opaque placeholder (`ibsec_...`). Your app sends that placeholder in its usual
header, to its usual HTTPS URL, and the outbound proxy swaps in the real value for the hosts the Secret
allows. The real key never lands on the machine's disk or in its snapshots.

## Save a Secret and give it to a machine

**1. Save a Secret**

To save a Secret, you need to be an org owner or admin.

```bash theme={null}
instabox secrets set SERVICE_API_KEY --org team --host api.example.com
```

The CLI reads the value from stdin, or from a prompt without echo.

**2. Give it to a machine**

```bash theme={null}
instabox create --name app --org team --secrets all
instabox secrets show app
```

**3. Use it as usual**

The app reads `SERVICE_API_KEY` and sends it as it always would, such as
`Authorization: Bearer $SERVICE_API_KEY` or `X-Api-Key: $SERVICE_API_KEY`. There is no separate
retrieval API.

New machines made in the dashboard receive all current and future proxy Secrets of their workspace,
once Secrets is enabled for your account and on the server. Existing machines keep their selections. To
change those, use the CLI or the HTTP API, below. A machine uses Secrets from its own org only.

## Scope a model provider key to its API

`--ai` scopes a model provider key to its API. The proxy puts the key into `x-api-key` or
`Authorization` only for that host.

```bash theme={null}
instabox secrets set ANTHROPIC_API_KEY --ai anthropic
instabox secrets select app --secrets ANTHROPIC_API_KEY
```

| `--ai` | Host |
| - | - |
| `anthropic` | `api.anthropic.com` |
| `openai` | `api.openai.com` |
| `gemini` | `generativelanguage.googleapis.com` |
| `openrouter` | `openrouter.ai` |

You can repeat `--ai` and combine it with `--host`.

## Choose the hosts a Secret may go to

Each Secret lists the exact hosts it may be sent to. Without `--host`, it may go to every public HTTPS
destination (`*`). Partial wildcards such as `*.example.com` are not supported.

> [!WARNING]
>
> With `*`, any process in the machine can send the Secret to any public HTTPS host, including one it
> controls. When the credential must stay with one service, use exact hosts.

## Change a running machine's Secrets

```bash theme={null}
instabox secrets select app --secrets SERVICE_API_KEY   # replaces the previous selection
instabox secrets push app
instabox secrets show app
```

* `--secrets all` includes Secrets created later. `--secrets none` blocks the machine from all Secrets.
* Rotating a value keeps its placeholder, so apps need no restart.
* If you remove a Secret and recreate it with the same name, an explicit grant to the old one is not
  restored.

## Use a Secret for git over HTTPS

Git sends its token as Basic auth, which the proxy handles, so `git clone` and `git push` to private
repos work with a placeholder. Add `github.com` (for git) to the Secret's hosts next to `api.github.com`
(for `gh`). Then give git a credential helper that returns the placeholder, such as
`gh auth setup-git` for `GH_TOKEN`.

## Import a .env file

```bash theme={null}
instabox secrets import .env --org team --host api.example.com
```

The whole file is validated and written at once, and every entry shares that host scope. Importing does
not give any machine the Secrets. Select them with `instabox secrets select`, as above.

## Turn off QUIC in browsers

The proxy sees HTTPS over TCP port 443 only. HTTP/3 (QUIC, on UDP 443) bypasses it, so the API receives
the placeholder and usually refuses it, for example with `401`. curl and most HTTP libraries use TCP,
but Chromium may still pick HTTP/3. In a Chromium-based browser that sends Secrets, turn QUIC off:

```bash theme={null}
chromium --disable-quic
```

In Playwright or Puppeteer, pass the same flag: `chromium.launch({ args: ['--disable-quic'] })` or
`puppeteer.launch({ args: ['--disable-quic'] })`. The browser must also trust the sandbox CA with normal
certificate checks. Do not turn verification off.

## Put the real value in the machine with a direct Secret

A direct Secret (`--direct` in the CLI, `delivery: "env"` in the API) puts the **real value** in the
machine, as a variable or a file. Use one when the proxy cannot carry the credential.

> [!WARNING]
>
> Any code in the machine, including an agent or a package it installs, can read and copy a direct
> Secret. Prefer the proxy for HTTPS API keys. To revoke a direct credential, rotate it at its service,
> because copies already read stay.

| Use | Delivery |
| - | - |
| API keys and tokens in HTTPS headers, git over HTTPS | proxy (default) |
| Login files (`~/.local/share/opencode/auth.json`, `~/.config/gh/hosts.yml`), SSH keys | direct, as a file |
| Database, Redis, or SMTP URLs. Request signing (AWS SigV4, service-account JWTs, HMAC). Keys in bodies or query strings. Pinned clients. | direct, as a variable or a file |

As an owner or admin, save the Secret as direct. Then reference it from a machine as a variable, a home
file, or both:

* `--secret-env VAR=SECRET`, or `--secret-env SECRET` for the same name
* `--secret-file PATH=SECRET`

```bash theme={null}
instabox secrets set OPENCODE_AUTH --direct < ~/.local/share/opencode/auth.json
instabox secrets set PROD_PG --direct
instabox create --name agent --secret-file .local/share/opencode/auth.json=OPENCODE_AUTH --secret-env DATABASE_URL=PROD_PG
instabox secrets select agent --secret-env none          # remove every direct reference
instabox exec agent --secret-env DATABASE_URL=PROD_PG -- psql -c 'select 1'   # this command only
```

Who may reference direct Secrets, and the rules for names and paths, are under
[Reference](#reference) below.

## Select Secrets over the HTTP API

Select Secrets when you create a machine with `POST /v1/machines`:

```json theme={null}
{
  "name": "app",
  "templateId": "agent-browser",
  "orgId": "org_...",
  "secrets": ["SERVICE_API_KEY"],
  "directSecrets": [
    { "secret": "PROD_PG", "env": "DATABASE_URL" },
    { "secret": "OPENCODE_AUTH", "file": ".local/share/opencode/auth.json" }
  ]
}
```

After that, use `/v1/machines/{id}/secrets`:

* `PATCH` sets a new selection. Explicit `null` selections are rejected.
* `GET` returns metadata and status.
* `POST` refreshes delivery now.

`directSecrets` replaces the machine's direct references. `"envSecrets": ["DATABASE_URL"]` is the
same-name shorthand. Send one or the other, not both. `"secrets": "all"` never includes env (direct)
Secrets, which reach a machine only through `envSecrets` or `directSecrets`.

For one command, `POST /v1/machines/{id}/exec` takes `"secretEnvs": {"DATABASE_URL": "PROD_PG"}`.

To save a Secret over HTTP, see "Save a Secret" under [Reference](#reference). Full schemas are in the
[API reference](https://instabox.sh/docs/api-reference/introduction.md).

## How it works

The proxy carries proxy Secrets, and files and variables carry direct ones. The details below say what
the proxy changes in a request, when a change reaches a machine, and which processes see the values.

> [accordion] What the proxy replaces, and what it does not
>
> The proxy checks the machine's authorization, the host scope, and the upstream TLS certificate. Then
> it replaces the placeholder inside a header value. It supports HTTPS on port 443 only, and it does
> not substitute URL parameters or request bodies.
>
> * For HTTP Basic auth in `Authorization`, the proxy decodes the credential, replaces the placeholder
>   inside `user:password`, and re-encodes it. Any other encoding or signing of the placeholder
>   prevents replacement.
> * The proxy intercepts all supported HTTPS hosts, even without a placeholder. Clients must trust the
>   sandbox CA and cannot pin certificates.
> * The proxy closes intercepted connections to private addresses.
> * WebSockets work. The proxy replaces placeholders in the handshake headers, not in messages.
> * The proxy drops `Alt-Svc` from the responses it relays. Chromium may still choose HTTP/3 from DNS,
>   or from an earlier visit saved in its profile.

> [accordion] Delivery status and timing
>
> `instabox secrets show` reports proxy authorization and placeholder setup separately. A proxy update
> is pending until acknowledged, and an acknowledged lease expires within at most 60 seconds without
> renewal. Requests already admitted, streams included, may finish.
>
> Servers with per-sandbox usage show each Secret's proxied and denied request counts in
> `secrets show` and `GET /v1/machines/{id}/secrets`.

> [accordion] Which processes see the variables
>
> Secrets are written to `/run/instabox/secrets/env` (tmpfs, mode 0600, owned by the machine's user)
> and loaded by `/etc/profile.d/zz-instabox-secrets.sh`. Login shells get them: SSH, `instabox exec`,
> and the exec API.
>
> Processes that skip the profile do not get them, such as systemd units, cron, and `bash -c` without
> `-l`. Running processes keep their old environment until restarted.

> [accordion] Where a direct value lives
>
> Variables go to `/run/instabox/secrets/env`. A file's value is written to
> `/run/instabox/secrets/files/` (tmpfs, mode 0600, owned by the machine's user), and the home path
> becomes a symlink to it. So the disk, disk snapshots, and forks never hold the value.
>
> If the path already holds a file of the machine's, the file is left alone and `secrets show` reports
> the error. Move the file and run `secrets push`. A tool that rewrites its login file replaces the
> link with a real file on the disk, which is then the machine's own.

> [accordion] Rotation, removal and forks
>
> A new value reaches running machines within about 10 seconds. Files change in place and new shells
> get the new variable, but running processes keep the old one until restarted. Stopped and paused
> machines get the current value when they start or resume. Deleting the Secret or the reference
> removes the variable and the file at the next delivery.
>
> Forks copy the source's Secret selection, both proxy and direct, and resolve the values again. A
> machine created from a snapshot takes only its own selection.

## Reference

> [accordion] Save a Secret: PUT /v1/orgs/\{id}/secrets/\{name}
>
> | Field | Required | Meaning |
> | - | - | - |
> | `value` | yes | The credential, up to 64 KiB of UTF-8. Proxy values must fit in an HTTP header. |
> | `delivery` | yes | `"proxy"` or `"env"`, on every write, including a rotation. Without it: `400 delivery is required`. |
> | `hosts` | proxy only | Up to 32 exact hostnames, with no scheme, port, path, or wildcard. `"*"` allows every public HTTPS destination. Omitted or `[]` means `["*"]`. An env Secret with non-empty `hosts` is rejected. |
>
> A new Secret answers `201 Created`, and a replacement `200 OK`. Both carry metadata and never the
> value: `id`, `orgId`, `name`, `delivery`, `hosts`, `version`, `size`, `createdAt`, and `updatedAt`.
>
> `POST /v1/orgs/{id}/secrets/import` takes `{"values":{...},"delivery":...,"hosts":[...]}`, with the
> same rules for every entry. A missing `delivery` there answers `400 delivery must be proxy or env`.
> The CLI saves `proxy` unless you pass `--direct`.
>
> This example saves a proxy Secret in your personal org. It keeps the token header in a mode 0600 file
> and sends the body on stdin, so neither appears in the process list:
>
> ```bash theme={null}
> instabox whoami >/dev/null
> AUTH=$(mktemp)
> jq -r '"Authorization: Bearer " + .accessToken' "${XDG_CONFIG_HOME:-$HOME/.config}/instabox/credentials.json" >"$AUTH"
> ORG=$(curl -sS "https://api.instabox.sh/v1/orgs" -H @"$AUTH" | jq -r '.orgs[] | select(.personal) | .id')
> curl -sS -X PUT "https://api.instabox.sh/v1/orgs/$ORG/secrets/SERVICE_API_KEY" -H @"$AUTH" -H 'Content-Type: application/json' --data-binary @- <<'EOF'
> {"value":"example-api-key","delivery":"proxy","hosts":["api.example.com"]}
> EOF
> rm -f "$AUTH"
> ```
>
> A heredoc you type still lands in shell history. For a real credential, build the body from a file
> with `jq -Rs '{value: rtrimstr("\n"), delivery: "proxy", hosts: ["api.example.com"]}' <key.txt`, and
> pipe it into the same `curl`. For an env Secret, send `{"value":"postgresql://...","delivery":"env"}`
> to `.../secrets/DATABASE_URL`.

> [accordion] Who can do what
>
> * **Manage values** (create, import, change, delete): an org owner or admin, signed in, or the
>   workspace's own `workspace:admin` API key sent as `X-API-Key`. After `instabox login`, the CLI's
>   access token works, and `instabox whoami` refreshes it. A personal org's key and narrower keys get
>   `403 missing_scope`. Management reads never return values.
> * **Read** Secret metadata and machine selections: keys with `machines:read`.
> * **Change selections and refresh delivery**: keys with `machines:write`.
>
> To work headless, put the `workspace:admin` key in `INSTABOX_API_KEY`. Org commands then default to
> the key's org, and `secrets rm` needs `--yes` without a terminal.

> [accordion] Who can reference direct Secrets
>
> Only an org owner or admin can save a Secret as direct. Anyone who can change a machine
> (`machines:write`) in the org can reference the org's direct Secrets. A proxy Secret can never be
> referenced as direct, and a Secret of another org cannot be referenced at all.

> [accordion] Reserved names
>
> The name is the variable the machine exports, so it follows the rule for
> [Environment variables](https://instabox.sh/docs/machines/environment.md). A name that env refuses is refused here too
> (`400 bad_request`), including `BASH_ENV`, `ENV`, and `IFS`.
>
> A Secret saved under a name before the name was reserved is not delivered. The rest of the selection
> is delivered, `secrets show` reports `not delivered: <names>`, and `secrets push` answers `400`. To
> fix it, save the value under another name, select it, delete the old Secret, and run `secrets push`.

> [accordion] Direct Secrets: names, paths and limits
>
> Variable names follow the Secret name rule. They cannot be `PATH`, `HOME`, `BASH_ENV`, `IFS`, or the
> CA variables, and they cannot start with `INSTABOX_` or `SSH_`.
>
> Paths are relative to the machine's home directory: write `.ssh/id_ed25519`, not
> `~/.ssh/id_ed25519`. The shell turns an unquoted `~/` into your own home, and the CLI takes that back
> as home-relative. A machine has at most 64 direct references.
>
> No API returns a value. `secrets show` and `GET /v1/machines/{id}/secrets` list `directSecrets` as
> names and paths. Each reference (create, select, fork, exec) is audited by name.

> [accordion] Vault, and opting out
>
> Vault (per-user) and Org Secrets are independent stores, with no automatic migration. Existing Vault
> env values, file delivery, and metadata selectors keep working, and a machine can use both. For a
> same-named variable, an explicitly selected Secret wins.
>
> * Omitting `secrets` on create keeps the Vault behavior without enrolling the machine in Secrets.
> * `--no-vault --secrets none` disables delivery from both stores on a new machine.
> * `--no-vault`, and the SDK option `vault: false`, affect only Vault. Clearing Secret selections
>   affects only Secrets. Where an SDK does not expose Secret selection fields, use the CLI or HTTP API.
> * `instabox vault export NAME --out recovered.json` remains for items kept by an earlier explicit
>   migration. Its output contains the real value.

> [card] [Environment variables](https://instabox.sh/docs/machines/environment.md)
>
> Plain variables, and the names a machine refuses.

> [card] [API keys](https://instabox.sh/docs/security/api-keys.md)
>
> Scopes, including `workspace:admin`.
