# Org Secrets

https://instabox.sh/docs/secrets.md

Credentials added to HTTPS headers by the outbound proxy, scoped to your org.

Secrets are encrypted key=value pairs owned by an org. A sandbox can only use Secrets from its own org. Each Secret lists the exact hosts it may be sent to, or `*` for every public HTTPS destination. Partial wildcards such as `*.example.com` are not supported. Once Secrets is enabled for your account and supported by the server, new Dashboard sandboxes receive all current and future proxy Secrets from their workspace. Existing sandboxes keep their selections. Use the CLI or HTTP API to change a sandbox's selection.

### How a Secret reaches an API
The sandbox sees the named environment variable, but its value is an opaque placeholder. Applications keep their normal HTTPS URL and header name. The outbound proxy checks sandbox authorization, host scope and the upstream TLS certificate before replacing the placeholder within a header value. Version one supports HTTPS on port 443; URL parameters and request bodies are not substituted. HTTP Basic auth in `Authorization` is decoded, the placeholder inside `user:password` is replaced, and the credential is re-encoded; any other encoding or signing of the placeholder prevents replacement.

### Save and select
Sign in with the CLI as an org owner or admin, or run it headless with the workspace's `workspace:admin` API key in `INSTABOX_API_KEY` (org commands then default to the key's org, and `secrets rm` needs `--yes` without a terminal). Values below are read from stdin or a prompt without echo:
```sh
instabox secrets set SERVICE_API_KEY --org team --host api.example.com
instabox create --name app --org team --secrets all
instabox secrets show app
```

Omitting `secrets` in CLI or API creation keeps the existing Vault behavior without enrolling the sandbox in Secrets. To disable delivery from both stores on a fresh sandbox, pass `--no-vault --secrets none`. Without `--host`, a Secret may be sent to every public HTTPS destination (`*`); pass `--host` to limit it.

### AI provider keys
`--ai` scopes a model provider key to its API: `anthropic` (`api.anthropic.com`), `openai` (`api.openai.com`), `gemini` (`generativelanguage.googleapis.com`) and `openrouter` (`openrouter.ai`). It can be repeated and combined with `--host`:
```sh
instabox secrets set ANTHROPIC_API_KEY --ai anthropic
instabox secrets select app --secrets ANTHROPIC_API_KEY
```
The box only sees a placeholder in its environment and files, so the real key never lands on its disk or in snapshots; the proxy puts the key into `x-api-key` or `Authorization` only for the provider's host. Block a box from all Secrets with `secrets select BOX --secrets none` and unblock it with `--secrets all`. Servers with per-sandbox usage enabled show each Secret's proxied and denied request counts in `secrets show BOX` and `GET /v1/machines/{id}/secrets`.

With `*`, any process in an enrolled sandbox can send the Secret to any public HTTPS destination, including a host it controls. All supported HTTPS hosts are intercepted, even without a placeholder. Intercepted connections to private addresses are closed, WebSocket upgrades are unsupported, and clients must trust the sandbox CA without certificate pinning. Prefer exact hosts when the credential must stay with a specific service.

The application reads `SERVICE_API_KEY` as a sandbox-specific placeholder and sends it in its usual header, such as `Authorization: Bearer <placeholder>` or `X-Api-Key: <placeholder>`. No separate Secret retrieval API is needed.

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

For bulk import, `instabox secrets import .env --org team --host api.example.com` validates and writes the whole file atomically; all entries share that host scope. Importing Secrets does not authorize any sandbox to receive them.

### Update a running sandbox
```sh
instabox secrets select app --secrets SERVICE_API_KEY
instabox secrets push app
instabox secrets show app
```
The supplied selection replaces the previous one. Removing and recreating a Secret with the same name does not restore an explicit grant to the old Secret. `all` includes newly created Secrets.

`instabox secrets show` reports proxy authorization and placeholder setup in the sandbox independently. A proxy update is pending until acknowledged, and an acknowledged lease expires within at most 60 seconds without renewal. Requests already admitted, including streams, may finish.

Rotating an existing value keeps its placeholder, so applications need no restart.

### Save a Secret with the HTTP API
`PUT /v1/orgs/{id}/secrets/{name}` creates or replaces a Secret. The body has three fields:

| 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 the request fails with `400 delivery is required`; nothing is inferred from the name or value. |
| `hosts` | proxy only | Up to 32 exact hostnames (no scheme, port, path or wildcard); `"*"` allows every public HTTPS destination. Omitted or `[]` means `["*"]`. An env Secret with non-empty `hosts` is rejected. |

The caller must be an org owner or admin, signed in. After `instabox login`, the CLI's access token works (`instabox whoami` refreshes it first). A workspace API key with `workspace:admin` also works for its workspace, sent as `X-API-Key`. This saves a proxy Secret in your personal org:
```sh
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"
```
The token header is read from a mode 0600 file and the body from stdin, so neither appears in the process list. A heredoc you type still lands in shell history: for a real credential, build the body from a file instead, `jq -Rs '{value: rtrimstr("\n"), delivery: "proxy", hosts: ["api.example.com"]}' <key.txt`, and pipe it into the same `curl`.

A new Secret answers `201 Created`, a replacement `200 OK`, with the Secret's metadata and never its value: `id`, `orgId`, `name`, `delivery`, `hosts`, `version`, `size`, `createdAt`, `updatedAt`. For an env Secret, send `{"value":"postgresql://app:example-password@db.example.com/app","delivery":"env"}` to `.../secrets/DATABASE_URL`. `POST /v1/orgs/{id}/secrets/import` takes `{"values":{...},"delivery":...,"hosts":[...]}` with the same rules for every entry, except that a missing `delivery` there answers `400 delivery must be proxy or env`. The CLI always saves `proxy`.

Where each mode exposes the value:

| Delivery | In the sandbox | Where the real value appears |
| --- | --- | --- |
| `proxy` | `NAME=ibsec_...`, a sandbox-specific placeholder | Only in HTTPS request headers (port 443) to the Secret's `hosts`, where the outbound proxy swaps it in. Never in the sandbox, URLs or request bodies. |
| `env` | `NAME=<real value>` | Inside the sandbox: any process there can read and copy it. No host restriction applies. |

Both modes are written to `/run/instabox/secrets/env` (tmpfs, mode 0600, owned by the sandbox 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 (systemd units, cron, `bash -c` without `-l`) do not, and running processes keep their old environment until restarted. An env Secret reaches a sandbox only when the sandbox selects it in `envSecrets` (`"envSecrets": ["DATABASE_URL"]` on `POST /v1/machines` or `PATCH /v1/machines/{id}/secrets`); `"secrets": "all"` never includes env Secrets.

### HTTP and SDK callers
Use the HTTP API for explicit selections when an SDK does not expose them:
```json
{
  "name": "app",
  "templateId": "universal",
  "orgId": "org_...",
  "secrets": ["SERVICE_API_KEY"]
}
```
Send this to `POST /v1/machines`. After creation, use `PATCH /v1/machines/{id}/secrets` with a new `secrets` selection, `GET` on the same path for metadata and status, and `POST` on that path for an immediate refresh. Explicit `null` selections are rejected.

API keys with `machines:read` can read org Secret metadata and sandbox selections. `machines:write` can change selections and refresh delivery for authorized sandboxes. Creating, importing, changing or deleting org Secret values requires the org owner or admin: a signed-in session or the workspace's own `workspace:admin` API key. A personal org's key and narrower keys cannot manage values (`403 missing_scope`). Management reads never return values. Full endpoint schemas are in the [OpenAPI specification](https://api.instabox.sh/v1/openapi.json).

The SDK creation option `vault: false` disables only per-user Vault delivery; it does not change org Secret selections. Vault inspection and push APIs remain available. When an SDK does not expose Secret selection fields, use the Secret CLI or HTTP API.

### Existing Vault items
Vault and Org Secrets remain independent stores. Existing env values, file delivery and metadata selectors keep working; there is no automatic migration. A sandbox can use both stores, and explicitly selected Secrets take precedence for same-named environment variables. `--no-vault` affects only Vault; clearing Secret selections affects only Secrets. `instabox vault export NAME --out recovered.json` remains available for items retained by an earlier explicit migration. Its output contains the real value.
