132 lines
8.1 KiB
Markdown
132 lines
8.1 KiB
Markdown
---
|
|
topic: what runs on it
|
|
status: accepted
|
|
date: 2026-09-05
|
|
deciders: jochen
|
|
reconstructed: false
|
|
---
|
|
|
|
# 48. 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 0047: 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 and leaves — checked, not
|
|
assumed: nothing else in the sdk or the catalogue called it, so it is removed with the
|
|
provisioner it belonged to.
|
|
- **How this is verified:** redis is assigned as a provider in the lab, the contributions and
|
|
the unsealed password the mesh would deliver are put in its `receives` path, and a client
|
|
authenticates as that consumer with the mesh's password and gets PONG — where a provider that
|
|
invented its own password answers WRONGPASS — with `$MESH_SEAL_KEY` set nowhere and no
|
|
`.credential` file written. Proven: `provider-uses-mesh-credential` is green.
|
|
|
|
**What this does not cover — credential provisions, not data provisions.** This decision is about a
|
|
provision whose credential is a *secret the mesh mints* — a login and password (redis, postgres,
|
|
minio). A provider that instead *generates* the thing the consumer needs, and that thing is not a
|
|
secret — umami's `analytics`, where the consumer wants back a `siteId` umami assigned — does not fit,
|
|
because a contract that returns nothing has no way to hand that data back. The seal-key fault was
|
|
never umami's (it sealed no password; it returned a public id), so removing the seal does not break
|
|
it further, and it still reconciles its sites off the mesh's contributions. But delivering
|
|
provider-generated data back to a consumer is a *return path* the mesh does not have and this
|
|
decision does not build — a separate shape, left to a separate decision.
|
|
- 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/032](../04-ISSUES/032-provider-runtime-has-no-seal-key/00-report.md) — the
|
|
observation and the cross-repo trace this decision rests on.
|
|
- ADR 0047 (the module runtime) — what first ran a provider's provisioner as a delivered
|
|
process and exposed this; link to be filled when 0047 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()`.
|