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