From 332c334767fd0475372d2c56724ad6475c444e56 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 4 Sep 2026 23:40:20 +0200 Subject: [PATCH] =?UTF-8?q?Issue=20008=20=E2=80=94=20sharpen:=20the=20prov?= =?UTF-8?q?isioner's=20seal=20model=20is=20orphaned,=20not=20just=20undeli?= =?UTF-8?q?vered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../00-report.md | 115 +++++++++++------- 1 file changed, 71 insertions(+), 44 deletions(-) diff --git a/04-ISSUES/008-provider-runtime-has-no-seal-key/00-report.md b/04-ISSUES/008-provider-runtime-has-no-seal-key/00-report.md index addfe98..7f5d232 100644 --- a/04-ISSUES/008-provider-runtime-has-no-seal-key/00-report.md +++ b/04-ISSUES/008-provider-runtime-has-no-seal-key/00-report.md @@ -1,67 +1,94 @@ --- status: open opened: 2026-09-04 -located-in: [] +located-in: [mesh-sdk, mesh-catalog] fixed-by: amended-design: --- -# A provider module's runtime needs a seal key the mesh cannot deliver +# 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 +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, and the provisioner's reconcile harness -opens with: +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. -- it requires a **seal key**, read from the environment, and fails immediately without one; -- every credential it produces for a consumer is **sealed to that key** before it is 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. -Nothing in the control plane delivers such a key to a module's runtime. The mesh delivers a -module its **broker account** (a sealed, machine-bound credential) and its **own-secrets**, -and it seals those deliveries with a key of its own — but there is no provision for handing a -provider the key it must seal *its own* outputs with, nor for a consumer to receive the -matching key to unseal them. +## What a trace of the credential path turned up -The consequence has two faces, both bad: +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.** -- **A provider runtime cannot start.** The harness treats the missing key as a hard, - up-front failure — correctly, because a provider that silently sealed to nothing would be - worse. So a provider module, assigned and pushed, comes up dead until a key is supplied - out of band. -- **Supplying one out of band is not a fix.** A key set by hand on the provider is a key the - consumer has no principled way to obtain. The seal is symmetric; the two ends must share - it, and there is nothing that makes them share it. +- `runProvisioner` reads request files named `*.grant.json`. **Nothing writes those.** +- It writes sealed credential files named `..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**. -The slice proved the mechanism only by setting a lab-local key in the manifest by hand — an -admission, not a solution. The test that does so says as much in its header. +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 design (the ADR that says a module runs its own code under its own -account) is the model every provider now follows — a cache, an object store, a database, -each stands up per-consumer resources and returns sealed credentials. The delivery the mesh -already does for a broker account is exactly the shape a seal key needs, so the gap is not -that the idea is hard; it is that one required secret in the provider/consumer handshake was -never given an owner. Left open, every provider converted to the runtime model inherits a -module that cannot come up as delivered, and the failure lands at assignment time on whoever -is standing up the node — far from the design decision that caused it. +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. -There is also a **rule-with-no-check** here: the design states that a consumer receives a -sealed credential and unseals it. Nothing today establishes the key that makes "unseal" -possible, so the rule is, at present, enforced by nothing. +## 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 -- Whose secret is the seal key — the mesh's, the node's, or the specific provider/consumer - grant's? The answer decides who generates it and who it is delivered to. -- Is it one key the mesh holds and delivers to both ends of every grant, or a per-grant key - minted when a grant is made? A per-grant key confines a leak to one consumer; a mesh-wide - key is one thing to deliver and rotate. -- Should it be delivered the way the broker account already is — a sealed, machine-bound - own-secret file the runtime reads — so a provider needs no new delivery channel, only a new - named secret? -- Does rotation of this key have to re-seal every outstanding credential, and if so, is that - the provisioner's reconcile loop's job or the control plane's? +- 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?