From fa2c09a2c5043ad790afb5a404b76a94fa7bcddf Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 23:10:52 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200113=20and=20to-be=2027:=20the=20vault=20?= =?UTF-8?q?makes=20every=20secret=20=E2=80=94=20provisioning=20all=20the?= =?UTF-8?q?=20way=20down?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A secret comes into being seven ways today: provider credentials, own secrets (54 modules), broker accounts through a command that is easy to forget, a vault that only records what the controller mints (6 modules), operator values, licences, and root secrets. The vault was built to end own secrets and did not; the old path was never retired. 0113 is rewritten as a waterfall. The vault makes every secret and nothing else does. A provider that needs a secret for a consumer requires it from the vault, declared once in its provision's contract and expanded per consumer by resolution; the vault delivers it to both holders, each sealed to its own node, so a provider's code is unchanged. Own secrets, broker passwords, operator values and licence credentials take the same path. Genesis is not an exception: it raises the vault first and asks it, so there is one way a secret is made from the first one on. The vault can sit at the bottom because it requires nothing but a broker account. One shared mint function in the SDK was considered and rejected: generation becomes uniform but custody stays spread over every provider's machine, and each SDK language needs its own implementation. Rotation is asked of the vault and is provider-first: the value goes to the holder that accepts it, which confirms, before the holder that presents it gets it, so the lockout window shrinks to the consumer's own restart, and an unconfirmed provider holds the rotation rather than half-doing it. The host derives which processes to restart or recreate from the requirement a definition reads, so no definition declares restart-on for a secret. A rotation shows unconfirmed until each consumer restarted and passed its health check. Issue 103 becomes a prerequisite. The file is renamed to match what it now decides. 0112 follows. --- ...e-definition-names-no-node-mesh-or-path.md | 12 +- ...hat-it-provides-and-the-mesh-carries-it.md | 143 -------------- .../0113-the-vault-makes-every-secret.md | 182 ++++++++++++++++++ 02-DECISIONS/README.md | 2 +- .../27-a-module-requires-the-mesh-resolves.md | 97 +++++++--- 03-DESIGN/01-to-be/README.md | 2 +- 6 files changed, 265 insertions(+), 173 deletions(-) delete mode 100644 02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md create mode 100644 02-DECISIONS/0113-the-vault-makes-every-secret.md diff --git a/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md b/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md index f2743c3..fa771d0 100644 --- a/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md +++ b/02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md @@ -82,8 +82,9 @@ like an external API key, is still the operator's: the vault is where it is *kep operator-delivered value ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)), not who provides it. -**What a provider answers with is its contract's fields**, made by the provider and carried back to -the consumer by the mesh ([ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)). +**Every secret is made by the vault, and a provider answers with resources and data** +([ADR 0113](0113-the-vault-makes-every-secret.md)). A provider that needs a secret for a consumer +requires it from the vault, like any consumer. The mesh carries every answer back. **A directory is a host provision.** Its contract is the owner and mode the module needs, including the owner its image expects ([ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)). @@ -111,8 +112,9 @@ carries, and a provider's identity. So **one module may be assigned to one node must stay singular stays so by a claim, or by an operator value colliding: a public name already taken is refused like any other singular thing. -**Genesis is the one exception**, as [ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md) -states: the foundation's requirements are answered by genesis itself, before any provider exists. +**Genesis is not an exception.** It raises the vault first and asks it for the foundation's secrets, +so the foundation's requirements are answered the same way as everything else +([ADR 0113](0113-the-vault-makes-every-secret.md)). ## What this changes in earlier records @@ -177,7 +179,7 @@ On acceptance, each of these is amended by a record of its own, not edited: - [ADR 0038](0038-the-mesh-assigns-the-port.md), [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md), [ADR 0084](0084-which-provider-serves-a-consumer.md): the parts already unified - [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): which module provider answers -- [ADR 0113](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md): who makes an answer, and how it travels +- [ADR 0113](0113-the-vault-makes-every-secret.md): the vault makes every secret, and how answers travel - [ADR 0092](0092-an-operator-delivers-a-pair-credential.md): the operator as a provider - [ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0107](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md), [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): what a directory's contract carries, and what it must not diff --git a/02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md b/02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md deleted file mode 100644 index fe81eb3..0000000 --- a/02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -topic: what runs on it -status: proposed -date: 2026-09-25 -deciders: jochen -reconstructed: false ---- - -# 113. A provider makes what it provides, and the mesh carries it back to the consumer - -## Context - -[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) decided that a provider is -handed the credential and makes none: the controller mints one per consumer and provider pair, seals -it to both nodes, and the provider creates the login under it. It fixed a real fault. The provider -harness of the time generated its own password and sealed it with a symmetric key nobody held, so a -consumer could never receive what the provider made. Controller-minting worked because it needed no -way back from provider to consumer. - -**It left that way back undecided, deliberately.** 0048 says so: *"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."* Since then, the missing return path has come up -repeatedly: - -- **Data provisions have nothing to answer with.** The analytics provider assigns a site id the - consumer needs. The DNS provider registers a name the consumer should be told. Both have no path - back, and say so in their code. -- **Some contracts need a value the controller cannot make.** The broker needs its admin password in - a hashed form the mesh's plain secret delivery cannot produce, so a module-specific bootstrap step - was written to derive it. That admin is a foundation credential, which genesis makes, so this - record does not remove that step. It is the clearest instance of the general problem, though: a - contract can require a form only the party that understands the software can produce. -- **The vault is a ledger.** A module's own secret is "a `secret` provision the controller mints and - the vault records" ([ADR 0085](0085-a-secret-is-a-provision.md), as amended). The one module whose - job is secrets generates none, and cannot apply a policy (length, form, lifetime) because it never - makes one. Every other provider creates what it provides. The vault is the exception. - -[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) proposes that everything a module -needs is a requirement answered by a provider against a contract. Under that, "the controller mints -this one kind of answer on the provider's behalf" is a special case the model would carry forever. - -## Considered Options - -**1. Keep 0048: the controller mints credentials, and data provisions stay without a way back.** -Rejected. The vault stays a ledger, contracts needing a derived form keep needing bespoke steps, and -a data provision stays unable to answer at all. - -**2. The provider makes the value and hands it to the consumer itself.** Rejected. The two may be on -different machines with no path between them the mesh has agreed to, and a provider reaching -consumers directly is a second delivery system beside the mesh's. 0048's objection, a symmetric key -both ends hold, is not what rules this out: node keys are asymmetric, and a provider can seal to a -node's public key without sharing anything. - -**3. The provider makes the value and gives it to the controller in plaintext, which seals and -delivers it.** Rejected. It works, and it puts every secret in the controller's memory and on the -broker in the clear, a surface today's design does not have for provider-side values. - -**4. The provider makes the value, seals each secret field to the consumer's node itself, and the -mesh carries the sealed answer.** Chosen. The mesh already tells a provider who each consumer is and -where; it also hands it the consumer node's public key. The provider seals to it, and answers over -its own scoped account. The controller delivers the sealed fields as it delivers everything else, -and can open none of them. - -## Decision - -**A provider makes what it provides.** Given a consumer, it creates the resource and answers with -the fields its contract names: a password, an access key, a site id, a registered name, a hashed -admin secret. The vault generates the secrets it provides, to their contract, and rotates them. - -**The provider seals, and the mesh carries.** With each consumer, the mesh hands the provider that -consumer node's public key. The provider seals every field its contract marks secret to it, and -hands the answer to the controller over its own scoped broker account. The controller delivers the -answer as the consumer's resolved values, and can open none of the sealed fields. A provider never -reaches a consumer directly. A secret's plaintext exists where it is made, on the provider's machine, -and where it is used, on the consumer's, and nowhere between. That is stricter than today, where the -controller holds every minted credential in the clear when it makes it. - -**Who a consumer is stays the mesh's.** The login a consumer presents is the mesh's derivation, which -both ends agree on by construction ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md), -issue 023). A provider makes what a consumer is *given*, never what it is *called*. - -**Rotation is the provider's act.** Asked to rotate, a provider makes a new value and answers again, -and the mesh redelivers it. An operator-delivered value ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)) -is still never replaced by the mesh: its provider is the operator. - -**The foundation is the one exception.** The foundation's own credentials, the vault's own access and -the mesh's root secrets exist before any provider can answer. The controller mints those: at genesis, -and when an operator rotates a root secret ([ADR 0085](0085-a-secret-is-a-provision.md)). It seals -them to the operator key as today, including any form the foundation's software needs, such as the -broker admin's hash. Nothing else is minted by the controller. - -## What this changes in earlier records - -On acceptance, each of these is superseded or amended by this record, not edited: - -- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) is superseded: a provider no - longer receives a minted credential. Its refusal of provider-held symmetric keys stands, and is why - option 2 is rejected here. -- [ADR 0085](0085-a-secret-is-a-provision.md) is amended: the vault generates a module's own secret - rather than recording one the controller minted. "The vault stores no plaintext, ever" stands. It - makes a value, seals it, hands it to the mesh and keeps only what it keeps today. -- [To-be 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) and - [to-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) are amended: both describe - the controller minting a module's credentials and rotating them. After this, the controller mints - only the foundation's, and a provider rotates by answering again. - -## Consequences - -- **A consumer waits for its provider.** Its requirement is not resolved until the provider has - answered, so resolution gains a state, *waiting on a provider*, that is shown rather than silent. - Today a consumer can receive a credential before the resource behind it exists. After this it - cannot. -- The provider harness in the SDK changes: an adapter's create answers with its contract's fields - instead of returning nothing, and every provider in the catalogue moves to it. This is one - migration per provider, the cost 0048 named for changing the contract, and it is paid once. -- The controller gains the return path: receiving a sealed answer on a provider's account and - delivering it. Each contribution gains the consumer node's public key. Data provisions gain the same - path, so the analytics and DNS providers can finally answer. -- A contract needing a derived form is met by its provider. The broker admin's hash stays with the - controller, because that admin is a foundation credential. -- **What got harder:** a provider that is down cannot hand out credentials, where today the controller - could mint one in its absence. That is honest, because a credential for a resource that does not - exist yet was never usable, but it moves a failure from later and silent to earlier and visible. - -## How it is checked - -| Rule | Checked by | -|---|---| -| A provider's secret fields are sealed before they leave it | An SDK test: an answer whose contract marks a field secret cannot be handed over unsealed. A controller test: an unsealed secret field in an answer is refused, not delivered. | -| A provider's answer reaches the consumer sealed to its node | A controller test delivering a provider's answer: each secret field opens with the consumer node's key and with no other, the controller's included. | -| A consumer's identity is still the mesh's | A resolution test: the login a consumer presents is the mesh's derivation, whatever the provider answers. | -| A consumer waits for its provider | A resolution test with no answer yet: the requirement shows as waiting on a provider, and nothing is delivered. | -| The controller mints only the foundation's credentials | A controller test: the minting function is reachable only from genesis and from root-secret rotation, and never for a module's provision. | -| The vault generates and rotates | A vault test: a requested secret is generated to its contract, and a rotation answers with a new value. | - -## References - -- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md): the decision this supersedes, - and the return path it left open -- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): identity stays the mesh's -- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0092](0092-an-operator-delivers-a-pair-credential.md): - the vault, and the operator as a provider -- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md): everything a module needs is a requirement diff --git a/02-DECISIONS/0113-the-vault-makes-every-secret.md b/02-DECISIONS/0113-the-vault-makes-every-secret.md new file mode 100644 index 0000000..7dd624d --- /dev/null +++ b/02-DECISIONS/0113-the-vault-makes-every-secret.md @@ -0,0 +1,182 @@ +--- +topic: what runs on it +status: proposed +date: 2026-09-25 +deciders: jochen +reconstructed: false +--- + +# 113. The vault makes every secret, a provider makes resources and data, and the mesh carries both + +## Context + +**A secret comes into being seven different ways today**, counted across the catalogue and the +controller on 2026-09-25: + +| kind | made by | used by | +|---|---|---| +| a credential between a consumer and a provider | the controller | 19 modules | +| a module's own secret (`own-secrets`) | the controller, as a random value nothing owns | 54 modules | +| a broker account | the controller, but only when a person runs a separate command; otherwise the random value above, which cannot work ([issue 095](../04-ISSUES/095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md)) | 49 modules | +| a `secret` from the vault | the controller mints it, and the vault only records it ([ADR 0085](0085-a-secret-is-a-provision.md), as amended) | 6 modules | +| a value an operator accepts | a person ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)) | where accepted | +| a licence for model access | a separate controller context with its own store | model consumers | +| the mesh's root secrets | genesis, sealed to the operator key | the foundation | + +**The vault was built to end the second row, and did not.** ADR 0085 says a module's own secret +*"stops being a generated value that nothing owns"*. 54 modules still use one, and 6 use the vault. +The replacement was added and the old path was never retired. The same has happened to the broker +account, a special case of the second row that fails silently when the separate command is forgotten. + +**Rotation has its own gaps.** To-be 13 makes rotation one command, all-or-nothing, and states the +window in which a consumer cannot authenticate: the provider has taken the new password, and the +consumer has not yet restarted with it. A consumer restarts only if its definition remembered to say +so, and a container fed by an env-file is not recreated when that file changes, so it keeps the old +value ([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)). +Nothing confirms that the new secret works. + +**And providers cannot answer with data.** [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) +left *"delivering provider-generated data back to a consumer"* to a separate decision. The analytics +provider's site id and the DNS provider's record have no way back, and say so in their code. + +## Considered Options + +**1. Keep the controller minting, and tidy the seven paths.** Rejected. The paths are the problem: +each is made, kept, rotated and audited differently, and tidying keeps all seven. + +**2. Every provider mints its own secrets, with one shared function in the SDK.** Rejected. It makes +generation uniform and leaves custody scattered: every provider's machine holds secrets the vault +never sees, so rotation, audit and the operator's break-glass copies cover only some of them. The +function would also need a conforming implementation in every language a provider is written in. + +**3. The vault makes every secret, and a provider that needs one requires it, like any consumer.** +Chosen. A provider serving a consumer requires a secret for that consumer from the vault. Secrets +become provisioning all the way down, with one maker at the bottom. + +## Decision + +**The vault makes every secret in the mesh.** Generation, to a secret's contract, exists in the vault +and nowhere else. The controller mints nothing. + +**A provider that needs a secret for a consumer requires it from the vault.** A provision's contract +declares it: *for each consumer, one secret*. Resolution expands that into one requirement per +consumer, named for the consumer. So gitea requiring a database makes the database's provider require +a secret named for gitea, and the vault answers it. The provider's own code does not change. It is +handed a login and a password, as it is today. + +**A secret has holders, and the vault delivers to each.** The database credential has two: the +provider, which creates the login with it, and the consumer, which presents it. The vault hands it to +the mesh, which delivers it to each holder sealed to that holder's node. Plaintext exists in the +vault while it is made and on each holder's machine, and nowhere else. The controller carries sealed +values it cannot open. + +**Every other secret takes the same path:** + +- a module's **own secret** is a `secret` requirement the vault answers. `own-secrets` is retired; +- a **broker account** is the module's identity on the bus. Its name is the mesh's, its password is a + secret the vault makes, and the controller creates the account with it, as it creates accounts + today. There is no separate command to forget; +- an **operator's value** that is secret is handed to the vault, which provides it like any other + secret. It is never replaced by rotation ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)); +- a **licence's** credential is an operator's value the vault keeps. What the licences context adds, + refreshing a token and a manager holding the refresh credential, is provider behaviour, decided in + its own record. + +**Genesis is the vault's first answer, not an exception.** Genesis raises the vault before anything +else and asks it for the foundation's secrets: the store's superuser, the broker's admin and its hashed +form, and the vault's own broker account. The vault answers with the same code it always uses, before +the bus exists. Genesis mints nothing itself. + +**A provider makes resources and data, and the mesh carries data back.** A provider answers with its +contract's non-secret fields: a site id, a registered name. The mesh delivers them to the consumer as +resolved values. Who a consumer is stays the mesh's: a provider makes what a consumer is *given*, +never what it is *called* ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)). + +### Rotation + +**It is asked of the vault**, by an operator or by the vault's own policy, such as the maximum age a +secret's contract sets. + +**It is provider-first.** The vault delivers the new value first to the holders that *accept* it, +such as the database, and waits for each to confirm it has applied it. Only then does it release the +value to the holders that *present* it, such as gitea. A consumer is never sent a value its provider +has not accepted, so the window shrinks to the consumer's own restart. A provider that does not +confirm holds the rotation: the consumer keeps the old value, which still works, and `status` shows the +rotation as waiting on that provider. It is never half-done and never silently abandoned. + +**Restarts are derived, not declared.** The mesh knows which process reads which secret, because the +definition reads it through its requirement ([to-be 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)). +When a secret changes, the host restarts every process that reads it, and recreates a container whose +env-file carries it. No definition has to remember `restart-on` for a secret. + +**It is confirmed.** A rotated secret is shown as unconfirmed until each consumer has restarted with +it and, where its definition declares a health check, passed it. Delivered is not the same as working, +and the mesh says which one it knows. + +| step | who | +|---|---| +| asks | an operator, or the vault's policy | +| makes the value | the vault | +| carries it | the controller, sealed, provider first | +| applies it on the provider | the provider's provisioner, which confirms | +| applies it on the consumer | the host, restarting or recreating what reads it | +| confirms it works | the consumer's restart and health check, shown in `status` | + +## What this changes in earlier records + +On acceptance, each of these is superseded or amended by this record, not edited: + +- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) is superseded: the controller + no longer mints a provider's credential; the vault makes it. That a provider is handed its + credential and seals nothing stands. +- [ADR 0085](0085-a-secret-is-a-provision.md) is amended: the vault makes a module's own secret rather + than recording one the controller minted, own secrets are retired, and genesis asks the vault for + the root secrets. "The vault stores no plaintext, ever" stands. +- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md) is amended: an operator delivers a secret + to the vault. +- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) and + [to-be 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) are amended: rotation is provider-first, + derived and confirmed, and the vault is the only maker. +- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md) + becomes a prerequisite: rotation cannot be trusted while a changed env-file leaves a container on + its old value. + +## Consequences + +- **The vault is on the path of every new or rotated secret.** Today the controller is, and both run + on the control-node, so no new single point of failure appears. It is stated rather than implied. +- Resolution expands per-consumer requirements from a provision's contract. The contract declares + them, never the provider's code, so what a provider requires stays predictable from the catalogue. +- The SDK's provider loop gains one thing: confirming that a rotation was applied. Adapters are + unchanged. +- The mesh gains the way back from provider to consumer, for confirmations and for data. +- 54 modules move from own secrets to vault requirements, and the broker account stops needing a + separate command. +- **What got harder:** a secret can no longer be made when the vault is down, where today the + controller makes one regardless. And a rotation waits for its provider. Both move failures from + late and silent to early and visible, which is the trade this record makes throughout. + +## How it is checked + +| Rule | Checked by | +|---|---| +| Only the vault makes secrets | A controller test: no code path mints a secret. A vault test: the one generation function is the only one, and genesis reaches it through the vault. | +| A provider's per-consumer secret comes from the vault | A resolution test: a consumer requiring a database expands to a secret requirement named for it, answered by the vault. | +| A secret reaches each holder sealed to its node | A controller test: each holder's copy opens with that holder's node key and with no other, the controller's included. | +| Own secrets are retired | A catalogue test: no definition declares an own secret, with a declared list of exceptions that shrinks to empty. | +| A broker account needs no separate command | A resolution test: assigning a module that speaks on the bus yields its account. | +| Rotation is provider-first | A rotation test: the consumer is not sent the new value until the provider confirms; a provider that never confirms leaves the consumer on the old value, shown as waiting. | +| Restarts are derived | A host test: changing a secret restarts every process that reads it, and recreates a container whose env-file carries it, with no `restart-on` declared. | +| Rotation is confirmed | A rotation test: the secret shows as unconfirmed until the consumer restarted and passed its health check. | +| A provider answers data back | A resolution test: the analytics provider's site id reaches its consumer as a resolved value. | + +## References + +- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md): the decision this supersedes, + and the return path it left open +- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0092](0092-an-operator-delivers-a-pair-credential.md), + [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): the vault, the operator, and identity +- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [to-be 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md): + everything a module needs is a requirement +- [Issue 095](../04-ISSUES/095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md), + [issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md): what fails today diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 4083388..984208e 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -158,7 +158,7 @@ python3 00-META/checks/index.py fail if stale - **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md) - **0110** — [A seat is a module assignment from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) - **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)* -- **0113** — [A provider makes what it provides, and the mesh carries it back to the consumer](0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md) *(proposed)* +- **0113** — [The vault makes every secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)* ### How it is built diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index 42b4b4a..b9fc2ff 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -5,7 +5,7 @@ code: [] updated: 2026-09-25 decisions: - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md - - 02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md + - 02-DECISIONS/0113-the-vault-makes-every-secret.md - 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md - 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md - 02-DECISIONS/0084-which-provider-serves-a-consumer.md @@ -69,11 +69,29 @@ Which module answers, in order: 5. otherwise **refused**: naming the unheld seat, for a provision a seat delivers, even when exactly one provider exists; naming the candidates otherwise. -The provider makes what it provides and answers with its contract's fields. The mesh carries the -answer back to the consumer, sealing every secret field to the consumer's node -([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)). The vault -is a module provider like any other: it holds the `mesh-vault` seat and generates the secrets it -provides. +### Secrets: provisioning all the way down + +**The vault makes every secret, and it is the only thing that does** +([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)). It holds the `mesh-vault` seat, +so every `secret` requirement resolves to it. + +**A provider that needs a secret for a consumer requires one, like any consumer.** A provision's +contract declares it: *for each consumer, one secret*. Resolution expands that into one requirement +per consumer, named for that consumer: + +1. gitea requires `postgres-database`; +2. the database provider, to serve gitea, requires a `secret` named for gitea; +3. the vault makes it and hands it to the mesh; +4. the mesh delivers it to both holders, each sealed to its own node: the database's machine, to + create the login, and gitea's, to present it; +5. the database provider creates the login, exactly as it does today, and gitea connects. + +A module's own secret, a broker account's password and a secret operator value take the same path. +Nothing in the mesh makes a secret except the vault. + +**A provider makes resources and data.** Beyond secrets, a provider answers with its contract's +non-secret fields: an analytics site id, a registered public name. The mesh carries them back to the +consumer as resolved values. ### The node's host @@ -116,11 +134,11 @@ A value a person chooses: a public name for an endpoint, a greeting, how many wo needs no provider module, no grant and no credential. If asking a person for a value took more than that, module authors would route around it, and the literals this replaces would come back. -**A secret operator value**, such as an external API key, is still an operator requirement: its -provider is the operator. What differs is where it is kept. The vault holds it as an -operator-delivered value ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)), -never as a setting, because anything secret belongs in one place that can seal and audit it. The -vault is its custodian, not its provider. +**A secret operator value**, such as an external API key, follows the one rule for secrets: the +vault provides it. The operator hands the value to the vault, once +([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)), and the module requires +a `secret` like any other. The only difference is that rotation never replaces it: the vault cannot +make a new external key, so rotating one means an operator handing over a new value. **An endpoint** is an operator value inside a route requirement: the public name is chosen on the assignment, and the route provider answers. A public name already held by another assignment is @@ -162,10 +180,36 @@ slug, under the same rules as a slug. This has to be settled before a second ins ## Genesis -The one exception. Before any provider exists, genesis answers the foundation's own requirements -itself: the store's and broker's credentials, the vault's own access, and the root secrets. It seals -them to the operator key as it does now ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)). -After genesis, nothing is answered except by a provider. +**Genesis is the vault's first answer, not an exception.** It raises the vault before anything else +and asks it for the foundation's secrets: the store's superuser, the broker's admin in the hashed form +the broker needs, and the vault's own broker account. The vault answers with the same code it always +uses, before the bus exists, and seals the root secrets to the operator key as today +([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)). Genesis makes no secret itself, so +there is one way a secret is made, from the first one onwards. + +The vault can sit at the bottom because it requires nothing but a broker account: it keeps its data +on its own disk, not in the store. So the waterfall ends at the vault. + +## Rotation + +Rotating a secret is asked of the vault, by an operator or by the vault's own policy, such as a +maximum age in the secret's contract ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)). + +1. **The vault makes the new value.** +2. **It is delivered to the holders that accept it first.** For a database credential that is the + database's provider, whose provisioner applies it and confirms it did. +3. **Only then is it released to the holders that present it**, such as gitea. A consumer is never + sent a value its provider has not accepted, so the window in which it cannot log in shrinks to its + own restart. A provider that does not confirm holds the rotation: the consumer keeps the old value, + which still works, and the rotation shows as waiting on that provider. +4. **The host restarts every process that reads the secret**, and recreates a container whose + env-file carries it. It knows which, because a definition reads a secret only through its + requirement. No definition declares a restart for a secret. This needs + [issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md) + fixed, or a container fed by an env-file keeps the old value. +5. **It is confirmed.** The rotation shows as unconfirmed until each consumer has restarted with the + new value and passed its health check, where its definition declares one. Delivered and working + are shown as different things. ## Refusing @@ -175,10 +219,10 @@ requirement it names what is missing and what would answer it: - an unheld seat, and which modules could hold it; - no provider, and which modules could provide it; - an operator value with no default, and that the assignment must give it; -- a provider that has not answered yet, and which one. +- a provider, or the vault, that has not answered yet, and which one. -The last one is a state, not a failure. A consumer waiting for its provider is shown as waiting, and -nothing is delivered until the answer arrives ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)). +The last one is a state, not a failure. A consumer waiting for its provider or for the vault is shown +as waiting, and nothing is delivered until the answer arrives. ## What this retires @@ -188,7 +232,9 @@ nothing is delivered until the answer arrives ([ADR 0113](../../02-DECISIONS/011 | settings on an assignment ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)) | operator requirements, addressed to an instance | | a port the mesh assigns | a host requirement | | machine facts and machine placeholders | host requirements | -| a secret the controller mints | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)) | +| every secret the controller mints: provider credentials, own secrets, broker passwords, root secrets | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)) | +| a separate command issuing a broker account | a requirement resolved on assignment | +| `restart-on` naming a secret's file | a restart the host derives | | paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment | | literals carried in a definition | operator requirements with defaults | @@ -204,10 +250,12 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r the controller. The controller resolves requirements from the four providers, refuses as above, and fills the one form. Old mechanisms keep working beside it. *Ends when* a definition written entirely in the new form installs on a lab machine. -2. **Providers answer.** The SDK harness answers with contract fields and seals the secret ones, the - controller carries answers back, and the vault holds its seat and generates. *Ends when* the - analytics and DNS providers answer their consumers, and a module's own secret is generated by the - vault and rotated by it. +2. **The vault makes every secret, and providers answer.** The vault holds its seat and is the only + maker; resolution expands per-consumer secret requirements; genesis asks the vault first; the mesh + carries providers' data back. [Issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md) + is fixed first. *Ends when* no code path outside the vault makes a secret, the analytics and DNS + providers answer their consumers, and a database credential rotates provider-first, with the + consumer restarted by derivation and the rotation confirmed. 3. **Definitions move.** Every catalogue definition is rewritten, adopted and running assignments placed where their data already is. *Ends when* the list of definitions using an old form is empty, and the old forms are removed. @@ -229,6 +277,9 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r | A public name already held is refused | A resolution test: a second instance asking for a public name the first holds is refused, naming the first. | | Everything keyed by a module is keyed by its instance | A resolution test: two instances of one module on one node get two directories, two containers, two logins and separate settings. | | A consumer waits for its provider | A resolution test with a provider that has not answered: shown as waiting, and nothing delivered. | +| Only the vault makes secrets | A controller test: no code path mints a secret, genesis included. | +| A provider's per-consumer secret comes from the vault | A resolution test: requiring a database expands to a secret requirement named for the consumer, answered by the vault and delivered to both holders. | +| Rotation is provider-first, derived and confirmed | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): the consumer waits for the provider's confirmation, is restarted without a declared restart, and shows unconfirmed until healthy. | | Refusal names everything at once | A resolution test with three unresolved requirements of different kinds: one refusal naming all three. | | The old forms retire | The catalogue test listing definitions still using one. It must be empty before a form is removed. | diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index fdbfce4..6a913d5 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -35,7 +35,7 @@ document is written and this one's status becomes `implemented`. | [`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) | | [`26-the-seats.md`](26-the-seats.md) | What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) | -| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md), [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) | +| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) | ## Not yet written