ADR 0053 (proposed) — a provider creates the credential the mesh minted, and seals nothing
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
This commit is contained in:
@@ -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()`.
|
||||
Reference in New Issue
Block a user