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

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 → "First Deploy".

Plain vars vs. secrets

tawa config set K=Vtawa config set --secret K=V
At rest in the builderplaintextAES-256-GCM under CONFIG_ENCRYPTION_KEY
Returned by tawa config listkey and valuekey only, K=********
In the podenv varenv var (via {service}-managed-secrets)
In the deploy snapshotvalue recordedname 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-locker — a secret from the Locker, never through a terminal

This is the way to give a service a secret. The 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; each item's Use this item panel in the portal has the exact command.

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.

ErrorMeansDo
LOCKER_HANDLE_NOT_FOUNDno such item in the owner org's lockercopy the handle from the item in the Locker
LOCKER_RESOLVE_FORBIDDENthe 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_UNREACHABLEthe Locker did not answerretry; nothing was set
LOCKER_NOT_CONFIGUREDthe builder has no LOCKER_URL, or Bio-ID would not issue it a locker-resolve tokena 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. --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 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:

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.

ErrorMeansDo
VAULT_ALIAS_NOT_FOUNDno such alias, or it was deleteddaryx ssh credentials list
VAULT_RESOLVE_FORBIDDENthe builder's token may not use that aliasask a platform operator to tag its token for the alias
VAULT_UNREACHABLEthe vault did not answerretry; nothing was set
VAULT_NOT_CONFIGUREDthis builder holds no vault credentiala 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 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+)

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.

Platform operators

VarPurpose
CONFIG_ENCRYPTION_KEYAES-256-GCM key for managed secrets. Without it, --secret fails.
DARYX_VAULT_URLdaryx vault base URL (default https://vault.daryx.com). Deprecated with --from-vault
DARYX_VAULT_TOKENthe 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_URLiec-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

# ❌ 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

Last updated: October 6, 2026