From 942ebe350f7903512eae63e5c21de45aa7cebfb1 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 00:14:23 +0200 Subject: [PATCH] Research 016: survey how each provider can rotate a credential Overlap as drafted in 0113 would have deleted consumer data: seven of eight providers name the resource after the login and five drop it on remove. Rotation is now undecided in 0113 and to-be 27, pending the survey. Also: a requirement naming a seat resolves to its holder, a person chooses among remaining candidates at assignment, the controller's secrets are requirements of its definition, genesis seals to the control-node key, and moving the vault or broker is break-glass. --- .../00-overview.md | 51 ++++++ .../01-the-providers.md | 71 ++++++++ .../02-the-readers.md | 42 +++++ .../03-the-options.md | 75 ++++++++ ...s-a-module-assignment-from-a-closed-set.md | 54 +++--- ...e-definition-names-no-node-mesh-or-path.md | 10 +- .../0113-the-vault-makes-every-secret.md | 168 +++++++----------- 03-DESIGN/01-to-be/26-the-seats.md | 34 ++-- .../27-a-module-requires-the-mesh-resolves.md | 116 ++++++------ 9 files changed, 422 insertions(+), 199 deletions(-) create mode 100644 01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md create mode 100644 01-RESEARCH/016-how-a-credential-can-be-rotated/01-the-providers.md create mode 100644 01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md create mode 100644 01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md new file mode 100644 index 0000000..83f184d --- /dev/null +++ b/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md @@ -0,0 +1,51 @@ +--- +status: active +initiated: 2026-09-26 +touches: + - 02-DECISIONS/0113-the-vault-makes-every-secret.md + - 02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md + - 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md + - 03-DESIGN/01-to-be/13-credentials-and-their-rotation.md + - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md + - 04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md +--- + +# 016 — How a credential can be rotated + +**What.** Which rotation mechanisms the mesh's providers can actually support, measured against +their code rather than assumed. Every provider in the catalogue was read: how it names what it +makes for a consumer, what its remove destroys, whether it re-applies a password, whether its +backend can hold two secrets for one login or two logins on one resource, and how its own +administrative credential is set. The consumer side was read too: when a module reads a secret, and +what makes it read a new one. + +**Why.** [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), as first drafted, +chose *overlap*: add a second login beside the first, move every reader, then remove the old one, +"through the adapter's existing create and remove", with "no consumer changes". A review showed that +claim false. In most providers the consumer's data is named after its login, and remove drops the data +with the login. Overlap as written would have deleted every consumer's database on its first +rotation. The mechanism has to be chosen on what the providers do. + +**What it touches.** Rotation in 0113 and [to-be 27](../../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md), +which both mark it undecided and point here. The identity budget in +[ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md), if a consumer +gets two logins. The rotation already implemented, which [to-be 13](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) +describes. + +**Documents.** + +- [01 — The providers](01-the-providers.md): the survey, one row per provider, and what it shows. +- [02 — The readers](02-the-readers.md): how a secret reaches a running process, and what already + recreates it. +- [03 — The options](03-the-options.md): each rotation mechanism against those facts, and a + recommendation. + +**Finding, in one paragraph.** All eight credential providers already re-apply a consumer's password +in place on every create, and the controller's `rotate` command relies on that. It is a working +rotation with a stated window. Seven of the eight name the consumer's resource after its login, +and five drop the resource when they remove the login, so a second login is impossible without +changing the adapter. Only one backend holds two passwords on one login. But every backend can grant +two logins the same rights over one resource. So overlap is possible everywhere, but only after each +adapter separates *the consumer's resource* from *the login that reaches it*. Administrative +credentials are a different case. They have one party, a fixed name, and in three providers they are +taken only at first initialisation. diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/01-the-providers.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/01-the-providers.md new file mode 100644 index 0000000..e537ffa --- /dev/null +++ b/01-RESEARCH/016-how-a-credential-can-be-rotated/01-the-providers.md @@ -0,0 +1,71 @@ +# 01 — The providers + +Read from the catalogue's main branch: each provider's provisioner adapter (`create`, `remove`), +the client functions they call, and each definition's own credentials. The provisioner harness in +`mesh-sdk` calls `create` for a consumer when its contribution appears or changes, and after the +provisioner restarts, and `remove` when the contribution goes. Its record of what was applied is +kept in memory. + +## The credential providers + +`login` is the consumer's derived identity, which the adapter receives as `as` +([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)). + +| provider | the consumer's resource is named | remove destroys | create re-applies the password | two secrets on one login | two logins on one resource | +|---|---|---|---|---|---| +| postgres | a database named `login`, owned by the role `login` | the database and the role | yes, `ALTER ROLE … PASSWORD` when the role exists | no: a role has one password | yes, both as members of one role that owns the database. Not done today | +| mssql | a database named `login`, with the login mapped into it | the database and the login | yes, `ALTER LOGIN … WITH PASSWORD` | no: a login has one password | yes, two logins mapped to users in `db_owner`. Not done today | +| mongodb | a database named `login`, with a user holding `dbOwner` | the database and the user | yes, `updateUser` with the new password | no: a user has one credential | yes, two users with `dbOwner` on one database. Not done today | +| redis | the key prefix `login:` on an ACL user named `login` | the user, **not** its keys | yes: `ACL SETUSER … reset … >password` replaces all of them | **yes**: an ACL user holds several passwords, added with `>` and removed with `<`. Today's `reset` discards all but the new one | yes, two users on one key prefix, once the prefix is not the login | +| minio | a bucket derived from `login`, and a service account whose access key is `login` | the access key and the bucket | yes, by removing the access key and adding it again | no, but an access key *is* the login: a second key is a second login | yes, two service accounts with one bucket policy. The access key is capped at 20 characters | +| lavinmq | a virtual host named `login`, and a user named `login` with permissions on it | the virtual host and the user | yes, the user is written again with the password | no: a user has one password | yes, permissions for two users on one virtual host | +| mosquitto | a client named `login`, with a role named for it on the topic prefix `login/#` | the client and its role | yes, the password is set when the client exists | no: a client has one password | yes, two clients holding one role, once the prefix is not the login. An MQTT client identifier still has to be unique per connection | +| gitea (npm) | a user named `login` on a team of an organisation that owns every package | the user; **packages survive**, because the organisation owns them | yes, the user's password is set on every run | no for the password; a user can hold several access tokens | yes, trivially: a second member of the same team | + +## The other providers + +| provider | answers with | credential | +|---|---|---| +| umami | a website, found by its public name | none. The site id it makes has no way back to the consumer today | +| cloudflare-dns | a public name derived from `login` | none handed to the consumer; its own API token is an operator value | +| showcase | a route | none | +| mesh-vault | custody: it records and withdraws sealed values in a ledger | it holds secrets; it makes none today | + +## Each provider's own administrative credential + +| provider | identity | how the backend takes it | +|---|---|---| +| postgres | a fixed superuser | from a file **only at first initialisation**. Afterwards the file is read by the provisioner to connect, and changing it changes nothing in the database | +| mssql | `sa` | from the environment at first setup. The image documents no file form, and the definition records that as a declared exception | +| mongodb | a fixed `root` | from a file **only at first initialisation**, when the data directory is empty | +| redis | the default user | from `requirepass` in a configuration the mesh renders, read when the server starts | +| minio | a fixed root user | from a file, read when the server starts | +| lavinmq | a fixed admin name | from the module's own secret, which its provisioner connects with | +| mosquitto | a fixed admin client | from the module's own secret, which its provisioner connects with; the broker's dynamic-security file holds it | + +Every provider module also has its own bus account, an own secret, read at start. + +## What the table shows + +1. **Every credential provider already rotates in place.** All eight re-apply the password on the + same login each time `create` runs. The controller's `rotate` command relies on that: it replaces + the credential in the inventory and sends both ends in one push. Its own comments state the window, + between the provider applying and the consumer restarting, in which the consumer cannot + authenticate. +2. **Seven of eight name the consumer's resource after its login.** Only gitea separates them, + because an organisation owns the packages. A second login therefore has no resource of its own to + reach, and cannot share the first one's without the adapter granting it. +3. **Five of eight destroy the resource when they remove the login.** These are postgres, mssql, mongodb, + minio and lavinmq. In today's adapters, *retire a login* and *delete the consumer's data* are one call. + This is the fault the first overlap draft would have triggered. +4. **Only redis holds two passwords on one login**, and gitea through tokens rather than its + password. A rotation built on two secrets per login would work for one provider out of eight. +5. **Every backend can give two logins the same rights over one resource.** Group roles in postgres, + database roles in mssql and mongodb, permissions in lavinmq, a shared role in mosquitto, a shared + policy in minio, a shared key prefix in redis, a shared team in gitea. No adapter does it today. +6. **The administrative credentials have one party and a fixed name.** The provider module is both the + one that applies the credential and the only one that reads it. In postgres, mongodb and mssql, a new + value only takes effect through a command run with the old value. A restart changes nothing. +7. **A consumer's identity is already the resource's name.** The login is derived from the + assignment, which is a module on a node, so the current login and "the consumer" are the same + string today. A second login would need a new name. The resource can keep the one it has. diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md new file mode 100644 index 0000000..7504391 --- /dev/null +++ b/01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md @@ -0,0 +1,42 @@ +# 02 — The readers + +How a secret reaches a running process, and what makes the process take a new one. + +## No module watches a secret + +A search of every module's code in the catalogue found no file watching of any kind, and no +re-reading of a secret while running. **Every reader reads a secret when it starts.** There is no +consumer that takes a new value live, so every rotation that changes what a consumer presents ends in +the consumer restarting. + +## The host already recreates what read a changed file + +The node host records, for every long-running container, the digest of each file it read when it +was created: its env-files, and every file bind-mounted into it directly. When a digest changes, the +host recreates the container, even though its spec is otherwise unchanged. This is the fix for +[issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md). +It is on the host's main branch, while the issue is still recorded as located, not fixed. + +Two cases are deliberately left out and need `restart-on` in the definition: + +- a file read out of a **mounted directory**, because the host cannot know whether the service reads + it once or watches it (a route proxy re-reads its routes live; a provisioner polls what it receives); +- a **process** rather than a container. + +## What the catalogue does with it + +22 definitions declare a secret they receive. In 16 of them it reaches the service through a +rendered file, usually an env-file. That case the host already covers. 14 declare `restart-on` for +something. Whether each of the 22 is fully covered depends on how its secret travels: through an +env-file or a direct mount, which the host covers, or through a directory or into a process, which +needs `restart-on`. **That was not classified module by module.** It is the check to run before a +rotation mechanism relies on it. + +## What this means for rotation + +- The *read at start* half of 0113's recipient model is already true, and mostly already handled by + the host. The restart is derived from the files a container reads, not declared per secret. +- Any mechanism, in place or overlapping, ends with the reader being recreated. What differs is + whether the credential it held until then still works. +- For a single-party secret, a module's own, the reader is also the only holder. There is nobody to + overlap with, and delivering the new file recreates the reader. diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md new file mode 100644 index 0000000..e668691 --- /dev/null +++ b/01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md @@ -0,0 +1,75 @@ +# 03 — The options + +Three mechanisms, weighed against [01](01-the-providers.md) and [02](02-the-readers.md). + +## A. In place, as today + +The vault makes a new value. Every applier re-applies it on the same login, which all eight +providers already do. Every reader is recreated by the host. + +- **Works with:** every provider, unchanged. It is what `rotate` does now. +- **Costs:** a window per consumer, from the provider applying to the consumer being recreated. They + are on different machines, and nothing orders them. A reader whose machine is unreachable from the + mesh but still reaches its provider stays locked out until the mesh reaches it again. +- **Admin credentials:** the natural form. The provider module is the only party, and it has to + apply the new value with the old one anyway (finding 6). + +## B. Two secrets on one login + +The applier adds the new password beside the old one, readers move, and the old one is removed. + +- **Works with:** redis natively, and gitea through tokens. **Not** with the other six, whose backends + hold one password per login (finding 4). +- **Verdict:** not a mechanism, a special case. Using it where it exists and something else + elsewhere is the "this way or that way" the design is trying to remove. + +## C. Two logins per consumer, over one resource + +The consumer has two logins derived by the mesh, and uses one at a time. The applier creates the other +with the new value and grants it the same rights over the consumer's resource. Readers move to it, +and then the old login is retired, which removes the login only, never the resource. + +- **Works with:** every backend (finding 5), **after** each adapter changes: + - the resource is named after the consumer, not the login. Today the two are the same string (finding + 7), so existing resources keep their names, the current login stays one of the two, and only the + second is new; + - both logins get the same rights, through a group role or its equivalent; + - *retire a login* and *remove the consumer* become two operations. Today they are one call, and + in five providers that call destroys data (finding 3). This is the whole of the danger, and it has + to be split, whatever else is chosen. +- **Costs:** + - seven adapters change; + - the harness learns the alternation and a confirmation per step; + - the second login's name must fit the tightest backend. That is 20 characters for a minio access + key ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)), and + a suffix spends part of it; + - an MQTT client identifier stays unique per connection, so mosquitto needs the client id kept apart + from the login. +- **Gains:** no window. A reader that cannot be reached keeps a working login until it can. +- **Does not apply to** single-party secrets: admin credentials and a module's own secrets. There is + no second party to overlap with. + +## Independent of the choice + +- **Split remove.** Retiring a credential must never be able to destroy a consumer's data. That holds + under A too, because A's remove is the same call. A remove that drops a database should be a + separate, explicit operation, which [ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md) + already implies: data outlives the declaration. +- **Admin credentials are applied by their own provider**, using the old value. That happens in place + whichever mechanism consumers get. Where a backend takes the value only at first initialisation, the + file alone changes nothing, and rotation needs the provider's provisioner to run the change. +- **Classify the 22 readers** ([02](02-the-readers.md)) before relying on derived restarts. + +## Recommendation + +- **Consumer credentials: C**, because it is the only mechanism every backend supports, and it closes + the window instead of shortening it. Its prerequisite, separating the resource from the login and + retiring a login from removing a consumer, is worth doing on its own, because it removes a + data-loss path that exists today. +- **Single-party secrets (admin credentials, a module's own): A.** In place, applied by the provider + that holds them. +- **Until the adapters are changed, A stays** as `rotate` implements it, with its window stated. It is + not replaced by a mechanism the providers cannot yet carry. + +This is two mechanisms, split by a property of the secret rather than by provider: whether it has one +party or two. Every provider is treated the same way for the same kind of secret. diff --git a/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md b/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md index 6bfbbb9..fdd08b4 100644 --- a/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md +++ b/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md @@ -74,19 +74,23 @@ node's settings for it, and what it serves. Holdings are not stored separately. assignment, and a second record of the same fact would be a second thing to disagree with the first. **Holding a seat may deliver a provision.** A seat that delivers a provision may only be held by an -assignment of a module that provides it, at the seat's scope. A requirement for that provision -resolves, in order, to: +assignment of a module that provides it, at the seat's scope. -1. a provider the consumer's node was **pinned** to, a consumer coupled to one provider's contents; -2. **the holder of the seat**, **even when another provider runs on the consumer's own node**; -3. otherwise refused, naming the unheld seat. +**A requirement may name a seat, and then the seat's holder answers it.** Naming the seat asks for +*the mesh's* one, not for whichever provider is nearest, so the holder answers **even when another +provider runs on the consumer's own node**, and nothing is asked of anyone. Unheld, the requirement is +refused, naming the seat. A builder asks for the mesh's npm registry this way, and is served by the +holder of `npm-package-registry` wherever it runs, with no pin on any machine. -**Co-location does not apply to a provision a seat delivers.** A seat exists to say *which one is the -mesh's*, and co-location answering first would let any second provider on a consumer's machine take -over silently for that consumer. That is the failure [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) -names for the vault. This keeps [ADR 0009](0009-modules-and-the-graph.md)'s rule that a requirement with -several answers is never guessed: the seat is the choice made once, mesh-wide, by assigning the holder, -instead of once per consumer by pinning. +**A requirement that names no seat resolves as [ADR 0084](0084-which-provider-serves-a-consumer.md) +has it**: a pin, then the provider on the consumer's own node, then the only provider. Where several +remain and none is local, **a person chooses when the module is assigned**. Assignment lists the +candidates, with the holder of a seat that delivers the provision suggested first, and records the +answer on the assignment as its pin. Without an answer the module is not assigned. This keeps +[ADR 0009](0009-modules-and-the-graph.md)'s rule that a requirement with several answers is never +guessed. The choice is made either by the requirement naming the seat, or by a person at assignment, +and never silently by what happens to run nearby. That is the failure +[issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) names for the vault. **A seat delivers a provision only where the mesh has one answer for everyone.** The artifact store, the npm registry, git and the vault are each one per mesh by their own records, so their seats @@ -95,13 +99,15 @@ deliver them. **The foundation's seats deliver nothing.** `mesh-controller`, `mesh-store` and `mesh-broker` name which assignment the mesh *itself* uses: the controller, the store holding its records, the broker carrying its bus. The store and broker modules may run on other nodes too, and a database or `amqp` -consumer is served by co-location from whichever runs on its own node, the seat's holder included. -Were `mesh-store` to deliver, every database consumer on every node would be sent to one machine. +consumer that names no seat is served by co-location from whichever runs on its own node, the seat's +holder included. Were `mesh-store` to deliver, a consumer could name it and be sent to the store the +mesh keeps its own records in. That is not a store for consumers. **A seat may reserve its provision.** Where a second provider would break the reason the provision exists, only an assignment holding the seat may provide it at all: the parser refuses a definition that provides it without being able to hold the seat, resolution refuses an assignment providing it -without holding the seat, and a pin cannot choose anyone else. `secret` is the one reserved provision. +without holding the seat, and a pin cannot choose anyone else: a requirement for it always names the seat. `secret` is the one +reserved provision. The vault is one per mesh because a second *"would be a second place to lose"* ([ADR 0085](0085-a-secret-is-a-provision.md), as amended), and a second `secret` provider is exactly that, whether a pin chose it or not. @@ -150,9 +156,13 @@ On acceptance, each of these is amended by this record, not edited: - [ADR 0009](0009-modules-and-the-graph.md): a claim in a definition says a module *can* hold a seat; the assignment says it does. -- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md): "a mesh runs one postgres - and one lavinmq" becomes one holder of `mesh-store` and one of `mesh-broker`. The store and broker - modules may run on other nodes. +- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) and + [ADR 0078](0078-the-store-and-broker-are-modules.md): "a mesh runs one postgres and one + lavinmq" becomes one holder of `mesh-store` and one of `mesh-broker`. The store and broker modules may + run on other nodes. +- [ADR 0084](0084-which-provider-serves-a-consumer.md): a requirement may name a seat, which its holder + answers; and where several providers remain and none is local, the choice is asked when the module is + assigned and recorded as a pin, rather than refused until someone pins it. ## Consequences @@ -163,8 +173,9 @@ On acceptance, each of these is amended by this record, not edited: - Manifest validation refuses an unknown seat, a seat named at the wrong scope, and a delivering seat named by a module that does not provide the provision. Resolution refuses a second holder, and an assignment holding a seat its module cannot hold. -- Resolution prefers the seat's holder for a provision it delivers, after a pin. A provider record - gains the module it came from. +- Resolution answers a requirement naming a seat with its holder. Assignment asks a person where + several providers remain, suggesting the seat's holder first, and records the answer as a pin. A + provider record gains the module it came from. - A `seats` command lists the set with each seat's holder, derived from assignments. - **What got harder:** a module wanting a new singular role can no longer invent a name. It needs a record. And an assignment has one more thing to say. Both are the point. @@ -179,8 +190,9 @@ On acceptance, each of these is amended by this record, not edited: | A seat outside the set is refused | Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat named by a module that does not provide it. | | Every module in use names a seat in the set | A controller test parses every catalogue manifest and fails on any refused seat. The lab's beds read the same manifests ([ADR 0089](0089-a-bed-reads-the-catalogue-it-proves.md)). | | A seat is held by one assignment, not by a module | A resolution test: the store module assigned to two nodes resolves, with one assignment holding `mesh-store`; a second assignment asking to hold it is refused. | -| The holder answers for a provision its seat delivers | Resolution tests: two providers with the seat held; a pin overriding the seat; a second provider on the consumer's own node, where the holder still answers; the seat unheld with two providers, and with **one** provider, both refused naming the seat. | -| The foundation's seats route nobody | A resolution test: with the store module on two nodes, a database consumer is served by the one on its own node, whichever holds `mesh-store`. | +| A requirement naming a seat is answered by its holder | Resolution tests: two providers with the seat held; a second provider on the consumer's own node, where the holder still answers; the seat unheld with two providers, and with **one** provider, both refused naming the seat. | +| Several providers and none local is a person's choice | An assignment test: the candidates are listed with the delivering seat's holder first; the answer is recorded as a pin; with no answer the module is not assigned. | +| The foundation's seats route nobody | A resolution test: with the store module on two nodes, a database consumer is served by the one on its own node, whichever holds `mesh-store`; a requirement naming `mesh-store` is refused, because it delivers nothing. | | A reserved provision has no other provider | The parser refuses a definition providing `secret` that cannot hold `mesh-vault`; resolution refuses an assignment providing it without holding the seat, and a pin on a `secret` requirement. | ## References 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 8d68e3e..e247c3e 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 @@ -71,8 +71,9 @@ unresolved requirement and what could answer it, all at once. | **the operator, through the assignment** | a value a person chooses that is not secret: a public name, a greeting, a number of workers | settings, carried literals | A module provider is chosen as [ADR 0084](0084-which-provider-serves-a-consumer.md) and -[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) say: a pin, then the holder of a -seat that delivers the provision; for a provision no seat delivers, co-location and then the only one. +[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) say. A requirement naming a seat is +answered by its holder. Otherwise it is a pin, then co-location, then the only provider, and where +several remain, a person chooses at assignment and the choice is recorded as a pin. A host provider is always the module's own node, because a host path or a port means nothing on any other. An operator value is the assignment's, or the requirement's default, or unresolved. @@ -133,6 +134,11 @@ On acceptance, each of these is amended by a record of its own, not edited: path moves from the definition to the assignment. - [ADR 0091](0091-a-mount-is-declared-three-ways.md): a mount's host side is a resolved requirement, checked as resolved rather than as a path the definition declares. +- [To-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md): the step that builds and runs a + store module as a database provider, beside the foundation's store on the same node, would run the + store module twice on one node. The adopted store module ([ADR 0078](0078-the-store-and-broker-are-modules.md)) + holds `mesh-store` and serves that node's database consumers by co-location, so there is no second + one. - The [glossary](../00-META/glossary.md): *provision* widens from "a service one module provides" to a requirement answered by any of the four providers, and *requirement* and *contract* are added. None of it lands while this record is only proposed, because the glossary is the authority on the words diff --git a/02-DECISIONS/0113-the-vault-makes-every-secret.md b/02-DECISIONS/0113-the-vault-makes-every-secret.md index 465f8c6..cc70c48 100644 --- a/02-DECISIONS/0113-the-vault-makes-every-secret.md +++ b/02-DECISIONS/0113-the-vault-makes-every-secret.md @@ -36,9 +36,9 @@ it rather than step around it. **Rotation has gaps.** To-be 13 makes rotation one command, all-or-nothing, with a stated window in which a consumer cannot authenticate. A consumer restarts only if its definition remembered to say so; -a container fed by an env-file is not recreated when that file changes -([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)); -and some secrets are read only when a service first initialises, where a restart changes nothing. +a container fed by an env-file was not recreated when that file changed +([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md), +since fixed in the host); and some secrets are read only when a service first initialises, where a restart changes nothing. **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 @@ -93,11 +93,12 @@ it first means reordering the whole installation and giving the vault a second w The vault cannot make that value. The module that received it delivers it to the vault, which keeps it and provides it like any other; rotating it means asking the backend again. -**Parties that are not modules take the same path.** The controller's own store login and bus account, -and each node agent's bus account, have no definition to require them. The controller asks the vault -on its own behalf, or a node's, and the vault answers the way it answers any requirement: made by the -vault, sealed to the recipient, carried by the mesh. The requirement is not written in a definition, -because the controller and a node agent are the mesh itself, but it is answered no differently. +**The controller is a module, and takes the same path.** Its store logins (inventory, identity and +licences) and its bus accounts are own secrets of its definition today, and become `secret` requirements +of that definition like any module's. **A node's host is the one party with no definition.** Its bus +account is a requirement the mesh makes for each enrolled node, answered by the vault, sealed to that +node and carried like any other. It is the only requirement not written in a definition, because the +host is what runs definitions. **Only the vault may provide `secret`.** An assignment providing it must hold the `mesh-vault` seat. The parser refuses a definition that provides it and cannot hold the seat, and a pin cannot route a @@ -115,18 +116,25 @@ as that base exists**, before any other module built on it, and everything neede generated by genesis: - the store's superuser, and the broker's admin in the hashed form the broker needs; -- the bus accounts of the temporary and permanent controller, the control-node's agent, the builder, - the broker's own provisioner and the vault; -- the controller's store login, and the first enrolment token. +- the bus accounts of the temporary and permanent controller (its account and the broker-management + login), the control-node's host, the builder, the broker's own provisioner and the vault; +- the controller's three store logins (inventory, identity and licences), and the first enrolment + token. Until the broker's provisioner runs, genesis creates the bus accounts it generated, with the broker's -admin, as the controller does today. Genesis seals all of it to the operator key, and when the vault is -installed it **delivers the values to the vault, recorded as the mesh's own**, not as an operator's. +admin, as the controller does today. Genesis seals all of it to the control-node's key, and when the vault +is installed the controller **delivers the values to the vault, recorded as the mesh's own**, not as an +operator's. Nobody has to be present for it. That distinction matters: an operator's value is never replaced ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)), and these are, because the vault can make their replacements. The broker's provisioner then adopts the accounts genesis created. From then on the vault makes every shared secret, and genesis has made its last one. +**Raising the vault or the broker again is a genesis act.** Moving the `mesh-vault` or `mesh-broker` +seat to a new assignment, or recovering either after it is lost, is done the way genesis did it: the +values it needs are delivered, not made by a vault that is not there. That is a break-glass procedure, +stated and checked, never an ordinary assignment. + **A provider makes resources and data, and the mesh carries data back.** A provider's adapter may answer 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 @@ -134,74 +142,44 @@ is *given*, never what it is *called* ([ADR 0049](0049-a-consumers-identity-fits ### Rotation -**It is asked of the vault**, by an operator or by the vault's policy, such as a maximum age in the -secret's contract. A delivered value the vault cannot replace, such as an external API key, is not -rotated by the vault: rotating it means an operator delivering a new one. +**Who asks and who makes are decided here; the mechanism is not.** A rotation is asked of the vault, +by an operator or by the vault's policy, such as a maximum age in the requirement's contract, and the +vault makes the new value. A delivered value the vault cannot replace, such as an external API key, is +not rotated by the vault: rotating it means an operator delivering a new one. A secret a backend +issued is rotated by the module that holds the backend asking it again and delivering the new value to +the vault. -**A secret's contract says how each recipient takes a new value:** +**Each recipient takes a new value one of two ways, marked on its requirement:** | recipient takes it by | example | what happens on rotation | |---|---|---| -| **applying** it | a provider creating the login; the broker's provisioner updating an account; the store's own provisioner changing its superuser | its provisioner applies the new value; it is never restarted for it | -| **reading it at start** | a consumer reading its password when it starts | the host restarts it, or recreates a container whose env-file carries it | +| **applying** it | a provider setting a login's password; the broker's provisioner updating an account; a store's provisioner changing its own superuser | its provisioner applies the new value; it is never restarted for it | +| **reading it at start** | a consumer reading its password when it starts | the host recreates it, because a file it read at creation changed | -A secret read only when a service first initialises cannot be rotated by a restart. Its contract marks -it applied, and a provisioner makes the change. Where no provisioner exists to make it, such as a -module's own bootstrap password, the contract marks the secret **not rotatable by the mesh**, and a -rotation request is refused, saying why, rather than restarting a service that would carry on with -the old value. The host derives which recipients read a secret at start from the requirement their -definition reads it through, so no definition declares a restart for a secret. A provider's -per-consumer secrets are applied, never read at start, so the host never restarts a provider for one. +The marking is on each requirement, not on the secret, because one secret has recipients of both kinds. +Every module in the catalogue reads its secrets at start, and none watches them +([research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/02-the-readers.md)). A secret a +backend takes only when it first initialises is marked applied, and its provider's provisioner makes +the change using the old value. Where no provisioner can make it, the requirement is marked **not +rotatable by the mesh**, and a rotation request is refused, saying why, rather than restarting a service +that would carry on with the old value. -**Old and new overlap: nobody is ever without a credential that works.** A credential is never -changed in place. The new one is added beside the old, every reader moves to it, and only then is the -old one removed. There is one mechanism, the same for every provider: +**How old and new change over is not decided here.** Three mechanisms were measured against every +provider's code in [research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): +in place, as the controller's `rotate` does today; two secrets on one login; and two logins over one +resource. Its findings bound the choice: -1. **The vault makes the new value.** -2. **Each applier adds it beside the old.** A consumer has two logins, both derived by the mesh, and it - uses one at a time. The provider's loop creates the other with the new value, through the adapter's - existing create, and leaves the one in use untouched. It verifies that the new login works and the - old one still does, and confirms. It repeats that confirmation on every reconcile pass until the - vault acknowledges it, so a lost message costs one pass. -3. **Only then is the new login released to the readers.** A reader receives the new login and its - value together. The host restarts it, or recreates a container whose env-file carries it. -4. **Each reader confirms**, by restarting with the new login and passing its health check, where its - definition declares one. -5. **Only when every reader has confirmed is the old login retired.** Each applier removes it, through - the adapter's existing remove, and verifies that it no longer authenticates. +- every credential provider already re-applies a password in place, so today's rotation works, with a + window in which a consumer cannot authenticate; +- seven of eight name the consumer's resource after its login, and five destroy the resource when they + remove the login. **No mechanism may retire a login through today's remove**, because in those five + it deletes the consumer's data; +- only one backend holds two passwords on one login; +- every backend can give two logins the same rights over one resource, once the adapter separates the + resource from the login. -**What overlap closes:** - -- a reader whose machine is offline keeps the old login, which still works, until it returns and - moves; the rotation shows as waiting on that reader, and nobody is locked out; -- a bus account's owner keeps its old account until it has confirmed the new one over the bus it still - has, so no party can lose the bus it would hear the new value on; -- a provisioner restarted mid-rotation is still delivered both values until the old is retired, so it - can verify either. - -**What overlap costs.** - -- **Consumer modules: nothing.** A consumer reads one login at a time and changes it when it restarts. -- **Providers: one duty.** Both of a consumer's logins must have the same rights over its data, - because the consumer's data was written under one login and is read under the other. In postgres, - both are members of one role that owns the data. That is the adapter's part, and the only place - overlap touches provider code. The alternation itself is the provider loop's, so every provider gets - it by using the harness. -- **The mesh:** it derives two logins per consumer, and both must still fit the tightest backend - ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)). -- **Secrets with no applier**, such as a module's own secret read only by itself, have no second party - to overlap with. They are delivered and the reader restarted, where their contract allows rotation at - all. - -| step | who | -|---|---| -| asks | an operator, or the vault's policy | -| makes the value | the vault | -| adds the new login beside the old | each applier's provisioner, confirming on every pass until acknowledged | -| moves each reader | the host, restarting or recreating what reads the secret | -| confirms each reader | its restart and health check | -| retires the old login | each applier's provisioner, once every reader has confirmed | -| shows progress | `status`: waiting on which applier or reader, never done until the old is retired | +The mechanism is decided in its own record, on those facts. Until then rotation stays as the +controller implements it, in place, with its window stated. ## What this changes in earlier records @@ -218,19 +196,17 @@ On acceptance, each of these is superseded or amended by this record, not edited so 0092's rule that an operator's value is never replaced does not apply to them. - [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) is amended: a broker account is created by the broker's provisioner, not the controller. Its scoping stands. -- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) is amended: the mesh derives two - logins per consumer, and both fit the tightest backend. - [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md), [to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md) and - [to-be 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) are amended: rotation overlaps old and new - instead of being all-or-nothing, with restarts derived and each step confirmed; the vault is - installed as soon as the shared runtime base exists, and genesis delivers its secrets to it; the vault - is the only maker. + [to-be 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) are amended: the vault makes a rotated value and each + requirement says whether its recipient applies it or reads it at start, while the changeover + mechanism stays as to-be 13 describes until its own record; the vault is installed as soon as the + shared runtime base exists, and genesis delivers its secrets to it; the vault is the only maker. - The [glossary](../00-META/glossary.md) gains *shared secret*, *recipient*, *applies* and *reads at start*, and *reserved provision*, once this record is accepted. - [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. + is a prerequisite, and its fix is in the host: a container is recreated when a file it read at + creation changes. The issue is to be recorded as fixed, and derived restarts rest on it. ## Consequences @@ -238,43 +214,37 @@ On acceptance, each of these is superseded or amended by this record, not edited place, on the same node. A secret can no longer be made while the vault is down. - 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 the alternation of two logins per consumer and repeated confirmation - of each step. A credential provider's adapter gains one duty, giving both logins the same rights over - the consumer's data. A data provider's adapter gains a return value. No consumer module changes. +- A data provider's adapter gains a return value. What a credential provider's adapter must change for + rotation is decided with the mechanism ([research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md)). - The broker's provisioner gains every bus account, and the controller loses five separate places it generates a secret today. - 54 modules move from own secrets to vault requirements. Six provider clients export a password generator nothing uses any more; it is removed, so no module can quietly start minting again. - The installation changes order: the vault is installed as soon as the shared runtime base exists, before any other module built on it. -- **What got harder:** a rotation lasts until its slowest reader has moved, so a reader offline for a - week keeps the old login valid for a week. That is shown, and it is the price of never locking anyone - out. A provider briefly holds two logins per consumer. A secret some services read only at first - start can no longer be "rotated" by a restart that quietly changes nothing; it is refused instead. +- **What got harder:** a secret some services read only at first start can no longer be "rotated" by + a restart that quietly changes nothing; it is refused instead, or applied by its provisioner. And + moving the vault or the broker is a procedure, not an assignment. ## How it is checked | Rule | Checked by | |---|---| -| Only the vault generates a shared secret after genesis | A controller test: no code path generates a shared secret. A catalogue test: no module's code generates one, found by scanning for generation calls, with none exempt. An installer test: genesis generates exactly the list above and delivers it to the vault, recorded as the mesh's own. | +| Only the vault generates a shared secret after genesis | A controller test: no code path generates a shared secret. A catalogue test: no module's code generates one, found by scanning for generation calls. Exempt are the vault itself, and randomness that is not a secret any other party holds, such as a password hash's salt, each named in a declared list. An installer test: genesis generates exactly the list above and delivers it to the vault, recorded as the mesh's own. | | A private key is made where it is used | A test per key: a node's sealing key never leaves the node, the operator's private key never enters the mesh, and the certificate authority's private key never leaves the controller's identity store. | | Only the vault provides `secret` | The parser refuses a definition providing `secret` that cannot hold `mesh-vault`, and resolution refuses a pin on a `secret` requirement. | -| Parties that are not modules take the same path | Controller tests: its own store login, its bus account and a node agent's bus account are each made by the vault and delivered sealed; an enrolment token reaches the controller only as what verifies it. | +| The controller and each node's host take the same path | A catalogue test: the controller's definition declares no own secret, only requirements. A controller test: a node's bus account is made by the vault and delivered sealed to that node; an enrolment token reaches the controller only as what verifies it. | +| Genesis's values reach the vault unattended | An installer test: genesis's values are sealed to the control-node's key and delivered by the controller when the vault is installed, with no operator step. | +| Moving the vault or broker is a procedure | A resolution test: an ordinary assignment moving `mesh-vault` or `mesh-broker` is refused, naming the procedure. | | A backend-issued secret enters through the vault | A vault test: a value delivered as issued is provided like any other, and rotating it is refused as the vault's act. | -| A secret with no provisioner to apply it is not rotated by restart | A vault test: rotating a secret marked not rotatable by the mesh is refused, naming why. | +| A secret with no provisioner to apply it is not rotated by restart | A vault test: rotating a secret whose requirement is marked not rotatable by the mesh is refused, naming why. | +| A rotation never destroys a consumer's data | A provider test per credential provider: rotating a consumer's credential leaves its resource and data intact. It fails today for no provider, because rotation is in place; it guards whichever mechanism replaces it. | | A provider's per-consumer secret comes from the vault | A resolution test: a consumer requiring a database expands to a secret requirement for it, answered by the vault and delivered to both recipients. | | Values are carried sealed | A controller test: each recipient's copy opens with that recipient's node key and no other; neither the controller nor a message on the broker can open one. | | Own secrets are retired | A catalogue test: no definition declares an own secret, with a declared list of exceptions that shrinks to empty. | | Bus accounts come from the broker's provisioner | A resolution test: assigning a module that speaks on the bus yields its account, created by the broker's provisioner with no separate command. | | An operator's value is never rotated by the vault, and genesis's values are | Vault tests: a rotation request on an operator's external key is refused, naming the operator; the same request on a value genesis delivered makes a replacement. | -| Old and new overlap | A rotation test: after an applier adds the new login, both authenticate; readers are released only after it confirms; the old login is removed only after every reader confirms, and then no longer authenticates. | -| An offline reader is never locked out | A rotation test with one reader's node offline: it keeps authenticating with the old login throughout, the rotation shows waiting on it, and completes when it returns. | -| A bus account's owner keeps the bus | A rotation test on a node agent's bus account: the agent stays connected on the old account until it has confirmed the new one. | -| A restarted provisioner can still verify | A rotation test restarting the applier's provisioner mid-rotation: it is delivered both values and confirms. | -| Both logins have the same rights | A provider test per credential provider: data written under one of a consumer's logins is read and changed under the other. | -| Both logins fit the tightest backend | A controller test: the two derived logins for the longest node and module names fit the limit ADR 0049 sets. | -| Restarts are derived from how a secret is read | A host test: a secret read at start restarts its reader, and recreates a container whose env-file carries it; an applied secret restarts nothing. | -| Rotation is confirmed | A rotation test: the rotation shows unconfirmed until every reader has restarted with the new login and passed its health check, and the old login is retired. | +| Restarts are derived from how a secret is read | A host test: a secret read at start recreates the container that read it at creation, through an env-file or a direct mount; an applied secret restarts nothing. A catalogue test: a secret that reaches a process, or a file in a mounted directory, has `restart-on` naming it. | | A provider answers data back | A lab test with a consumer requiring analytics: the provider's site id reaches it as a resolved value. | ## References diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index cfb9a9d..28adb35 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -8,7 +8,7 @@ code: - mesh-controller cmd/mesh-controller/source.go - mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql - mesh-catalog modules/gitea/module.json -updated: 2026-09-25 +updated: 2026-09-26 decisions: - 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 @@ -78,7 +78,8 @@ merges. controller, the store holding its records, the broker carrying its bus. **They route no consumer.** The store and broker modules may run on other nodes too. A database or `amqp` consumer is served by co-location, from whichever runs on its own node, the seat's holder included -([23 — Choosing a provider](23-choosing-a-provider.md)). +([23 — Choosing a provider](23-choosing-a-provider.md)). A requirement cannot name one of them, +because they deliver nothing. ## A seat that delivers a provision @@ -86,19 +87,18 @@ co-location, from whichever runs on its own node, the seat's holder included the npm registry, git and the vault are each one per mesh by decision. A seat that delivers a provision may only be held by an assignment of a module that provides it, at the seat's scope. -**Its holder answers for that provision.** A requirement for it resolves, in order, to: +**A requirement may name the seat, and then its holder answers.** Naming the seat asks for *the +mesh's* one, so the holder answers **even when another provider runs on the consumer's own machine**, +and nobody is asked anything. With the seat unheld, the requirement is refused, naming the seat. A +second provider can run beside the holder and harm nothing. A forge assignment holds +`npm-package-registry`, and an npm proxy may provide the same provision on another machine. A builder +that names the seat is still served by the forge, without anybody pinning it. -1. the provider the consumer's node was pinned to, because a consumer coupled to one provider's - contents has said so ([23 — Choosing a provider](23-choosing-a-provider.md)); -2. the holder of the seat, **even when another provider runs on the consumer's own machine**; -3. otherwise nothing, and the requirement is refused, naming the unheld seat. - -Co-location, which answers first for every other provision, does not apply here: a seat says which -one is the mesh's, and co-location answering first would let any second provider on a consumer's -machine take over for that consumer, silently. So a second provider can run beside the holder and -harm nothing. A forge assignment holds `npm-package-registry`. An npm proxy may provide the same -provision on another machine, and a module requiring an npm registry is still served by the forge, -without anybody pinning it. +**A requirement that names no seat resolves as any other**: a pin, the provider on the consumer's own +machine, the only provider. If several remain and none is local, a person chooses when the module is +assigned. The candidates are listed with the seat's holder suggested first, and the answer is recorded +as the assignment's pin ([27](27-a-module-requires-the-mesh-resolves.md)). Nothing is guessed, and +nothing changes silently because a second provider happened to appear nearby. **Moving the role is changing which assignment holds the seat.** No definition changes and nothing is unassigned: the forge keeps running, and keeps holding `git`, when its npm role moves. A module can @@ -106,9 +106,9 @@ take the role only if its definition says it can hold the seat. **The vault's provision is reserved.** Only an assignment holding `mesh-vault` may provide `secret` at all: a definition providing it that cannot hold the seat is refused, an assignment providing it without -holding the seat is refused, and a pin cannot choose another provider, because there is none. A second -provider of secrets would be a second place secrets live, which is what the vault being one per mesh -exists to prevent. +holding the seat is refused, and a `secret` requirement always names the seat, because there is no +other provider. A second provider of secrets would be a second place secrets live, which is what the +vault being one per mesh exists to prevent. **What a consumer receives is what it required**, the same as for any provision: where the provider answers, what it serves, and a credential. A consumer never reads the seat directly. The one exception 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 4e7473a..10d1ac9 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 @@ -2,7 +2,7 @@ layer: to-be status: proposed code: [] -updated: 2026-09-25 +updated: 2026-09-26 decisions: - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md - 02-DECISIONS/0113-the-vault-makes-every-secret.md @@ -59,15 +59,20 @@ can come from and a reviewer has to know every one. Which module answers, in order: -1. **a pin**: the assignment names a provider, because this consumer is coupled to that provider's +1. **the holder of the seat the requirement names.** A requirement may name a seat instead of leaving + the provider open. It asks for *the mesh's* one, and the mesh answers with whichever assignment + holds that seat, with nothing asked of anyone. Unheld, the requirement is refused, naming the seat + ([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), + [26 — The seats](26-the-seats.md)). A `secret` requirement always names `mesh-vault`, because + that provision is reserved; +2. **a pin**: the assignment names a provider, because this consumer is coupled to that provider's contents ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md)); -2. **the holder of a seat** that delivers the provision, where one does. Co-location does not apply - to these: the seat is the mesh's one answer for everyone ([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), - [26 — The seats](26-the-seats.md)); -3. for a provision no seat delivers, **the provider on the consumer's own node**; -4. for a provision no seat delivers, **the only provider** in the mesh; -5. otherwise **refused**: naming the unheld seat, for a provision a seat delivers, even when exactly - one provider exists; naming the candidates otherwise. +3. **the provider on the consumer's own node**; +4. **the only provider** in the mesh; +5. otherwise **a person chooses, at assignment**. Assigning the module lists the candidates, with the + holder of a seat that delivers the provision suggested first, and the answer is recorded on the + assignment as its pin. Without an answer the module is not assigned, and the refusal names the + candidates. Nothing is ever guessed ([ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md)). ### Secrets: provisioning all the way down @@ -103,10 +108,10 @@ Every other shared secret takes the same path: - a secret a backend issues itself, such as a forge's API token, which the module that received it delivers to the vault. -**Parties that are not modules take the same path too.** The controller's own store login and bus -account, and each node agent's bus account, have no definition to require them, because the controller -and a node agent are the mesh itself. The controller asks the vault on its own behalf or a node's, and -the answer is made, sealed and carried exactly as for a module. +**The controller takes the same path, because it is a module.** Its store logins and bus accounts are +own secrets of its definition today, and become requirements of that definition. **A node's host is the +one party with no definition**, because it is what runs definitions. Its bus account is a requirement +the mesh makes for each enrolled node, answered and carried exactly as for a module. **A provider makes resources and data.** Beyond secrets, a provider's adapter may answer with its contract's non-secret fields: an analytics site id, a registered public name. The mesh carries them @@ -206,14 +211,14 @@ runtime base, which the installation makes only after the store, the broker and the controller over the bus. So the vault is installed **as soon as that base exists**, before any other module built on it, and genesis generates what is needed until then: - the store's superuser, and the broker's admin in the hashed form the broker needs; -- the bus accounts of the temporary and permanent controller, the control-node's agent, the builder, - the broker's own provisioner and the vault; -- the controller's store login, and the first enrolment token. +- the bus accounts of the temporary and permanent controller (its account and the broker-management + login), the control-node's host, the builder, the broker's own provisioner and the vault; +- the controller's three store logins, and the first enrolment token. Until the broker's provisioner runs, genesis creates those bus accounts with the broker's admin, as the controller does today; the provisioner adopts them when it starts. Genesis seals everything to the -operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)), and when the -vault is installed it **delivers the values to it, recorded as the mesh's own**. That distinction keeps +control-node's key, and when the vault is installed the controller **delivers the values to it, +recorded as the mesh's own**, with nobody present. That distinction keeps them rotatable: an operator's value is never replaced, and these are, because the vault can make their replacements. @@ -221,48 +226,34 @@ That is the one time anything but the vault generates a shared secret, and it en over. It is also the answer to the objection ADR 0085 had to the vault being the only maker: the vault cannot make what exists before it, so what exists before it is delivered to it. +**Raising the vault or the broker again is a genesis act.** Moving either seat to a new assignment, or +recovering either after it is lost, delivers the values it needs the way genesis did. It is a stated +break-glass procedure, and an ordinary assignment attempting it is refused. + ## 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)). -An operator's external key is not rotated by the vault, which cannot make its replacement: an -operator delivers a new one. +maximum age in the requirement's contract, and the vault makes the new value +([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)). An operator's external key is +not rotated by the vault, which cannot make its replacement: an operator delivers a new one. -**A secret's contract says how each recipient takes a new value.** A recipient either *applies* it, -through a provisioner (a provider creating the login, the broker's provisioner updating an account, -the store's own provisioner changing its superuser), or *reads it at start*. A secret a service reads -only when it first initialises is marked applied, because a restart would change nothing. +**Each requirement says how its recipient takes a new value.** It either *applies* it, through a +provisioner (a provider setting a login's password, the broker's provisioner updating an account, a +store's provisioner changing its own superuser), or *reads it at start*. Every module in the catalogue +reads its secrets at start, and none watches them. The host already recreates a container when a file +it read at creation changes, its env-files and files mounted into it directly +([issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)). +So a reader's restart is derived, and a definition declares `restart-on` only for a secret reaching a +process, or a file in a mounted directory. A secret a service takes only at first initialisation is +applied by its provisioner or marked not rotatable by the mesh, and a rotation of it is refused rather +than reported done. -**Old and new overlap, so nobody is ever without a credential that works.** A credential is never -changed in place. Each consumer has two logins, both derived by the mesh, and uses one at a time: - -1. **The vault makes the new value.** -2. **Each applier adds it beside the old**, as the consumer's other login, through the adapter's - existing create. It verifies that the new login works and the old one still does, and confirms, - repeating that confirmation on every reconcile pass until the vault acknowledges it. -3. **Only then is the new login released to the readers**, such as gitea, login and value together. -4. **The host restarts every such reader**, and recreates a container whose env-file carries the - secret. It knows which, because a definition reads a secret only through its requirement, so no - definition declares a restart for a secret. An applier is never restarted for it. 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. **Each reader confirms** by passing its health check with the new login, where its definition - declares one. -6. **Only when every reader has confirmed is the old login retired**, through the adapter's existing - remove, and verified to no longer authenticate. - -So a reader whose machine is offline keeps working on the old login until it returns, a bus account's -owner keeps its bus until it has moved, and a provisioner restarted mid-rotation is still delivered -both values. The rotation shows as waiting on whichever applier or reader has not moved, and is done -only when the old login is gone. - -**No consumer module changes.** A provider's adapter gains one duty: both of a consumer's logins get the -same rights over its data, which in postgres means both belong to one role that owns it. The -alternation itself is the provider loop's. - -A secret some service reads only when it first initialises cannot be rotated by restarting it. It is -applied by a provisioner, or, where none exists, marked not rotatable by the mesh, and a rotation is -refused rather than reported done. +**How old and new change over is not settled.** Until it is, rotation stays as the controller does it +today: in place, both ends sent in one push, with a stated window in which a consumer cannot +authenticate. [Research 016](../../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md) +measured three mechanisms against every provider. One constraint holds whichever is chosen: **retiring a +credential must never remove a consumer's resource.** Today's adapters remove both in one call, and in +five providers that deletes the consumer's data. ## Refusing @@ -271,6 +262,7 @@ 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; +- several candidates and no choice made, and which they are; - an operator value with no default, and that the assignment must give it; - a provider, or the vault, that has not answered yet, and which one. @@ -309,9 +301,9 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r foundation's first secrets to the vault; the broker's provisioner creates every bus account; 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* nothing outside the vault generates a shared secret after genesis, a lab - consumer of analytics receives its site id, and a database credential rotates with old and new - overlapping, the consumer restarted by derivation and the rotation confirmed. + is fixed in the host already. *Ends when* nothing outside the vault generates a shared secret after + genesis, a lab consumer of analytics receives its site id, and a database credential rotates, the + vault making the value, the consumer recreated by derivation, and its data intact. 3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted and running assignments placed where their data already is, and each claim becomes a seat the module can hold, held by the assignment that holds it today. *Ends when* the list of definitions @@ -324,7 +316,7 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r |---|---| | Every requirement has one of the four provider kinds | The parser refuses any other. | | A definition names no host path, node or mesh | The catalogue tests of [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md). | -| A module provider is chosen by pin, seat, co-location, only one, refusal | Resolution tests for each step; for a second provider on a consumer's own machine when a seat delivers the provision; and for an unheld seat with exactly one provider, refused. | +| A module provider is chosen by named seat, pin, co-location, only one, a person's choice | Resolution tests for each step: a requirement naming a seat served by its holder even with another provider on the consumer's node, and refused when the seat is unheld; several candidates and none local, where assignment lists them with the seat's holder first and records the choice as a pin, and refuses without one. | | A provider answers within its contract | A controller test: an answer carrying a field its contract does not name, or missing one it does, is refused and not delivered. | | A module narrows a contract and never widens it | The parser refuses a module specification that loosens a contract's field. | | A host requirement is answered on its own node | A resolution test placing one elsewhere: refused. | @@ -337,12 +329,16 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r | Only the vault generates a shared secret after genesis | A controller test: no code path generates one. An installer test: genesis generates exactly the foundation's first secrets and delivers them to the vault. | | Only the vault provides `secret` | The parser refuses another provider of it, and resolution refuses a pin on a `secret` requirement. | | 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 recipients. | -| Rotation overlaps old and new | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): both logins authenticate while readers move; an offline reader keeps working on the old login; the old login is retired only after every reader confirms; an applied secret restarts nothing, and one read at start restarts its reader without a declared restart. | +| Restarts are derived, and rotation keeps data | The tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): an applied secret restarts nothing, one read at start recreates its reader without a declared restart, and rotating a consumer's credential leaves its resource and data intact. | | 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. | ## Not settled here +- How old and new credentials change over on rotation: in place, as today, or two logins over one + resource, as [research 016](../../01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md) + recommends for credentials with two parties. It is decided in its own record. + - The exact spelling of the one form. It must name a requirement and a field and nothing else. - The layout a node's default root uses beneath it, beyond one directory per assignment. - Whether a module provider's answer can change without the provider being asked, for example a