To-be 27 (proposed): a module requires, the mesh resolves — with ADRs 0109–0114, research 016 and issue 119 #113
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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