diff --git a/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md b/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md new file mode 100644 index 0000000..13b515e --- /dev/null +++ b/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md @@ -0,0 +1,63 @@ +--- +topic: the tiers +status: accepted +date: 2026-09-21 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0085-a-secret-is-a-provision.md +--- + +# 94. A module may hold several secrets from one provider, each a pair of its own + +## Context + +[ADR 0085](0085-a-secret-is-a-provision.md) makes a module's own secret a provision: the module +requires `secret` from the vault and reads the pair credential minted for that consumer↔vault +pair. A pair has one credential, a module requires a provision once, and so a module received +one value. Read against the catalogue, nine modules hold two or more secrets besides their +broker account; seven of them hold genuinely independent values with independent lifetimes — a +root certificate, its key and that key's password; an admin password beside an API token. None +derives from another, so "one value, derivation the module's business" answers nothing +([issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md)). + +## Considered Options + +1. **One value per module; the module derives the rest.** Rejected: the values are independent. +2. **Require the provision several times.** Rejected: `requires` is a list of names, and a + requirement is matched by name everywhere. +3. **The `secrets` map names several files under local names, and each local name is a pair + credential of its own.** Adopted. + +## Decision + +A module's `secrets` entry for a requirement may be a path, as before, or an object of local +names to paths. Each local name is its own pair credential, keyed on it beside the provision, +the consumer node, the consumer module and the provider; its own file on the consumer, referred +to as `${secret:}`; its own holder at the provider, named the consumer's identity +with the local name after it; and rotated apart from the others. A local name may not be one of +the module's own secrets or something it requires, so what a placeholder means is never +ambiguous. The plain shape is unchanged, and every credential that exists is the one it was. + +The holder's suffix is not a login any backend checks — a secret is not a login — so the +identity limit that binds a database role or an access key does not apply to it. + +## Consequences + +The ten modules that could not move onto the vault can. What got harder: `rotate secret` for a +consumer rotates every local name it holds from that provider; rotating one of several is a +finer command than the mesh has, and waits for a case that needs it. + +## How it is checked + +Manifest tests read both shapes, write them back, and refuse a colliding or unusable local +name. A resolver test asserts two local names are two needs, two files with two credentials, +and two holders at the provider. An inventory test asserts two local names are two rows, that +rotating one leaves the other, and that the provider is told both. The vault bed installs a +consumer that keeps two secrets and asserts two values delivered, two holders in the vault's +ledger, and both rotated by one command. + +## References + +- [issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md) +- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) +- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 89583b4..0cbf5d5 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -113,6 +113,7 @@ python3 00-META/checks/index.py fail if stale - **0078** — [The store and the broker are ordinary modules](0078-the-store-and-broker-are-modules.md) - **0079** — [The foundation seats are named after their servers](0079-the-foundation-seats-are-named-after-their-servers.md) - **0092** — [An operator delivers a pair credential, and the mesh never replaces it](0092-an-operator-delivers-a-pair-credential.md) +- **0094** — [A module may hold several secrets from one provider, each a pair of its own](0094-a-module-may-hold-several-secrets-from-one-provider.md) ### What runs on them, and how it gets there diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index 98ec487..898abe5 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -4,6 +4,7 @@ status: implemented code: [mesh-catalog, mesh-controller, mesh-host] updated: 2026-09-21 decisions: + - 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md - 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md - 02-DECISIONS/0085-a-secret-is-a-provision.md - 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md @@ -48,8 +49,12 @@ A module that needs a secret for its own use requires a `secret` provision, exac a database from the store. The vault generates the value — or takes custody of one an operator delivered, through `secret accept … --provider`, which seals it to both ends and records the pair as accepted ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)) — and -the credential belongs to the consumer↔vault pair. An accepted pair is the one exception to what -follows: the mesh cannot make its replacement, so it is neither remade when a key changes nor +the credential belongs to the consumer↔vault pair. A module that needs several values names them +as local names under its `secrets` entry, and each is a pair of its own — keyed on the local +name, delivered as its own file, held at the vault as its own holder +([ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md)). +*How it is checked:* manifest, resolver and inventory tests on two local names; the vault bed's +two-secret consumer. An accepted pair is the one exception to what follows: the mesh cannot make its replacement, so it is neither remade when a key changes nor rotated; both are refused aloud, and accepting a new value is the rotation. *How it is checked:* an inventory test accepts, reads back unchanged, and asserts the two refusals name the remedy. Because it is an ordinary pair credential, **everything already built for pair credentials applies to it unchanged**: it rotates diff --git a/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md b/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md index 4ab4d54..18a06d6 100644 --- a/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md +++ b/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md @@ -1,9 +1,9 @@ --- -status: located +status: resolved opened: 2026-09-20 located-in: [mesh-controller internal/catalogue (requires/secrets), mesh-controller internal/inventory (secret key)] -fixed-by: -amended-design: +fixed-by: ADR 0094; mesh-controller feat/several-secrets (secrets: under local names, the pair keyed on the local name, migration 0027); proven by the vault bed +amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md --- # One `secret` provision yields one value, and a module may need several diff --git a/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md b/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md index b82ce93..9688ff0 100644 --- a/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md +++ b/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md @@ -18,6 +18,7 @@ is a manifest-vocabulary decision, and it moves the pair key: the credential must be keyed on the local name, not the provision name, or the second pair overwrites the first. -**Located in:** the manifest's `requires`/`secrets` vocabulary (the catalogue parser) and the pair -credential's key (the controller's secret store). Not fixed here: the key change touches every -existing pair and belongs in a feature of its own, with a lab run against the vault bed. +**Located in:** the manifest's `secrets` vocabulary (the catalogue parser) and the pair +credential's key (the controller's secret store). Fixed as +[ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md): the +key gains the local name, empty for every existing pair, so nothing that exists changed.