Config Vars & Managed Secrets
Anything a service needs at runtime that the platform does not provision for it — an API key, a third-party account id — is a **config var**. Tunables (feature…
Ruleprovision
About
Anything a service needs at runtime that the platform does not provision for it — an API key, a third-party account id — is a **config var**. Tunables (feature flags, rates, limits, URLs shown to users) are not config vars; they are runtime settings edited in the Console (see `runtime-settings`). Set a config var with `tawa config`, and the builder injects it into the pod on the next deploy.
Skill Content
This is the raw markdown that gets installed as a Claude Code rule.
# Config Vars & Managed Secrets
Anything a service needs at runtime that the platform does not provision for it — an API key,
a third-party account id — is a **config var**. Tunables (feature flags, rates, limits, URLs
shown to users) are not config vars; they are runtime settings edited in the Console (see
`runtime-settings`). Set a config var with `tawa config`, and
the builder injects it into the pod on the next deploy.
## The Short Version
```bash
tawa config set LOG_LEVEL=debug API_TIMEOUT=30000 # plain, readable
tawa config set --secret STRIPE_API_KEY=sk_live_... # encrypted at rest
tawa config set --secret BING_WEBMASTER_KEY --from-locker lk://insureco/daryx/bing-search-key
# ↑ the default for any secret: value never touches this terminal
tawa config list # names only for secrets
tawa config unset OLD_KEY
tawa config pull # → .env.local, for local dev
tawa deploy --prod # REQUIRED to apply any of the above
```
**`tawa config set` does not change the running pod.** Values are materialised into Kubernetes
at deploy time, so a value set after a deploy reaches the pod on the **next** deploy.
On a brand-new service, the service record is created by its *first deploy* (or by `tawa link`),
so `tawa config set` before then 404s. Full ordering:
[build-pipeline](build-pipeline.md) → "First Deploy".
## Plain vars vs. secrets
| | `tawa config set K=V` | `tawa config set --secret K=V` |
|---|---|---|
| At rest in the builder | plaintext | AES-256-GCM under `CONFIG_ENCRYPTION_KEY` |
| Returned by `tawa config list` | key **and** value | key only, `K=********` |
| In the pod | env var | env var (via `{service}-managed-secrets`) |
| In the deploy snapshot | value recorded | name only, value omitted |
Both end up as ordinary env vars in the pod. `--secret` is about who can read it *before* then.
**A secret that already lives in the Locker should never be pasted.** Use `--from-locker`, below.
`--from-vault` (the daryx vault) is deprecated — see [the migration](#--from-vault--deprecated).
## `--from-locker` — a secret from the Locker, never through a terminal
**This is the way to give a service a secret.** The [Locker](https://portal.insureco.io/locker)
(iec-locker) is where org credentials live. Hand the builder the item's **handle** and it fetches
the value itself (tawa ≥ 0.29.0). Bindings, the runtime service door and the agent tools are in
[locker](locker.md); each item's **Use this item** panel in the portal has the exact command.
```bash
tawa config set --secret BING_WEBMASTER_KEY --from-locker lk://insureco/daryx/bing-search-key
tawa config set --secret DB_PASSWORD --from-locker lk://insureco/carriers/paragon/password # name a field
```
```
tawa CLI ──{ "BING_WEBMASTER_KEY": { "lockerHandle": "lk://insureco/daryx/bing-search-key" } }──▶ builder
│
iec-locker ◀── POST /api/resolve (builder's Bio-ID token, scope locker-resolve) ─────────────────┘
──▶ value ──▶ encrypted into the service's managed secrets
```
The plaintext travels **Locker → builder** only — not the CLI, a log line, the config API
response, the deploy snapshot or a Septor payload. The Locker audits every attempt as a `config`
use with the handle and the destination (`service/env:KEY`), never the value.
`tawa config list` shows `BING_WEBMASTER_KEY=******** (locker: lk://insureco/daryx/bing-search-key)`.
### The item must be bound to the service
The Locker only hands a value to a service the item names. In the Locker, open the item and add
the service name (e.g. `iec-webmaster`) to its **service bindings** (`bindings.services`). An item
with no service bindings resolves for nobody. Also:
- the handle must be in the **service owner's org** locker — `lk://<spec.owner>/…`;
- personal handles (`lk://me/…`) are refused — move the item to an org vault;
- the item must be **Agent-usable** — a **Sealed** item is humans only and is refused (`ITEM_SEALED`);
- a handle without a field uses the item's only secret field; with several, name one
(`lk://org/vault/item/field`), or the Locker answers `FIELD_AMBIGUOUS`.
Same rules as `--from-vault`: `--secret` is required, one key per handle, a value together with
the handle is refused without echoing it, every handle resolves before anything is written, and
it is a **one-time copy** — rotating the item does not propagate; re-run, then deploy.
| Error | Means | Do |
|---|---|---|
| `LOCKER_HANDLE_NOT_FOUND` | no such item in the owner org's locker | copy the handle from the item in the Locker |
| `LOCKER_RESOLVE_FORBIDDEN` | the Locker refused; its code is in the message (`NOT_BOUND`, `ORG_MISMATCH`, `PERSONAL_NOT_RESOLVABLE`, `ITEM_SEALED`, `FIELD_AMBIGUOUS`, …) | for `NOT_BOUND`, bind the item to the service |
| `LOCKER_UNREACHABLE` | the Locker did not answer | retry; nothing was set |
| `LOCKER_NOT_CONFIGURED` | the builder has no `LOCKER_URL`, or Bio-ID would not issue it a `locker-resolve` token | a platform operator must fix the builder |
## `--from-vault` — deprecated
> **Deprecated (2026-10-04).** The daryx vault is being retired in favour of the Locker. Use
> `--from-locker` for every new secret, and move existing `--from-vault` keys over as part of
> [iec-builder#258](https://git.insureco.io/insureco/iec-builder/issues/258). `--from-vault`
> still works, so existing config keeps deploying. The rest of this section is kept for those
> keys.
### Resolution order
`--from-vault <alias>` no longer goes straight to the daryx vault. The builder:
1. **Asks the Locker first**, by `legacyAlias` (the daryx import kept every alias on its Locker
item), in the service owner's org locker. The item must be bound to the destination service.
2. **Falls back to the daryx vault only when the Locker has no item with that alias** (a 404).
The response and the build/API log carry a `[warn]` naming the alias and saying it should move.
3. **Surfaces any other Locker answer as the error**, with no fallback: `NOT_BOUND`,
`ITEM_SEALED`, `ORG_MISMATCH`, an unreachable Locker. The item exists, so a stale daryx copy
would only hide the real problem. Fix the binding, then re-run.
A successful Locker answer is recorded exactly as `--from-locker` records it, so `tawa config list`
shows which store answered: `(locker: lk://…)` or `(vault: <alias>)`. The Locker path also warns
with the handle to switch to — run `--from-locker <that handle>` next time.
Pasting a secret is the problem this solves. `tawa config set --secret KEY=sk-...` puts the
plaintext in the terminal, in shell history, in whatever scrolled past, and — when an agent ran
the command — in that agent's context. The [daryx vault](https://vault.daryx.com) already holds
these values and is built so they are *never* returned to a caller.
So hand over the **alias** and let the builder fetch the value:
```bash
tawa config set --secret ANTHROPIC_API_KEY --from-vault ledger-anthropic-lily
```
```
tawa CLI ──{ "ANTHROPIC_API_KEY": { "vaultAlias": "ledger-anthropic-lily" } }──▶ builder
│
daryx vault ◀── POST /credentials/<alias>/resolve (builder's own token)┘
──▶ value ──▶ encrypted into the service's managed secrets
```
The plaintext travels **vault → builder** and nowhere else. It is not in the CLI's process, not
in a log line, not in the config API response, not in the deploy snapshot, and not in a Septor
payload — only the alias name and the env-var key are.
`tawa config list` shows where it came from:
```
ANTHROPIC_API_KEY=******** (vault: ledger-anthropic-lily)
```
### It is a one-time copy
Rotating the value in the vault does **not** propagate. Re-run the command after a rotation,
then deploy. (A live reference would put a vault call on the deploy path, so a vault outage
would start failing deploys — a bigger change, deliberately not taken here.)
### Rules
- **`--from-vault` requires `--secret`.** A vault value in a plain config var would be readable
by anyone who can read the service, which defeats the point.
- **One key per alias.** Run it once per variable.
- **A value and `--from-vault` together is refused** — and the refusal does not echo what you
typed, because what you typed may be the secret.
- The alias must exist and the builder's vault token must be allowed to use it.
### When it fails, nothing changes
Every alias is resolved *before* anything is written, so a failure leaves the service's config
exactly as it was — never half-applied.
| Error | Means | Do |
|---|---|---|
| `VAULT_ALIAS_NOT_FOUND` | no such alias, or it was deleted | `daryx ssh credentials list` |
| `VAULT_RESOLVE_FORBIDDEN` | the builder's token may not use that alias | ask a platform operator to tag its token for the alias |
| `VAULT_UNREACHABLE` | the vault did not answer | retry; nothing was set |
| `VAULT_NOT_CONFIGURED` | this builder holds no vault credential | a platform operator must set `DARYX_VAULT_TOKEN` |
### The other direction
`daryx ssh credentials export <alias> --to tawa --service <svc> --key KEY` does the same trip
with the vault pushing instead of the builder pulling. It predates `--from-vault` and still
works. Both are deprecated with the daryx vault; the Locker equivalent is `--from-locker`.
## Platform-provisioned vars — do not set these
`BIO_CLIENT_ID`, `BIO_CLIENT_SECRET`, `BIO_ID_URL`, `MONGODB_URI`, `REDIS_URL`, `NEO4J_*`,
`KOKO_URL`, `JANUS_URL`, `IEC_WALLET_URL`, `SERVICE_NAMESPACE`, `HTTP(S)_PROXY` and friends are
injected by the builder on every deploy. So are three [Claude access](claude-access.md) ids —
`ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID` — for a
service that has it switched on. (`ANTHROPIC_WORKSPACE_ID` is injected with them but stays
settable, because a service on a plain API key sets it itself.) `tawa config set` refuses them
(`RESERVED_ENV_VAR`) — a manual value would shadow the provisioned one and break auth, DB
connections, or gas attribution. Declare the *feature* instead (`spec.auth`, `spec.databases`,
`spec.egress`) and let the builder provision it.
## Declaring what a service needs (catalog 0.5.0+)
```yaml
spec:
config: # ← spec.config, NOT spec.configDeclarations
- key: ANTHROPIC_API_KEY
secret: true
required: true
- key: LOG_LEVEL
default: info
```
The block is `spec.config`. The builder reads `key`, `secret`, `required` and `default` and
ignores anything else — a `description:` is dropped silently, and a whole block written as
`spec.configDeclarations` parses, deploys, and does nothing.
A `required` declaration with no value **fails the deploy** in the config preflight, naming what
is missing. A `default` is injected when nothing is set. `tawa preflight` checks the same thing
without spending a build.
## Build-time vars (frontends)
`NEXT_PUBLIC_*` / `REACT_APP_*` values are baked into the client bundle at `docker build` time,
before the pod exists. Declare them in `insureco.io/env-vars` and the builder writes the
declared keys into a `.env.production` in the build context. Only declared keys, which are
public by design — the rest stay runtime-only. See [nextjs](nextjs.md).
## Platform operators
| Var | Purpose |
|---|---|
| `CONFIG_ENCRYPTION_KEY` | AES-256-GCM key for managed secrets. Without it, `--secret` fails. |
| `DARYX_VAULT_URL` | daryx vault base URL (default `https://vault.daryx.com`). Deprecated with `--from-vault` |
| `DARYX_VAULT_TOKEN` | the builder's vault token. **Mint it with the vault's `config-resolve` scope and nothing else**, pinned to the builder host IP, tagged to the aliases it should reach. Empty disables `--from-vault`. |
| `LOCKER_URL` | iec-locker base URL (default `https://iec-locker.tawa.pro`). Empty disables `--from-locker`. No secret: the builder uses its own Bio-ID client (`builder-prod`), whose allowed scopes must include `locker-resolve`. |
On the Locker side, `POST /api/resolve` accepts only a `client_credentials` token from a client
in iec-locker's `LOCKER_RESOLVE_CLIENTS` (today `builder-prod`) carrying the exact
`locker-resolve` scope — `*` does not satisfy it.
`POST /credentials/:alias/resolve` is the only vault route that returns a plaintext value, and
it is reachable only with that scope — a `*` scope does not satisfy it, so existing broad tokens
gain nothing. Every call is audited with the alias and its destination, never the value.
## What NOT to do
```bash
# ❌ paste a secret you already keep in the Locker
tawa config set --secret ANTHROPIC_API_KEY=sk-ant-...
# ✅
tawa config set --secret BING_WEBMASTER_KEY --from-locker lk://insureco/daryx/bing-search-key
# ❌ --from-vault for a new key — deprecated (iec-builder#258)
tawa config set --secret ANTHROPIC_API_KEY --from-vault ledger-anthropic-lily
# ✅ move the item into the Locker, bind it to the service, use --from-locker
# ❌ expect a config change to reach a running pod
tawa config set --secret K --from-locker lk://org/vault/item # then wonder why the pod has the old value
# ✅ deploy — that is when config is materialised
tawa config set --secret K --from-locker lk://org/vault/item && tawa deploy --prod
# ❌ commit .env.local (tawa config pull writes decrypted secrets to it, 0600)
# ✅ .gitignore it
# ❌ set MONGODB_URI / BIO_CLIENT_SECRET by hand — refused, and would break the service
```
Install
Copy the skill content and save it to:
~/.claude/rules/tawa-config.mdComing soon via CLI:
tawa chaac install tawa-configDetails
- Format
- Rule
- Category
- provision
- Version
- 1.0.70159
- Tokens
- ~3,425
- Updated
- 2026-10-13
platformconfigsecretsvaultdaryxlockerenv-varstawa-cli