Graduate issues 067 and 068 to decisions and to-be designs
ADR 0084 (extends 0027) — a provision is served by a node-scoped provider the consumer selects, defaulting to co-location; a module may instead carry a private embedded instance that is not a provision. Design: 01-to-be/23-choosing-a-provider. ADR 0085 (extends 0031) — a secret is a provision and the vault is the module that provides it; a module's own local secret becomes an ordinary pair credential that rotates through the existing machinery, while the controller's provisioning-credential mint (0048) is unchanged. Design: 01-to-be/24-the-secrets-vault; doc 13 amended to cross-link the non-pair secret. Issues 067/068 marked resolved with amended-design set. ADR index regenerated; records and index checks pass. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -5,10 +5,11 @@ code:
|
||||
- mesh-controller internal/inventory/secrets.go
|
||||
- mesh-controller cmd/mesh-controller/rotate.go
|
||||
- mesh-controller examples/postgres-provisioner
|
||||
updated: 2026-09-01
|
||||
updated: 2026-09-20
|
||||
decisions:
|
||||
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
|
||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
||||
---
|
||||
|
||||
# 13 — Credentials, and moving them
|
||||
@@ -105,3 +106,19 @@ a provider that added a password and removed nothing.
|
||||
|
||||
**Not over loopback.** `pg_hba` trusts anything there, so every password looks correct — a
|
||||
deliberately wrong one returned a row for an afternoon before that was noticed.
|
||||
|
||||
|
||||
## The secret that is not a pair
|
||||
|
||||
Everything on this page is about the credential *between a consumer and a provider* — the login
|
||||
one module uses against another. A mesh also holds secrets that are not that: a value a single
|
||||
module needs for **its own** use, and a value only an operator can supply. Those had no owner, and
|
||||
so no rotation — the gap this page's machinery could not reach because there was no pair to rotate.
|
||||
|
||||
[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) gives them one. Such a secret is
|
||||
provided by a **vault** module ([24](24-the-secrets-vault.md)): a module requires a `secret`
|
||||
provision, and the credential of that consumer↔vault pair is an ordinary pair credential — so it
|
||||
rotates, and is queried for who holds it, through exactly the machinery described above, unchanged.
|
||||
The rotation a module's own secret lacks is not a second mechanism; it is this one, pointed at a
|
||||
secret the vault provides. What this page proves for a database password holds, by construction,
|
||||
for a secret from the vault.
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-09-20
|
||||
decisions:
|
||||
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
||||
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||
---
|
||||
|
||||
# 23 — Choosing a provider
|
||||
|
||||
A provision is named for what the consumer's code is coupled to
|
||||
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
|
||||
`postgres-database`, not `database`. That decides *what kind* of provider satisfies a requirement.
|
||||
It does not decide *which* provider, and the mesh runs more than one of most kinds.
|
||||
|
||||
## Why there is a choice at all
|
||||
|
||||
Node-specific services delivered to the mesh is the design, not an exception. Every control-capable
|
||||
node runs its own relational store, its own cache, its own object store; a single node may run
|
||||
several relational stores, each raised by the module that needed a particular engine or version.
|
||||
The one provision that is single today — the identity provider — already serves applications whose
|
||||
home is another node. So for a given provision name there are usually several providers, one per
|
||||
node, and they are **not** interchangeable: each holds different data and lives in a different
|
||||
place. A consumer bound to the wrong one reads the wrong database or takes a network hop it did not
|
||||
need.
|
||||
|
||||
Naming the kind is therefore only half of "how a consumer gets what it needs". The other half is
|
||||
which provider, and it has two shapes: **consume a provider**, or **carry your own**.
|
||||
|
||||
## Consuming a provider
|
||||
|
||||
A provider is not a mesh-wide singleton. It is identified by the node it runs on together with the
|
||||
module that provides it — a (node, module) pair. A consumer's requirement resolves to one such
|
||||
provider, and which one is part of the **assignment**, not the manifest
|
||||
([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)):
|
||||
the same module, assigned twice, may be served by two different providers.
|
||||
|
||||
**The default is co-location.** A consumer that names no provider is served by the provider of that
|
||||
provision on its own node. This is the ordinary case and is meant to need nothing said — a module
|
||||
that wants a database wants, almost always, the database on the machine it runs on. A mesh that
|
||||
happens to run exactly one provider of a kind is simply the case where co-location and "the only
|
||||
one there is" name the same thing; that is *there happens to be one*, not a mesh-wide scope written
|
||||
into the provision.
|
||||
|
||||
**Coupling to data is named.** The exception to co-location is a consumer coupled to a *particular
|
||||
provider's contents*: two modules that must share one database, or a consumer that must reach a
|
||||
provider on a different node. That coupling is exactly what may not be guessed, so the assignment
|
||||
names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is
|
||||
to that provider and not to whichever one is nearest.
|
||||
|
||||
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
|
||||
named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates
|
||||
shown — the same stance
|
||||
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took
|
||||
against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
|
||||
delivered quietly costs more than a refusal.
|
||||
|
||||
## Carrying your own
|
||||
|
||||
A module need not consume a provider at all. It may carry its **own** instance of an engine inside
|
||||
its own composition — reachable only on the module's own network, publishing no host port, and
|
||||
**not** declared as a provision. Nothing else in the mesh can see it or bind to it, and it cannot
|
||||
collide with anything on a well-known port. To resolution it does not exist; it is an internal part
|
||||
of the module, like any other container the module runs.
|
||||
|
||||
This is legitimate but it is the exception, and the design says when: **only when a genuine engine
|
||||
fork or a pinned server version makes the shared provider unusable.** A module written against a
|
||||
customised engine, or one that needs an extension the node's provider does not carry, has no choice
|
||||
but to carry its own. A module that merely pins an old image of an ordinary engine does not — the
|
||||
version on a compose file is the *server's*, and the application talks to a newer shared server
|
||||
perfectly well once its data is migrated in. The rule is *share by default; embed only when a fork
|
||||
or a version forces it*. Most of the per-module stores that exist in the mesh being migrated onto
|
||||
are the first kind wearing the second's clothes, and consolidate onto the node's provider.
|
||||
|
||||
The distinction is worth stating because the two cases look identical from outside — a module with
|
||||
a database either way — and the mesh must be able to tell them apart to reason about either. A
|
||||
consumed provider is a binding the mesh records, rotates and can move. An embedded instance is a
|
||||
private detail the mesh does not manage and must not mistake for a provider.
|
||||
|
||||
## What is not settled here
|
||||
|
||||
A provider that moves between nodes must keep its identity, so that a consumer's recorded choice
|
||||
does not silently rebind to a different provider that inherited its place. That is a property the
|
||||
provider lifecycle must supply, and this document names it as a requirement rather than describing
|
||||
its mechanism.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-09-20
|
||||
decisions:
|
||||
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
||||
- 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
|
||||
- 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||
---
|
||||
|
||||
# 24 — The secrets vault
|
||||
|
||||
Identity runs on the mesh, not of it — a module other modules require, not a part of the
|
||||
controller ([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)). The
|
||||
store and the broker are ordinary modules too
|
||||
([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)). Secrets are the piece
|
||||
that never got the same treatment: nothing owns a secret. This document describes the module that
|
||||
does — a **vault** that provides a `secret` provision — and, as importantly, the boundary of what
|
||||
it owns and what it deliberately does not.
|
||||
|
||||
## Three kinds of secret, and which the vault owns
|
||||
|
||||
A mesh handles three species of secret, and confusing them is how the current arrangement went
|
||||
wrong.
|
||||
|
||||
- **A provisioned credential** is the login one module uses against another — a database password,
|
||||
a broker account. The mesh mints it per consumer↔provider pair and seals it to the machines that
|
||||
must hold it ([ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md)),
|
||||
and its provider makes it true. **This is not the vault's**, and the vault does not change it. It
|
||||
already has an owner and a rotation
|
||||
([13](13-credentials-and-their-rotation.md)); disturbing it would buy nothing.
|
||||
- **A module's own secret** is a value a single module needs for itself — the password of a store
|
||||
it runs privately, an internal signing token. Today the mesh generates this as a value nothing
|
||||
owns, and so it cannot be rotated. **This is the vault's.**
|
||||
- **An operator-delivered secret** is a value only a person can supply — a credential for something
|
||||
outside the mesh. Today it is sealed in and then held, un-audited and un-rotatable. **The vault
|
||||
holds this**, and can hand it out and audit it, though it cannot generate it.
|
||||
|
||||
The vault, then, is the provider a module turns to for a secret that is **not** the byproduct of
|
||||
some other provision. It is the answer to *"this module needs a password, and there is no provider
|
||||
whose job it is to give it one."*
|
||||
|
||||
## A secret as a provision
|
||||
|
||||
A module that needs a secret for its own use requires a `secret` provision, exactly as it requires
|
||||
a database from the store. The vault generates the value — or takes custody of one an operator
|
||||
delivered — and the credential belongs to the consumer↔vault pair. Because it is an ordinary pair
|
||||
credential, **everything already built for pair credentials applies to it unchanged**: it rotates
|
||||
with the one command that discards a credential and delivers both ends together, it is one secret
|
||||
per holder so rotating one touches nothing else, and *who holds this* is a query rather than an
|
||||
assumption ([13](13-credentials-and-their-rotation.md)). The rotation a module's own secret lacks
|
||||
today is not new machinery; it is the machinery that already moves a database password, pointed at
|
||||
a secret the vault provides.
|
||||
|
||||
This is what replaces the "generated value that nothing owns". A module's own password stops being
|
||||
a special kind of thing injected by the synchroniser and becomes a provision with a provider, a
|
||||
holder, and a lifecycle — the same shape as everything else the mesh grants.
|
||||
|
||||
## The vault is a module, and node-scoped
|
||||
|
||||
The vault is an ordinary module. A mesh that wants one runs it; a mesh whose modules require no
|
||||
secret of their own runs none — the same test identity meets
|
||||
([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)). It is provisioned
|
||||
the ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does
|
||||
that, because the controller must mint in order to deliver any provision, the vault's own included.
|
||||
The vault mints secrets *for modules*, downstream of its own existence, never the credential that
|
||||
delivers it.
|
||||
|
||||
Like every provider it is node-scoped
|
||||
([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [23](23-choosing-a-provider.md)):
|
||||
each node may run its own vault, and a module's own secret is held by the vault on the module's
|
||||
node, selected the same way any provider is. There is no single mesh vault holding everything, for
|
||||
the same reason there is no single mesh store.
|
||||
|
||||
## Beyond generate and hold
|
||||
|
||||
Owning a secret means owning more than its creation. The mesh being migrated onto has a working
|
||||
secrets subsystem whose surface names the operations a vault is responsible for — locating a secret,
|
||||
backing it up, verifying it is what it should be, and a break-glass recovery for when the normal
|
||||
path is unavailable. These become the vault module's, specified against it rather than scattered.
|
||||
|
||||
One of them is left open on purpose. **Break-glass must not reintroduce a key that one place
|
||||
holds.** The whole point of sealing a secret to the machine that needs it, asymmetrically, is that
|
||||
no single place can open everything
|
||||
([ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md)); a
|
||||
recovery path that keeps a master key would undo exactly that. How the vault lets an operator
|
||||
recover a secret without becoming the thing the sealing was designed to prevent is a question this
|
||||
design opens and does not yet answer.
|
||||
|
||||
## What this changes for a module
|
||||
|
||||
A module that today writes a password into its own composition — an embedded store's login, an
|
||||
internal token — instead requires it from the vault and reads it where the mesh puts it. The change
|
||||
is taken module by module, not as a flag day, and it is the same change in each: a value the module
|
||||
authored becomes a value the vault provides. When it is done, the guarantee the as-is design already
|
||||
makes for generated secrets — *nothing in the repository contains a credential* — holds for a
|
||||
module's own secrets not by convention but because there is a provider whose job it is to keep them.
|
||||
@@ -32,6 +32,8 @@ document is written and this one's status becomes `implemented`.
|
||||
| [`20-writing-a-module.md`](20-writing-a-module.md) | A worked guide: one module, four capabilities, four languages, and the packages it publishes | [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md), [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md), [ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) |
|
||||
| [`21-the-installation-in-full.md`](21-the-installation-in-full.md) | Every step from a bare machine to a mesh that maintains itself, and what is not yet true | [ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md), [ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md), [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md) |
|
||||
| [`22-the-work-ahead.md`](22-the-work-ahead.md) | Everything decided and not yet built, in dependency order, each phase ending at a run | [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md), [ADR 0075](../../02-DECISIONS/0075-two-stores-and-which-provides-what.md), [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md) |
|
||||
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
|
||||
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
|
||||
|
||||
## Not yet written
|
||||
|
||||
|
||||
Reference in New Issue
Block a user