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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user