Files
hq/02-DECISIONS/0053-a-provider-creates-the-credential-the-mesh-minted.md
T
jschoubben e5f4af8cf2 Accept ADR 0053 and resolve issue 008 — provider contract implemented and proven
ADR 0053 accepted; adds the scope boundary the umami rework surfaced (credential
provisions vs data provisions — analytics' generated siteId return is left to a
separate decision) and records the lab proof. Issue 008 marked resolved: the sdk
harness and the four adapters are reworked, the symmetric seal removed, and
provider-uses-mesh-credential is green.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 00:27:56 +02:00

8.1 KiB

status, date, deciders, reconstructed
status date deciders reconstructed
accepted 2026-09-05 jochen 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 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/008 — 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().