# Secrets

URL: https://briq.cloud/docs/concepts/secrets

> Where a credential goes in briq, what each place protects it from, and what never leaves your side.

A credential never goes in a plain `env` value: plain values show in status, in the dashboard and
in the audit log. It goes as a secret, and a secret has four forms.

```text
env: {
  DATABASE_URL: { secret: "DATABASE_URL", value: "postgres://…" },                      // the process reads it
  GITHUB_TOKEN: { secret: "GITHUB_TOKEN", value: "github_pat_…", scope: "boot" },       // the boot steps only
  OPENAI_KEY:   { secret: "OPENAI_KEY", value: "sk-…", hosts: ["api.openai.com"] },    // this briq's own, never in it
  STRIPE_KEY:   { secret: "STRIPE_KEY", scope: "team" }                               // the team's, never in it
}
```

|                              | `{ secret, value }`                                    | `{ secret, value, scope: "boot" }`                                                | `{ secret, value, hosts }`                                                                               | `{ secret, scope: "team" }`                                                                       |
| ---------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Stored                       | Encrypted; masked in status, logs and audit            | The same                                                                          | The same, for this briq                                                                                  | The same, once per team, on the **Secrets** page                                                  |
| In the machine's environment | Yes: the process reads it, and so can `briq_exec`      | Never                                                                             | A placeholder (`briq_secret_…`), never the value                                                         | A placeholder, never the value                                                                    |
| How it reaches the briq      | As a variable                                          | As a file the boot script reads, exports for the steps, then deletes              | It does not: the team's credential gateway puts the value into this briq's outbound requests, to `hosts` | It does not: the gateway puts it in, to the hosts the secret allows, for every briq that names it |
| Who can read it              | The code, and any command run in the briq              | The `steps` of a language recipe or the sandbox, and the download of a `checkout` | Nobody in the briq; the destination host                                                                 | Nobody in the briq; the destination host                                                          |
| For                          | What briq generated for the stack: a database password | What only the boot needs: a private repo, a private registry, a fixture bucket    | A credential this briq's code uses while it runs: the API the task calls                                 | The same credential, used by many briqs and rotated in one place                                  |

## Boot-only secrets

A boot-only secret arrives at `/briq/secrets/<variable>` with the boot files, before the process
starts. The recipe's boot script turns each file into a variable of its own shell, deletes the
directory, runs the `steps`, drops the variables, then starts the process. Nothing that runs
later, the code or a `briq_exec`, finds it in the environment or on disk.

```text
briq_run({
  recipe: "sandbox",
  steps: [{ run: "git clone https://x:$GITHUB_TOKEN@github.com/acme/shop ." }, { run: "npm ci" }],
  env: { GITHUB_TOKEN: { secret: "GITHUB_TOKEN", value: "github_pat_…", scope: "boot" } }
})
```

* A `checkout` token is one: `token: { secret, value }` on a language recipe is boot-only without
  saying so.
* The steps run again at each cold start, and the file is there again for them. A sleeping briq
  that wakes from memory does not boot, so nothing is re-read.
* A fork gets the same boot-only secrets, from its own copy, and runs its steps with them.
* Only a recipe with a boot script takes one: the language recipes, `static` and `sandbox`. An
  image runs its own command, which has nothing that reads the file; `briq_run` refuses it.
* `{{secret:NAME}}` in a sibling's plain value never reads a boot-only secret: that would put it in
  an environment again.

What it does not protect against: a step that leaves a process running (a daemon it started)
keeps its copy of the variable, and a step can write the value to disk. Steps are your commands;
keep them to the install and the clone.

## A secret the briq never holds

Code in a sandbox is code you did not write, and a credential in its environment is one `curl`
away from leaving. Give the secret `hosts` and it never enters the briq: the value is stored for
that briq, its environment gets a placeholder, and its HTTPS requests go through the team's
**credential gateway**, a briq of ours on the team's private network, which swaps the
placeholder for the real value on requests to those hosts, in the allowed places (`locations`:
request headers, or `["header", "body"]` for a form or JSON field too), and rewrites the value
back to the placeholder in what comes back. Any other host is tunnelled untouched. `echo
$OPENAI_KEY` prints the placeholder; a copy of it opens nothing.

```text
briq_run({
  recipe: "sandbox",
  env: { OPENAI_KEY: { secret: "OPENAI_KEY", value: "sk-…", hosts: ["api.openai.com"] } }
})
```

A fork gets the same secret, with a placeholder of its own. Status and the dashboard show the
name and the hosts, never the value.

## Team secrets

A credential many briqs use, or one to rotate in one place, is a team secret: the same
placeholder and gateway, the value held by the team. It is added once, on the dashboard's **Secrets** page or with `POST /v1/secrets`: a
name, the value, the hosts it may be sent to (`api.github.com`, `*.githubusercontent.com`), and
where it goes (request headers; the body too, for a form or JSON field). `briq_secrets` lists
them for an agent, names and hosts only.

A briq names it with no value. Its environment gets a placeholder, and its HTTPS requests go
through the team's **credential gateway**: a briq of ours on the team's private network, which
swaps the placeholder for the real value on requests to the allowed hosts, in the allowed places,
and rewrites the value back to the placeholder in what comes back. Any other host is tunnelled
untouched. The code never holds the credential: `echo $STRIPE_KEY` prints the placeholder, and a
copy of it opens nothing.

```text
briq_run({
  recipe: "sandbox",
  env: { STRIPE_KEY: { secret: "STRIPE_KEY", scope: "team" } }
})
briq_exec({ briq: "bq_…", cmd: ["sh", "-c", "curl -s https://api.stripe.com/v1/balance -u \"$STRIPE_KEY:\""] })
→ the balance: the gateway put the real key in on the way out
```

* Every tool reads the proxy from `HTTPS_PROXY`, and the gateway's CA from `SSL_CERT_FILE`,
  `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` and `GIT_SSL_CAINFO`, which the
  briq gets with the placeholder. Our sandbox image installs the CA system-wide at boot too.
* The gateway puts in only the secrets a briq named in its `env`, its own and the team's, and
  only for that briq: naming a secret at `briq_run` is the grant, `briq_status` shows the names,
  and another briq's placeholder passes through as text.
* The gateway starts with the first briq that names a team secret, stays up while any briq of
  the team is live, sleeps once none is, and is not on the bill.
* Every request it put a value into is one audit line a minute (`secret.use`: the briq, the host,
  the secret), on the **Audit** page.
* A rotation (**Rotate**, or `PATCH /v1/secrets/{id}`) applies within seconds to running briqs: the
  placeholder does not change. Deleting a secret leaves placeholders that open nothing.
* Hosts and locations change from a session only: an agent cannot widen where a secret it cannot
  read may go.

What it does not do, said plainly:

* **HTTPS only, HTTP/1.1 only.** The gateway offers no HTTP/2; clients fall back on their own.
* **Textual substitution.** A credential used to compute a signature (AWS SigV4, an HMAC) does
  not work; a client that validates the key's format at startup may refuse the placeholder.
* **A token exchange** (an OAuth client-credentials grant) returns the new token into the briq.
  Exchange outside, store the result as the secret.
* **Bodies and responses over 8 MB** pass through as they are. The gateway asks for uncompressed
  responses and decompresses one that comes compressed anyway, to scrub it; one it cannot read is
  refused (502) rather than passed on unread.
* **A replaced gateway** (one that errored and was made again) has a new address and CA: briqs
  made before it reach the allowed hosts again once they are made again.
* **The briq can bypass the proxy.** It then carries only placeholders, which are worthless;
  what a bypass still allows is sending the briq's own data elsewhere, which an egress allow-list
  would stop and which briq does not have yet.

## Scope the credential too

A secret in a sandbox is used by code you did not write. Give it a token made for the task: a
fine-grained GitHub token on one repository, read-only, with an expiry; a test-mode key; never a
key to your cloud or your production. The code in a sandbox runs with an open internet, and the
sandbox protects your machine from the code, not the credential from the code.

## What briq generates stays inside

A password briq generated for a Postgres or a Redis of the stack is in the process environment of
the briqs that use it. That is on purpose: it unlocks only the stack's own services, which the
sandbox is meant to reach, and a fork gets fresh ones. Outputs return what briq generated, never a
credential you passed in.

## Good to know

* `briq_status` and the dashboard show `{ secret: NAME }` and, for a boot-only one, `boot only`;
  never a value.
* A secret's value cannot hold a `{{briq:…}}` reference. Put the address in a plain variable.
* In a [stack file](https://briq.cloud/docs/stack-file), a service writes `{ secret: NAME, scope: boot }` and the
  value is passed at `up` in `secrets`; the file never holds a value. A secret with `hosts` takes
  its value at `up` the same way. A team secret is `{ secret: NAME, scope: team }`, with no value
  anywhere.
* The audit log records the names of the secrets a call set, never their values.
* Secrets of a destroyed briq are deleted a day after it. See [Security](https://briq.cloud/docs/security).
