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:
@@ -0,0 +1,108 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-20
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||
---
|
||||
|
||||
# 84. Which provider serves a consumer, when the mesh runs more than one
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) settled what a provision
|
||||
is *named* for — the thing the consumer's code is coupled to, so `postgres-database` and
|
||||
`mssql-database` are different provisions and a wrong match is refused at resolution. It said one
|
||||
thing more, in passing, and left it: *"Two providers of `postgres-database` — a container on this
|
||||
node and a managed instance elsewhere — are interchangeable and should both match."* Naming was
|
||||
decided; **which of several providers serves a given consumer was not.**
|
||||
|
||||
The mesh assumes there is only one to choose. Provisions are mesh-scoped: the adopted store is a
|
||||
single `mesh-store` a consumer on any node reaches, and `scope: "mesh"` is written into the
|
||||
provision definitions. **That assumption is false on day one, and was always meant to be.**
|
||||
Node-specific services delivered to the mesh is the plan, not an edge case:
|
||||
|
||||
- Both control-capable nodes already run their **own** general-purpose relational store (the same
|
||||
engine, one instance each), serving that node's own applications.
|
||||
- Each runs its **own** SQL server, its **own** cache, its **own** object store. One node alone
|
||||
runs six separate relational-store instances, each raised by the module that needed it.
|
||||
- The one provision that is currently single — the identity provider, one instance on one node —
|
||||
already authenticates applications whose home is a **different** node.
|
||||
|
||||
So several providers of one provision name genuinely coexist, and they are **not** interchangeable
|
||||
the way 0027's aside supposed. They differ by node, by the data they hold, and by locality. A
|
||||
consumer bound to the wrong one reads the wrong database, or takes a cross-node hop it did not
|
||||
need, or cannot be moved without silently rebinding. The model has no field in which to say which
|
||||
one. This is 0027's own fault — *a match that resolves and is wrong* — one level up: 0027 refused
|
||||
the wrong **dialect**; nothing refuses, or even asks about, the wrong **instance**.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep `scope: "mesh"` — one provider per provision, mesh-wide.** Rejected: it is false on day
|
||||
one, and making it true would force every node's applications onto one node's server — the
|
||||
exact opposite of node-specific services delivered to the mesh, and a single point of failure
|
||||
the topology was built to avoid.
|
||||
2. **Resolve to any provider of the name (0027's "both match").** Rejected: when providers hold
|
||||
different data and live on different nodes they are not interchangeable, and picking one
|
||||
arbitrarily is a wrong-instance match — the confidently-wrong answer 0027 exists to prevent,
|
||||
restated at the level of the instance rather than the dialect.
|
||||
3. **Always require the consumer to name the provider explicitly.** Rejected: needless ceremony in
|
||||
the common case, where the consumer wants the provider on its own node; and a field every
|
||||
manifest must carry is a field an author forgets, which then matches everything again — the
|
||||
failure 0027 warned about for qualifiers.
|
||||
4. **A provision is node-scoped; the consumer selects the provider, defaulting to co-location.**
|
||||
Adopted.
|
||||
|
||||
## Decision
|
||||
|
||||
**A provision is served by a provider identified by its node, and the consumer selects which one.**
|
||||
A provider is a (node, module) pair, not a mesh-wide singleton. A consumer's binding resolves to a
|
||||
specific provider, and the selection is part of the assignment
|
||||
([ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)), not the
|
||||
manifest.
|
||||
|
||||
**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 common case and needs nothing said. A mesh with
|
||||
one provider of a kind is just the case where co-location and "the only one" coincide — expressed
|
||||
as *there happens to be one*, not as a scope.
|
||||
|
||||
**A consumer coupled to a provider's data names it.** Where two consumers must share one database,
|
||||
or a consumer must reach a provider on another node, the assignment names that provider — because
|
||||
that coupling is exactly what may not be guessed, and naming it is what makes a later move safe.
|
||||
|
||||
**A module need not consume a provider at all.** It may carry its **own** instance inside its own
|
||||
composition — on its own module network, publishing no host port, **not** declared as a provision —
|
||||
when a genuine engine fork or a pinned server version makes the shared provider unusable. Such an
|
||||
instance is invisible to resolution and can be bound by nothing else. The rule is *share by
|
||||
default; embed only when a fork or a version forces it* — most of the per-module stores that exist
|
||||
today are vanilla engines on stale pins that a consolidation onto the node's provider would absorb.
|
||||
|
||||
## Consequences
|
||||
|
||||
The mesh can carry its real topology **deliberately** rather than by the accident of which
|
||||
provider happened to be the single one. A consumer's data-coupling becomes a stated fact, which is
|
||||
what lets a provider be moved without a consumer silently following the wrong one — provided a
|
||||
provider keeps its identity across a relocation, which is a follow-up this record opens rather than
|
||||
closes. Rotation ([13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)) addresses a
|
||||
specific provider's holders, not "the provision's".
|
||||
|
||||
What got harder: an assignment now may carry a provider selection, and a wrong one is a new way to
|
||||
misconfigure. It is mitigated the way 0027 mitigated its own: the co-location default removes the
|
||||
choice in the common case, and genuine ambiguity — several providers, none named, none co-located —
|
||||
is refused with the candidates named, never resolved by picking.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — the naming this extends;
|
||||
its "both match" aside is the gap closed here.
|
||||
- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is exactly such a
|
||||
provider, and already serves consumers on another node.
|
||||
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) — where the
|
||||
selection lives.
|
||||
- [ADR 0078](0078-the-store-and-broker-are-modules.md) — the adopted store, whose mesh-wide
|
||||
binding is the single-provider assumption this record replaces.
|
||||
- [issue 067](../04-ISSUES/067-a-provision-cannot-name-which-provider-serves-it/00-report.md) — the
|
||||
gap, and the day-one evidence.
|
||||
- [`03-DESIGN/01-to-be/23-choosing-a-provider.md`](../03-DESIGN/01-to-be/23-choosing-a-provider.md)
|
||||
— the design.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-20
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
|
||||
---
|
||||
|
||||
# 85. A secret is a provision, and the vault is the module that provides it
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0031](0031-the-control-plane-authenticates-nobody.md) decided that identity *runs on the
|
||||
mesh, not of it* — a module other modules require, rather than a privileged part of the
|
||||
controller. [ADR 0078](0078-the-store-and-broker-are-modules.md) did the same for the store and
|
||||
the broker: the twelve-module floor has no specialty left in it. **Secrets are the exception that
|
||||
survived.** No module owns a secret.
|
||||
|
||||
Secret handling is smeared across three built-in parts of the runtime:
|
||||
|
||||
- the **controller mints** one credential per consumer↔provider pair and seals it to both node
|
||||
keys ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md));
|
||||
- the **mesh generates local secrets** — *"generated secrets are the mesh's, never authored"*
|
||||
(as-is, `06-configuration-and-secrets.md`) — for a value a single module needs for its own use;
|
||||
- the **synchroniser injects** both into a node's generated files.
|
||||
|
||||
Nothing is the owner of "a secret" the way the store module is the owner of "a database", and the
|
||||
cost is recorded rather than hypothetical. The as-is design names the weakness in its own words:
|
||||
*"Rotation is not a mesh operation… there is no mechanism that rotates one and informs everything
|
||||
holding it."* A `rotate` command has since been built and proven, but it reaches **only** the
|
||||
provisioned pairs; a secret a module generates for its own fully-local use — the password of a
|
||||
version-pinned embedded store ([ADR 0084](0084-which-provider-serves-a-consumer.md)), an internal
|
||||
token — is minted by the mesh and then has no operation that can remake it. Three species of
|
||||
secret, and only the first has an owner:
|
||||
|
||||
| species | minted by | rotates? |
|
||||
|---|---|---|
|
||||
| a provisioned credential (a database login) | controller, sealed to nodes (0048) | yes — `rotate`, per pair |
|
||||
| a module's own local secret | the mesh, as a generated value | **no owner, no rotation** |
|
||||
| an operator-delivered secret (an external key) | a person, sealed in (`secret accept`) | no rotation, no audit |
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Leave it a property of the controller.** Rejected: it is the smear above — no owner, local
|
||||
secrets that cannot be rotated, operator secrets that cannot be audited — and it is exactly the
|
||||
specialty 0078 removed for the store and broker, kept here for no reason anyone recorded.
|
||||
2. **A dedicated vault built into the foundation, not a module.** Rejected: it reintroduces a
|
||||
privileged built-in, the thing 0031 and 0078 went out of their way to remove, and a mesh that
|
||||
wants none would still carry it.
|
||||
3. **Fold all minting, the controller's provisioning credentials included, into the vault.**
|
||||
Rejected: the controller must mint in order to **deliver** any provision — the vault's own
|
||||
credential among them — so making the vault mint the credential of its own delivery is the
|
||||
store/broker chicken-and-egg for no gain. The provisioned-pair credential already has an owner
|
||||
and a rotation ([13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)); this record
|
||||
does not disturb it.
|
||||
4. **A secret is a provision; the vault is an ordinary module that provides it.** Adopted.
|
||||
|
||||
## Decision
|
||||
|
||||
**Secret-holding is a module, parallel to identity.** A module that needs a secret **for its own
|
||||
use** — a local service's password, an internal token, an external key it was handed — requires a
|
||||
`secret` provision from a vault provider, exactly as it requires a database from the store. The
|
||||
vault generates the value (or holds one it was given), and because the credential belongs to the
|
||||
consumer↔vault pair it **rotates, backs up and is audited through the same per-pair machinery**
|
||||
[13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) already defines. That is what
|
||||
gives the second and third species the rotation and audit they lack. A module's own secret stops
|
||||
being a generated value that nothing owns and becomes an ordinary provision with a provider.
|
||||
|
||||
**The controller's minting of provisioning-pair credentials is unchanged** ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)):
|
||||
it is how every provision, the vault's own included, is delivered. The vault does not mint the
|
||||
mesh's delivery credentials; it provides secrets to modules, and is itself provisioned the ordinary
|
||||
way.
|
||||
|
||||
**The vault is node-scoped like every provider** ([ADR 0084](0084-which-provider-serves-a-consumer.md)):
|
||||
each node its own, selected the same way, so a module's own secret is held by the vault on the
|
||||
module's node. **A mesh that wants no vault runs none** — a module requiring no secret needs
|
||||
nothing, which is the same test 0031 applied to identity.
|
||||
|
||||
## Consequences
|
||||
|
||||
The gap that opened this — a generated local secret with no rotation — **closes without new
|
||||
machinery**: rotating such a secret is the vault's provisioner remaking a pair credential, the
|
||||
operation 13 already specifies. Backup, audit and break-glass gain an owner — the vault module —
|
||||
and become things a design specifies rather than absences. The as-is sentence *"rotation is not a
|
||||
mesh operation"* is already false for provisioned pairs and, once the vault ships, for local
|
||||
secrets too; the as-is document is updated when it does, not before.
|
||||
|
||||
What got harder: a break-glass path — recovering a secret when the sealed delivery path is
|
||||
unavailable — must not reintroduce a key that one place holds, which is the property
|
||||
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) was built to preserve; how
|
||||
the vault offers recovery without it is left to the design as an open question. And a module that
|
||||
today bakes a password into its own composition must instead require it from the vault — a
|
||||
migration taken module by module, not a flag day.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is a module; this is the
|
||||
same move for secrets.
|
||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — the provisioning-credential
|
||||
path, left unchanged.
|
||||
- [ADR 0078](0078-the-store-and-broker-are-modules.md) — the de-specialisation this completes.
|
||||
- [ADR 0084](0084-which-provider-serves-a-consumer.md) — the node-scoping the vault obeys.
|
||||
- [issue 068](../04-ISSUES/068-secrets-have-no-owning-module/00-report.md) — the gap.
|
||||
- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md) —
|
||||
the design. The source mesh's `secret_locate` / `secret_backup` / `secret_verify` /
|
||||
`secret_breakglass` subsystem is the prior art it draws on.
|
||||
@@ -136,6 +136,8 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0053** — [A scheduled step is a container run on a recurring schedule](0053-a-step-that-runs-on-a-schedule.md)
|
||||
- **0054** — [Model usage is a vendor-neutral record, produced by the adapter, at two grains](0054-model-usage-is-recorded-at-two-grains.md)
|
||||
- **0055** — [Model access is answered by a licence, or by a node that hosts the model](0055-model-access-is-answered-by-a-licence-or-a-node.md)
|
||||
- **0084** — [Which provider serves a consumer, when the mesh runs more than one](0084-which-provider-serves-a-consumer.md)
|
||||
- **0085** — [A secret is a provision, and the vault is the module that provides it](0085-a-secret-is-a-provision.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
Reference in New Issue
Block a user