Issue 009 (fixed) + Issue 008 (resolved via ADR 0053) — module-runtime config & provider contract #21
@@ -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()`.
|
||||
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user