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.
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.
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
checkouttoken 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,
staticandsandbox. An image runs its own command, which has nothing that reads the file;briq_runrefuses 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.
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.
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 fromSSL_CERT_FILE,NODE_EXTRA_CA_CERTS,REQUESTS_CA_BUNDLE,CURL_CA_BUNDLEandGIT_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 atbriq_runis the grant,briq_statusshows 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_statusand 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, a service writes
{ secret: NAME, scope: boot }and the value is passed atupinsecrets; the file never holds a value. A secret withhoststakes its value atupthe 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.