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
95 lines
5.4 KiB
Markdown
95 lines
5.4 KiB
Markdown
---
|
|
status: open
|
|
opened: 2026-09-04
|
|
located-in: [mesh-sdk, mesh-catalog]
|
|
fixed-by:
|
|
amended-design:
|
|
---
|
|
|
|
# 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?
|