Skip to content

Per-client isolated vaults

Each onboarded client can be given its own Agent Vault identity — client-{slug} with agent {slug}-steelmoth-runtime and a durable token — instead of sharing a pre-provisioned client-pool-NN slot. A client gateway is pinned to its own vault (AGENT_VAULT_VAULT=client-{slug}) and reads its system secrets from that vault using that token. The isolation boundary is the prontera per-token ACL: the gateway’s token can read only its own vault, so a client can never reach another client’s — or the operator’s client-mark — secrets. This is the steelmoth side only; the broker edit and the prontera endpoint deploy are a separate, operator-gated prontera step.

Everything is default-OFF and inert until that gated step plus a flag flip.

On the broker GET path (credentials._brokered_secret), a SYSTEM-alias read (memlink-scope is the load-bearing one; also the shared OAuth-app id/secret and the model-provider keys) now carries the header X-Agent-Vault-Client-Vault: <the gateway's own vault>only when the gateway is vault-pinned to a client-{slug} / client-pool-NN vault. The operator (client-mark) and any unpinned process send no header, so their GET is byte-identical to today. Both bound and unbound reads prefer the auto-renewing 24h session (AGENT_VAULT_SESSION_TOKEN): the prontera broker validates that session against the declared X-Agent-Vault-Client-Vault (its non-reveal ACL check) and performs the reveal itself via its own writer grant, so a session bearer resolves per-client aliases (the broker patch that added this is a separate, operator-gated prontera deploy). Preferring the rotating session keeps a leaked bearer’s exposure ≤24h rather than the long-lived identity token’s until-re-mint life; the gateway keeps that session fresh in-process (AGENT_VAULT_INPROCESS_REFRESH=1 on the owlet template — the same VaultSessionRefresher daemon the operator API runs). The long-lived identity token (AGENT_VAULT_TOKEN_FILE) stays the one-shot fallback: any 401/404 on the env-session bearer gets exactly one identity-token retry (stale vault session detected warning), for bound and unbound reads alike (the earlier identity-token-first bound-read path — the 2026-07-02/03 garcia Telegram incident — is superseded). This mirrors the read-binding memlink_scope_sign already sends on the /sign path, so the broker applies the identical binding it enforces on signing. The in-process result cache is keyed by (alias, binding) so a client gateway’s binding can never serve the operator’s cached value. It is inert today: with the broker’s read allowlist empty (until the gated step) the broker returns the same result with or without the header.

Per-client end-user OAuth refresh handles (oauth-*, resolve_opaque_credential_ref) are deliberately not system aliases — they resolve to per-client secrets the broker already scopes by the handle’s own owner segment, never by vault.

The provisioner gains a default-OFF flag STEELMOTH_PROVISIONER_DEDICATED_CLIENT_VAULT_ENABLED. When set (and a client-vault-admin client is wired):

  • provisionprovision_once calls the prontera client-vault-admin provision-client endpoint to mint the client’s vault before spawning the gateway (the gateway binds the vault’s token), then records the host port via pool_repository.create_or_get_client_vault (reusing the pool’s free-port allocator). Any failure fails the job rather than spawning against a missing vault. When the flag is off, it claims a pre-provisioned pool slot exactly as today.
  • teardownteardown_once destroys the client’s vault (revoke token + vault delete + drop signing keys + remove host token) in place of the pool-slot release.

On teardown the vault is destroyed only after owner-data erasure (erase_owner_data) succeeds. If erasure fails, the job finishes FAILED and returns before any prontera delete — the vault identity is what authenticates the idempotent erase retry, so destroying it on a failed wipe would strand un-erased owner PII with no way back in. The teardown step order on the dedicated path is therefore: container → erase → destroy vault → drop port row → delete registry row.

A slug that resolves to the operator vault (markclient-mark) — or is not a vault/container-safe slug — is refused before any vault action, in three independent places: pool_repository.validate_client_slug, ClientVaultAdminClient.{provision,delete}_client, and scripts/provision-pool.sh::validate_client_slug. So a hostile slug can never point a client at the operator’s vault.

Authority: a dedicated bearer, not bare reachability

Section titled “Authority: a dedicated bearer, not bare reachability”

ClientVaultAdminClient authenticates its provision/delete calls with a DEDICATED admin bearer (broker alias client-vault-admin-token, held broker-only in memory per INV-024 / HARD RULE 10). Network reachability of the endpoint is never sufficient authority — an empty/missing bearer is refused on both ends. The endpoint host/scheme is allowlisted to prontera (HTTPS) or loopback.

scripts/provision-pool.sh exposes a reusable provision-client <slug> codepath (the SAME vault/agent/token/signing-key provisioning the pool loop uses, keyed by the client slug); the prontera endpoint wraps it. The prontera-assets/client-vault-admin/ artefacts are documentation-only (HR10) — they deploy to the vault host in the gated step and are never imported as steelmoth runtime code. New per-client vaults require that prontera deploy, not a compose change in this repo.

Claim Area Source
GET read-binding header + system-alias set + admin-token alias steelmoth-runtime/src/steelmoth_runtime/credentials.py
create_or_get_client_vault, identity, slug guard, admin client steelmoth-runtime/src/steelmoth_runtime/client_provisioning/pool_repository.py
Flag, provision-before-spawn, erase-before-destroy teardown steelmoth-runtime/src/steelmoth_runtime/client_provisioning/provisioner.py
Reusable provision-client codepath + slug refusal scripts/provision-pool.sh
Prontera endpoint contract (doc-only) prontera-assets/client-vault-admin/README.md
  • security-and-secrets.md — the broker-only secret model and the alias map.
  • access-request-flow.md — client onboarding that feeds the provisioner.
  • data-and-scope.md — per-tenant scope GUCs the erase runs under.
  • Update this page when the credentials read-binding, the provisioner flag, or the slug-guard logic changes.
  • The broker allowlist + the prontera client-vault-admin endpoint are external to this repo (gated prontera step); this page describes only the steelmoth side.
  • The exact prontera-side delete-client agent-vault verbs are wired in the gated deploy; the in-repo artefact specifies the contract, not the final implementation.