From e27f8b1b7d715724f7bfd4fc6a3178d5e9be93f7 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 5 Sep 2026 00:06:50 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200053=20(proposed)=20=E2=80=94=20a=20provi?= =?UTF-8?q?der=20creates=20the=20credential=20the=20mesh=20minted,=20and?= =?UTF-8?q?=20seals=20nothing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 008's trace confirmed the premise in control-plane code: the mesh already mints one password per consumer/provider pair and delivers the provider its copy (SecretFor/SecretsFrom/grantsFor -> Grant.Sealed; the receives contribution carries As + Secret). The provisioner's symmetric seal is an orphaned, contradictory second model. ADR 0053 corrects the provider contract in one place (the sdk harness): providers create the resource with the mesh-supplied login and password and drop seal/key/return entirely. Reframe 008 as contract-first (every provider, not four). Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- ...-creates-the-credential-the-mesh-minted.md | 120 ++++++++++++++++++ .../00-report.md | 68 ++++++---- 2 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 02-DECISIONS/0053-a-provider-creates-the-credential-the-mesh-minted.md diff --git a/02-DECISIONS/0053-a-provider-creates-the-credential-the-mesh-minted.md b/02-DECISIONS/0053-a-provider-creates-the-credential-the-mesh-minted.md new file mode 100644 index 0000000..9be49bd --- /dev/null +++ b/02-DECISIONS/0053-a-provider-creates-the-credential-the-mesh-minted.md @@ -0,0 +1,120 @@ +--- +status: proposed +date: 2026-09-05 +deciders: jochen +reconstructed: false +--- + +# 53. A provider creates the credential the mesh minted, and seals nothing + +## Context + +A provider module stands up a per-consumer resource — a database, a cache bucket, an object +store user — and the consumer must end up holding a credential that authenticates against it. +Building the module runtime (ADR 0052: a module runs its own code as its own process under its +own account), the provider's provisioner was run for the first time as a delivered thing, and +it did not work. It reads a seal key from the environment that nothing sets, and it seals every +credential it produces to that key with a symmetric passphrase. + +Tracing the credential's path turned up something larger than a missing key. **The provisioner +harness the whole catalogue is built on describes a credential flow the mesh does not have, and +duplicates — incorrectly — one it does.** + +What the sdk's `runProvisioner` does today: + +- reads request files named `*.grant.json` — which nothing in the mesh writes; +- calls an adapter whose `create` **generates its own password** and returns it; +- seals that password with a symmetric key (`$MESH_SEAL_KEY`) and writes a `*.credential` + file — which nothing in the mesh reads, and no consumer ever unseals. + +What the mesh already does, and has wired end to end: + +- The control plane mints one password per (consumer, provider) pair (`Inventory.SecretFor` → + `secrets.Make`) and seals it to **both** node keys asymmetrically — a copy the consumer's + host can open and a copy the provider's host can open. No shared symmetric key exists + anywhere, on purpose: a key both ends hold is a key the mesh would have to distribute, which + is the same problem one level down, and the control plane's own code refuses it. +- The provider is handed, at the path its `receives` names, one contribution per consumer: + the **login to create** (`As`, derived by the mesh so the two ends agree by construction), + the consumer's address and requested values, and a **`Secret` file** holding that consumer's + password sealed to the provider and unsealed onto the machine by its host. +- The consumer is handed the *same* password, as plaintext its own host wrote by unsealing its + copy and substituting it into a config file. The consumer never unseals anything itself and + holds no key. + +So the password a provider's provisioner invents is not even the password the consumer was +given: a consumer authenticating with the mesh's password against a resource the provisioner +created with its own would simply fail. The symmetric seal is not an incomplete feature to +finish delivering a key for. It is a second, contradictory credential model bolted beside the +real one, and it cannot be made to work without building the very thing the mesh was designed +not to have. + +This is a decision and not a patch because the harness is the **provider contract**. Every +provider — the four that exist and the many a real mesh grows — is built on +`runProvisioner(resource, adapter)`. Whatever it says a provider is, they all inherit; and +changing it later is one migration per provider. It is cheaper and more honest to settle what +a provider is now. + +## Decision + +**A provider is handed the credential; it does not make one, does not seal one, and does not +hand one back.** The provisioner's only job is to make the mesh's grants true in its own +software. + +Concretely, for the sdk harness and the adapter contract: + +- The harness reconciles the **contributions the mesh delivers** to the provider's `receives` + path — the list of consumers, each with its login name (`As`), address, requested values, + and the path to its unsealed password (`Secret`). It does not read `*.grant.json` and it + does not write `*.credential`. +- For each consumer present, the harness reads the password from that consumer's `Secret` file + and calls the adapter to bring the resource into being under the given login. For each + consumer no longer present — the mesh drops it from the contributions file when its consumer + goes away — the harness calls the adapter to withdraw it. +- The adapter shrinks to the per-software half and nothing else. It is given the login, the + password, and the values, and it makes the resource exist or removes it. It generates no + password, derives no name, seals nothing, and returns no credential: + roughly `create({ as, password, values })` and `remove({ as })`, both returning nothing. +- `$MESH_SEAL_KEY`, the symmetric `seal()`/`writeSealedCredential` path, and the `*.grant.json` + / `*.credential` files are removed from the provisioning path entirely. The credential + reaches the consumer through the mesh's own asymmetric channel, which already crosses node + boundaries and holds no shared secret. + +Identity stays the mesh's to say. The login the provider creates is the name the mesh derived +and gave the consumer to present; the provider never invents a name, because a name the +consumer cannot learn is a name it cannot authenticate with. + +## Consequences + +- A provider module becomes smaller and unable to be wrong in this way: with no password to + generate and no key to seal to, the class of bug where the two ends hold different secrets + cannot be written. A provider added after this inherits the corrected contract and has no + seal to reintroduce. +- The four current providers (redis, postgres, minio, umami) each lose their `generatePassword` + + seal code and gain a `create` that takes the password it is given. Their teardown becomes + "withdraw the login named `As`". +- The symmetric `seal()`/`unseal()` primitive loses its only caller. If nothing else uses it, + it leaves with the provisioner; that is checked, not assumed. +- **How this is verified:** a provider is assigned in the lab, a consumer is granted its + resource, and the consumer authenticates against the provider using only what the mesh + delivered — no `$MESH_SEAL_KEY` set anywhere, no `.credential` file written. The trail is the + consumer connecting; a provider that invented its own password fails it. This replaces the + audit-logger/redis lab beds' current use of a hand-set lab seal key, which existed only to + get the orphaned harness to start. +- Teardown beyond "remove the login" — data an object store leaves behind when a consumer + leaves — is named by each provider's adapter, not by the harness, and is out of scope here + except to say the contract must leave room for it. + +## References + +- [04-ISSUES/008](../04-ISSUES/008-provider-runtime-has-no-seal-key/00-report.md) — the + observation and the cross-repo trace this decision rests on. +- ADR 0052 (the module runtime) — what first ran a provider's provisioner as a delivered + process and exposed this; link to be filled when 0052 lands on the trunk. +- Control-plane mechanisms this relies on already existing: `mesh-control` — + `internal/inventory/secrets.go` (`SecretFor`, `SecretsFrom`), `internal/secrets/seal.go` + (`Make`, the two-blob asymmetric sealing), `cmd/mesh-control/plan.go` (`grantsFor`, the + `Grant.Sealed = ForProvider` delivery), `internal/catalogue/declaration.go` (the `receives` + contribution: `As`, `At`, `Values`, `Secret`). +- The path being removed: `mesh-sdk` — `src/provisioner/index.ts` (`runProvisioner`, `sealKey`, + `writeSealedCredential`) and the symmetric `src/primitives/index.ts` `seal()`/`unseal()`. 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 7f5d232..3583eb6 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 @@ -62,33 +62,55 @@ would authenticate with the mesh's password against a resource created with the ## 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. +This is not a four-module problem. The provider contract lives in **one place** — the sdk's +`runProvisioner(resource, adapter)` harness — and every provider is built on it. Four exist +today (redis, postgres, minio, umami); a mesh of any size ends up with many. Whatever the +provisioner harness does, every present and future provider inherits, so the orphaned +symmetric seal is a fault stamped into the interface, not into four adapters. That also sets +the cost of getting it wrong: a contract N providers depend on is N migrations to change +later, which is the argument for settling it deliberately now rather than patching around it. + +As written, each provider carries a provisioner that cannot start (no key), and that, if it +did, would create resources with a password it invented — a *different* password from the one +the mesh minted and handed the consumer — 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: no consumer unseals, and no shared key exists to unseal with. + +## The mesh already does this — confirmed + +The premise the fix rests on is not a hope; it is in the control plane today. For a served +interface, `Inventory.SecretFor` mints one password per (consumer, provider) pair via +`secrets.Make`, sealing it to **both** node keys — `ForConsumer` and `ForProvider`. +`SecretsFrom(provider)` is documented as "every credential a provider node was issued, so it +can be told what to create," and `grantsFor` (plan.go) hands the provider node one `Grant` per +consumer carrying `Sealed: ForProvider`. The provider receives, at the path its `receives` +names, one `Contribution` per consumer: the login to create (`As`, derived by the mesh so both +ends agree — 04-ISSUES/023), the consumer's address (`At`) and requested `Values`, and +`Secret`, the file holding that consumer's password sealed to this provider and unsealed by +its host. Everything the provisioner needs is delivered. It reads the wrong files +(`*.grant.json`, which nothing writes) and invents a password instead of reading the one in +`Secret`. ## 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:** +A **one-place contract change in the sdk harness**, plus re-pointing today's adapters at it — +not per-provider surgery, and inherited correctly by every provider after them: -- 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. +- `runProvisioner` reconciles the mesh-delivered `receives` contributions (not `*.grant.json`): + for each consumer, create the resource under the login `As` with the password read from the + delivered `Secret` file, for its `Values`; withdraw the login when a consumer leaves the file. +- The adapter stops generating a password and stops returning a credential — it is handed the + name and the password and only makes the resource exist. Roughly `create({as, password, + values})` / `remove({as})`, no return. +- `sealKey`, `seal()`, `writeSealedCredential`, `MESH_SEAL_KEY`, and the `.credential` file + leave entirely; the consumer already receives its copy through the mesh's own channel. + +This is proposed as ADR 0053, which defines the corrected provider contract, for ratification. ## 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? +- Does anything legitimate still use the symmetric `seal()`/`unseal()` primitive, or does it + leave with the provisioner? +- On withdrawal the mesh drops the consumer from the contributions file; is "remove the login" + the whole of teardown for every provider, or does an object store (data left behind) need a + policy the contract should name?