Files
hq/04-ISSUES/008-provider-runtime-has-no-seal-key/00-report.md
T
jschoubben 332c334767 Issue 008 — sharpen: the provisioner's seal model is orphaned, not just undelivered
A cross-repo trace showed nothing writes the provisioner's grant-request files,
nothing reads its sealed credentials, and no consumer unseals — while the mesh
already mints and delivers provider/consumer credentials asymmetrically with no
shared key. The fix is to drop the symmetric seal and have providers consume the
mesh-minted password, a breaking provider-contract change that wants an ADR.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 23:40:20 +02:00

5.4 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
open 2026-09-04
mesh-sdk
mesh-catalog

A provider's provisioner seals with a key the mesh has no way to deliver — and does not need to

What was observed

Building the vertical slice for the module runtime (the module runs its own code as its own process under its own account), a provider module — one that stands up a per-consumer resource and hands back a credential — was assigned to a node and run as a broker-bound runtime. The runtime hosts the module's provisioner (the sdk's runProvisioner), and the harness opens by reading a seal key from $MESH_SEAL_KEY, failing immediately without one. Every credential it produces for a consumer is sealed to that key with the sdk's symmetric seal() (AES-256-GCM, mesh-sdk/src/primitives/index.ts) before being written.

Nothing in the mesh sets $MESH_SEAL_KEY. It is read in exactly two places in the sdk and set nowhere — no manifest, no control-plane code, no host code. So a provider runtime, as delivered, aborts at start-up. The slice proved the mechanism only by setting a lab-local key in the manifest by hand.

What a trace of the credential path turned up

The seal key is not a missing delivery. The whole symmetric-seal provisioner is orphaned, and it duplicates — badly — a job the mesh already does.

  • runProvisioner reads request files named *.grant.json. Nothing writes those.
  • It writes sealed credential files named <consumer>.<resource>.credential. Nothing reads those — not the host, not the control plane. The host reports applied-resource digests upward and never ships credentials; the control plane has no reference to that filename.
  • No consumer ever calls the symmetric unseal(). Consumers receive plaintext.

Meanwhile the mesh already carries a provider→consumer credential across nodes, with no shared key anywhere:

  • The control plane mints the password once (secrets.Make) and seals it twice, asymmetrically — ForConsumer to the consumer node's X25519 public key, ForProvider to the provider node's (mesh-control/internal/secrets/seal.go, mesh-host/internal/identity/ sealing.go, NaCl box).
  • Each host opens its own copy with its own private key on the machine; the plaintext exists only for the length of one function call (mesh-host/internal/apply/apply.go, the ${secret:name} substitution — ADR 0024's "the host is the only thing that ever holds both").
  • serves carries no credential and says so; receives/bound tell each side where its sealed secret is, never the value.

The two models also contradict each other. The sdk's seal() comment says the key is "a per-node passphrase the host holds"; the host holds no such passphrase — it holds an X25519 private key, and the control plane's own code refuses a shared symmetric key on principle: "a key both ends hold is a key the mesh would have to distribute, which is this problem again one level down" (secrets/seal.go). A symmetric MESH_SEAL_KEY shared between a provider node and a consumer node is exactly the thing the mesh was built not to have.

And the provisioner's model is wrong in a second way: its adapter generates its own password (generatePassword()) and creates the resource with it — a different password from the one the mesh mints and hands the consumer. Even with a seal key delivered, a consumer would authenticate with the mesh's password against a resource created with the provisioner's.

Why it matters beyond this instance

The module-runtime model is what every provider now follows — a cache, an object store, a database. As written, each carries a provisioner that cannot start (no key), and that, if it did, would create resources with the wrong password and seal them for a reader that does not exist. The rule the design states — "a consumer receives a sealed credential and unseals it" — is enforced by nothing, because no consumer unseals and no shared key exists to unseal with.

The fix this points to

Not "deliver the seal key." Align the provider with the mesh's existing credential path and delete the symmetric seal:

  • The provisioner should stop generating a password and stop sealing. It should read the per-consumer password the mesh already mints and delivers to the provider (ForProvider, unsealed onto the machine by the host), and create the resource with that password.
  • The consumer already receives the matching password as plaintext its own host wrote — no change needed there.
  • runProvisioner's sealKey, seal(), writeSealedCredential, and the *.grant.json / *.credential file dance come out; what replaces them is a reconcile driven by the contributions file the mesh already writes to the provider's receives path.

Open questions

  • Does the mesh mint and deliver ForProvider for a served interface today (redis-cache, postgres-database), or only for the own-secret case the trace followed? Confirm the provider actually receives each consumer's password before reworking the adapter around it.
  • What is the adapter contract after the change — create(consumer, password) rather than create(grant) -> Credential? That is a breaking change to the four providers (redis, postgres, minio, umami) and wants an ADR, since it changes what a provider module is.
  • Is any of the symmetric seal()/unseal() primitive still used for anything legitimate, or does it leave with the provisioner?