Locker — Using Stored Secrets from a Service
The [Locker](https://portal.insureco.io/locker) (service `iec-locker`) is where org credentials live. A Tawa service never needs someone to copy a value out of…
Ruleconfigure
About
The [Locker](https://portal.insureco.io/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.
Skill Content
This is the raw markdown that gets installed as a Claude Code rule.
# Locker — Using Stored Secrets from a Service
The [Locker](https://portal.insureco.io/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
```bash
# 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:
| | Rule | Refusal |
|---|---|---|
| 1 | The 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` |
| 2 | The item is **Agent-usable**, not **Sealed** (Sealed means humans only) | `ITEM_SEALED` |
| 3 | The 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](config.md).
```bash
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 must be on the Locker's **service door**. That list is a runtime setting,
`serviceDoor.clients`, edited by a Locker settings owner in **Tawa Console → Operations →
Locker Switches** (or `PUT /api/settings/serviceDoor.clients`). It is live within ~15 seconds:
no `tawa config set`, no iec-locker redeploy. 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 owner maps it in the `serviceDoor.aliases`
setting (`{ "clientId": "service[/env]" }`). Coach, for example, resolves as `cloys-app`.
Bind items to **that** name.
- **Platform services need no list entry** when the owner switches on
`serviceDoor.platformAutoAdmit`: a `client_credentials` token whose verified `orgSlug` is
`insureco`, whose client id is the builder's `{service}-{env}`, and whose service Koko has
registered with `owner: insureco` is admitted. Koko failing refuses. Admission is identity
only: the item's bindings still decide what the service reads.
- 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.
### Service door settings (iec-locker#140)
| Setting | What | Default when nothing is stored |
|---|---|---|
| `serviceDoor.clients` | Bio-ID client ids allowed through the door | `LOCKER_RESOLVE_SERVICE_CLIENTS` |
| `serviceDoor.aliases` | client id → `service` or `service/env` | `LOCKER_RESOLVE_SERVICE_ALIASES` |
| `serviceDoor.platformAutoAdmit` | admit insureco `{service}-{env}` clients Koko knows | `false` |
- **Who writes:** a Locker settings owner (`LOCKER_SETTINGS_OWNERS`, default the platform
owner), with their own Bio-ID token. Service tokens and personas are refused. Bio-ID
`super_admin`s can read.
- **API:** `GET /api/settings`, `PUT /api/settings/:key` `{ value, expectedVersion }`,
`GET /api/settings/:key/history`. Send the `version` you read; a stale one is `409`.
- **Every write is audited** (before, after, who, when).
- **A stored value replaces the env var.** Start an edit from what `GET` returns. The env vars
are only a seed now; changing them does nothing once a value is stored.
- Binding an item to a service checks the same list (or, with auto-admit on, Koko), so
"add the client, then bind the item" is two calls and no deploy:
```
PUT /api/settings/serviceDoor.clients { "value": [ …, "chaac-prod" ], "expectedVersion": 3 }
locker_item_bindings { handle: "lk://insureco/chaacbot/…/key", add: ["chaac"] }
```
## Path 5 — agents through Jefferson
Jefferson (jci-mcp) has two Locker tools, and neither returns a value:
| Tool | Does |
|---|---|
| `locker_item_get` | An 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)
| Path | How | Reference |
|---|---|---|
| JCI plane apps | `secrets.locker: { orgSlug, data: [ { env: KEY, handle } ], sets: [...] }` in the app's plane values; ExternalSecrets reads each handle with the app's own token | jci-plane#300 |
| Server `.env` files | `daryx ssh env pull <service> --server <server> --to /path/.env --from-locker <handle>` — the item must be bound to service `daryx` | iec-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
```bash
# ❌ 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 `tawa config set LOCKER_RESOLVE_SERVICE_CLIENTS` on iec-locker and redeploy to add a
service. Edit the `serviceDoor.clients` setting; once it is stored, the env var is ignored.
- 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](config.md) · [Locker guide](https://portal.insureco.io/spaces/password-locker/pages/password-locker-guide#use) ·
[Locker encryption model](https://git.insureco.io/insureco/iec-locker/src/branch/main/docs/encryption-model.md)
Install
Copy the skill content and save it to:
~/.claude/rules/tawa-locker.mdComing soon via CLI:
tawa chaac install tawa-lockerDetails
- Format
- Rule
- Category
- configure
- Version
- 1.0.36610
- Tokens
- ~2,252
- Updated
- 2026-10-13
platformlockersecretsconfigbindingsservice-doorruntime-settingstawa-clijeffersonsecurity