← Back to Skills

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.md
Download .md

Coming soon via CLI:

tawa chaac install tawa-config

Details

Format
Rule
Category
provision
Version
1.0.70159
Tokens
~3,425
Updated
2026-10-13
platformconfigsecretsvaultdaryxlockerenv-varstawa-cli