Locker — Using Stored Secrets from a Service

The Locker (service iec-locker) is where org credentials live. A Tawa service never needs someone to copy a value out of it: you hand over a handle, and the Locker delivers the value to the service itself.

The Short Version

# 1. In the portal: put the item in an ORG vault, leave it Agent-usable, bind it to your service
# 2. Copy it into your service's config, then deploy
tawa config set --secret STRIPE_API_KEY --from-locker lk://insureco/payments/stripe/api-key
tawa deploy --prod

Each item page in the portal has a Use this item panel: its handles (with copy), the services it is bound to (editors can add and remove them), copy-ready recipes for every path below, and its recent use.

Handles

lk://<org|me>/<vault>/<item>[/<field>]
  • Leave off /<field> when the item has one secret field. With several, name one, or the Locker answers FIELD_AMBIGUOUS.
  • A handle carries no secret. It is safe in a catalog, a ticket, a commit or a chat.
  • lk://me/… is a personal item. No service can ever use one.

The rule: org locker, Agent-usable, bound

A service can use an item only when all three hold:

RuleRefusal
1The item is in an org locker, the org the call names (for config, the service owner's org, spec.owner)PERSONAL_NOT_RESOLVABLE, ORG_MISMATCH
2The item is Agent-usable, not Sealed (Sealed means humans only)ITEM_SEALED
3The item is bound to that service by name (bindings.services)NOT_BOUND

A binding is the grant. Binding an item to my-api means my-api may read that item's value at runtime. An item with no bindings resolves for nobody. The calling service's name comes from its verified Bio-ID token, never from the request, so a service can only reach items bound to itself. The Locker refuses a binding to a name no allowlisted client resolves as, since it could never be used.

Path 1 — copy into a service's config (--from-locker)

The default for a Tawa service. Full reference: config.

tawa config set --secret DB_PASSWORD --from-locker lk://insureco/carriers/paragon/password
tawa deploy --prod
  • The builder resolves the handle itself (iec-locker POST /api/resolve, its own Bio-ID client with the locker-resolve scope, on LOCKER_RESOLVE_CLIENTS). The plaintext goes Locker → builder → your managed secrets. Never the CLI, a log, the deploy snapshot or Septor.
  • Bind the item to the destination service, the one whose config you are setting.
  • --secret is required. One key per handle.
  • It is a one-time copy. Rotating the item does not reach the pod: re-run the command, then deploy.
  • tawa config list shows DB_PASSWORD=******** (locker: lk://…).

Path 2 — resolve at runtime (the service door)

For a value a service fetches when it needs it, instead of holding it in config.

POST {iec-locker}/api/resolve
Authorization: Bearer <your service's client_credentials token>

{ "handle": "lk://insureco/carriers/paragon/password", "orgSlug": "insureco" }
   // or { "legacyAlias": "…", "orgSlug": "…", "key": "…" }
→ { "value": "…", "handle": "lk://…", "field": "password" }   (Cache-Control: no-store)
  • Your service's Bio-ID client id must be on iec-locker's LOCKER_RESOLVE_SERVICE_CLIENTS. A platform operator adds it; the list fails closed. No scope is involved — locker-resolve is the builder's.
  • The service name is your client id minus -prod / -sandbox / -uat (iec-marketing-prod → iec-marketing). When the id does not name the service — an opaque client_<hex> id, or a renamed service — an operator maps it in LOCKER_RESOLVE_SERVICE_ALIASES as clientId=service[/env]. Coach, for example, resolves as cloys-app. Bind items to that name.
  • Resolves are rate limited per client. Cache in memory for the life of the process; do not resolve per request.
  • Never log the response body, never put the value in an error, never write it to disk.

Path 5 — agents through Jefferson

Jefferson (jci-mcp) has two Locker tools, and neither returns a value:

ToolDoes
locker_item_getAn item's metadata: name, handle, fields, bindings. Never values
locker_item_bindings{ handle | itemId, add: [...], remove: [...] } — change which services an item is bound to

They act as the signed-in person: jci-mcp sends its own service token (audience the Locker) and the person's Bio-ID token in X-Forward-User. The Locker applies that person's vault role and the agency's policy and records both the person and jci-mcp. Owner-tier toolset for now. Status: rolling out — waiting on a Bio-ID configuration deploy. There is no tool that returns a value, by design: an agent wires a binding, and the service receives the value.

Other paths (not Tawa-specific)

PathHowReference
JCI plane appssecrets.locker: { orgSlug, data: [ { env: KEY, handle } ], sets: [...] } in the app's plane values; ExternalSecrets reads each handle with the app's own tokenjci-plane#300
Server .env filesdaryx ssh env pull <service> --server <server> --to /path/.env --from-locker <handle> — the item must be bound to service daryxiec-locker#110

Audit

Every resolve, env render and binding change writes a UseEvent in the Locker — caller, client, destination (service/env:KEY), outcome, refusals included — and a Septor event (locker.item.resolved for the service door, locker.item.used for builder config). Never a value.

What NOT to do

# ❌ copy a value out of the Locker and paste it
tawa config set --secret STRIPE_API_KEY=sk_live_...
# ✅ hand over the handle
tawa config set --secret STRIPE_API_KEY --from-locker lk://insureco/payments/stripe/api-key

# ❌ a personal handle for a service — always refused
tawa config set --secret KEY --from-locker lk://me/dev/stripe

# ❌ --from-vault for a new secret — deprecated (iec-builder#258)
tawa config set --secret KEY --from-vault some-alias

# ❌ expect a rotated item to reach a running pod
# ✅ re-run --from-locker, then tawa deploy --prod
  • Don't bind an item to a service "to be safe". A binding is read access to the value.
  • Don't bind to a service name with a -prod suffix. Bind the service name.
  • Don't mark an item Sealed if a service needs it — Sealed is humans only.
  • Don't ask an agent, or a person, for a value that is already in the Locker. Bind it instead.

See also

config · Locker guide · Locker encryption model

Threads — persistent encrypted conversations (InsureBio v1, iec-locker #130/#131)

Two-way threads between an insured and their agents, on the Secure Send/Request crypto and audit. Shipped dark: every /api/threads/* route is 404 until INSUREBIO_THREADS_ENABLED=true on iec-locker.

  • Who: the agent (an active member of the agency org) opens the thread, gated by iec-bio-api may_message (no cold outreach); the insured replies freely; the agent side adds carrier/billing participants. Anyone else gets 404.
  • Storage: each thread has its own DEK wrapped by the platform KEK (locker-threads, bound to the thread id). Subject, bodies, request titles and answers are enc:v2: envelopes with AAD (thread, record, field).
  • Requests in a thread: document (loss run, dec page, COI, audit, W-9) is answered by the insured sharing the copy already in their Bio — the Locker asks iec-bio-api for a document grant to the agency and stores only grantId/documentId, never a copy. fields requests use #57 field definitions (no file fields yet) and are answered encrypted.
  • Notifications/audit: relay email says only "You have a secure message". Septor thread.* events carry ids, counts and enums, never content. An agent's every send/request re-checks may_message; a refusal is audited as thread.refused.
  • Fails closed: IEC_BIO_API_URL unset or iec-bio-api down → no thread, message or grant.
  • Full route contract: docs/threads-api.md in iec-locker. Not yet: attachments on messages, insured-initiated threads, upload-then-share.

Last updated: October 8, 2026