Apply review: two credentials, staged admin rotation, a ninth provider
The fact-check found mailu, whose user is its mailbox, so 0114 rotates over two credentials rather than two logins, the adapter choosing what a credential is. Also: minio keeps non-empty buckets; five backends take their admin credential only at first init, so single-party rotation is staged; postgres ownership moves to a non-login role; the harness keys by consumer; rotation state lives with the vault. Consistency fixes across 0110-0113, 26 and 27; issue 103 resolved by mesh-host PR #22.
This commit is contained in:
@@ -163,6 +163,10 @@ On acceptance, each of these is amended by this record, not edited:
|
||||
- [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.
|
||||
- [To-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md): the same two changes, in the design
|
||||
that describes choosing a provider.
|
||||
- [To-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md): genesis assigns the foundation's
|
||||
store, broker and controller holding their seats, where their definitions claim them today.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -191,6 +195,8 @@ On acceptance, each of these is amended by this record, not edited:
|
||||
| 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. |
|
||||
| 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. |
|
||||
| An assignment holds only a seat its module can hold | A resolution test: an assignment holding a seat its definition does not name is refused. |
|
||||
| Every seat is listed with its holder | A `seats` command test: every seat in the set is listed with its scope, what it delivers and its holder, and an unheld seat is listed as unheld. |
|
||||
| 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. |
|
||||
|
||||
@@ -65,6 +65,15 @@ failing to clone.
|
||||
**The build machine is not told the difference.** It receives a URL either way. Composing the URL is
|
||||
the controller's job, because only the controller knows where the seat's holder runs.
|
||||
|
||||
## What this changes in earlier records
|
||||
|
||||
On acceptance, each of these is amended by this record, not edited:
|
||||
|
||||
- [ADR 0069](0069-a-module-is-a-repository-and-a-path.md): a module's repository is recorded either as
|
||||
a path on the `git` seat or as an external URL, never as an address of the mesh's own forge.
|
||||
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): the closed set gains `git`,
|
||||
mesh-scoped, delivering `git`, held by a gitea assignment.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The controller's inventory gains a column saying which seat a source is on. It is empty for
|
||||
|
||||
@@ -22,7 +22,8 @@ copies. The issue records what that has already allowed:
|
||||
- a contributions file that carries host paths into containers, so every provider must mount its
|
||||
grants directory at the identical path;
|
||||
- defaults in code that disagree with their own manifests;
|
||||
- no way to assign one module to one node twice, because every identity is keyed by the module's name.
|
||||
- every identity keyed by the module's name, which is why one module cannot be assigned to one node
|
||||
twice. This record keeps that, and says so below.
|
||||
|
||||
**Paths are one case of a wider pattern.** A module gets what it needs through at least six separate
|
||||
mechanisms today, each with its own syntax and its own failure modes:
|
||||
@@ -80,8 +81,9 @@ other. An operator value is the assignment's, or the requirement's default, or u
|
||||
**A person's value stays cheap.** An operator requirement's contract is a type and, optionally, a
|
||||
default. It needs no provider module, no grant and no credential.
|
||||
|
||||
**Every secret is a `secret` requirement, answered by the vault**, with no exception by kind
|
||||
([ADR 0113](0113-the-vault-makes-every-secret.md)). An external API key an operator chooses is no
|
||||
**Every shared secret is a `secret` requirement, answered by the vault**, with no exception by kind
|
||||
([ADR 0113](0113-the-vault-makes-every-secret.md)). A private key is made where it is used and is not
|
||||
a requirement. An external API key an operator chooses is no
|
||||
different: the operator delivers it to the vault ([ADR 0092](0092-an-operator-delivers-a-pair-credential.md)),
|
||||
and the module requires a `secret` like any other. A provider that needs a secret for a consumer
|
||||
requires it from the vault, like any consumer, and answers with resources and data. The mesh carries
|
||||
@@ -124,7 +126,7 @@ way as every other secret ([ADR 0113](0113-the-vault-makes-every-secret.md)).
|
||||
|
||||
## What this changes in earlier records
|
||||
|
||||
On acceptance, each of these is amended by a record of its own, not edited:
|
||||
On acceptance, each of these is amended by this record, not edited:
|
||||
|
||||
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md): settings become
|
||||
operator requirements on an assignment.
|
||||
|
||||
@@ -15,7 +15,7 @@ controller on 2026-09-25:
|
||||
|
||||
| kind | made by | used by |
|
||||
|---|---|---|
|
||||
| a credential between a consumer and a provider | the controller | 19 modules |
|
||||
| a credential between a consumer and a provider | the controller | 16 modules, and 3 more for model access, counted below |
|
||||
| a module's own secret (`own-secrets`) | the controller, as a random value nothing owns | 54 modules |
|
||||
| a module's broker account | the controller, but only when a person runs a separate command; otherwise the random value above, which cannot work ([issue 095](../04-ISSUES/095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md)) | 49 modules |
|
||||
| a node's and the builder's broker accounts | the controller, each in its own code path | every node, the builder |
|
||||
@@ -78,7 +78,7 @@ it first means reordering the whole installation and giving the vault a second w
|
||||
requiring a database makes the database's provider require a secret for gitea, and the vault
|
||||
answers it. The provider's own code does not change: it is handed a login and a password, as today;
|
||||
- a module's **own secret**. `own-secrets` is retired;
|
||||
- every **broker account** on the mesh's bus: a module's, a node agent's, the builder's, the
|
||||
- every **broker account** on the mesh's bus: a module's, a node's host's, the builder's, the
|
||||
controller's. The broker holding `mesh-broker` carries the mesh's bus
|
||||
([ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), and its own provisioner creates
|
||||
each account from the vault's secret, like any provider. The controller no longer creates accounts,
|
||||
@@ -122,9 +122,11 @@ generated by genesis:
|
||||
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 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.
|
||||
admin, as the controller does today. Genesis seals each value twice: to the control-node's key, so that when the
|
||||
vault is installed the controller **delivers the values to the vault, recorded as the mesh's own**, not
|
||||
as an operator's, with nobody present; and to the operator key, as the break-glass copy
|
||||
[ADR 0085](0085-a-secret-is-a-provision.md) keeps of every root secret. The first enrolment token reaches
|
||||
the operator the same way.
|
||||
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
|
||||
@@ -132,7 +134,9 @@ 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,
|
||||
values it needs are delivered, not made by a vault that is not there. They come from the operator-sealed
|
||||
copies, which the operator opens. The vault keeps a copy of every secret sealed to the operator key
|
||||
(0085), so nothing the mesh relies on exists only inside the vault. 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
|
||||
@@ -149,14 +153,17 @@ not rotated by the vault: rotating it means an operator delivering a new one. A
|
||||
issued is rotated by the module that holds the backend asking it again and delivering the new value to
|
||||
the vault.
|
||||
|
||||
**Each recipient takes a new value one of two ways, marked on its requirement:**
|
||||
**Each recipient takes a new value one of two ways, marked per recipient:**
|
||||
|
||||
| recipient takes it by | example | what happens on rotation |
|
||||
|---|---|---|
|
||||
| **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 |
|
||||
|
||||
The marking is on each requirement, not on the secret, because one secret has recipients of both kinds.
|
||||
The marking is per recipient, not per secret, because one secret has recipients of both kinds. A
|
||||
provision's contract marks its provider's side, which applies. A consumer's side is read at start
|
||||
unless its requirement says otherwise. The broker's contract marks the host's bus account the same
|
||||
way: the broker's provisioner applies it, and the host reads it.
|
||||
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
|
||||
@@ -165,7 +172,7 @@ rotatable by the mesh**, and a rotation request is refused, saying why, rather t
|
||||
that would carry on with the old value.
|
||||
|
||||
**How old and new change over is decided in [ADR
|
||||
0114](0114-a-shared-credential-rotates-over-two-logins.md).** Three mechanisms were measured against
|
||||
0114](0114-a-shared-credential-rotates-over-two-credentials.md).** 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
|
||||
@@ -173,15 +180,15 @@ choice:
|
||||
|
||||
- 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.
|
||||
- eight of nine name the consumer's resource after its login, and five destroy the consumer's data 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;
|
||||
- one backend holds two passwords on one login, and two more hold several tokens;
|
||||
- every provider can hold two credentials over one resource, eight as two logins and one as two tokens,
|
||||
once the adapter separates the resource from the credential.
|
||||
|
||||
On those facts, 0114 rotates a credential two parties hold over two logins, rotates one a single
|
||||
party holds in place, and separates retiring a login from removing a consumer.
|
||||
On those facts, 0114 rotates a credential two parties hold over two credentials, rotates one a single
|
||||
party holds in place, staged, and separates retiring a credential from removing a consumer.
|
||||
|
||||
## What this changes in earlier records
|
||||
|
||||
@@ -192,7 +199,9 @@ On acceptance, each of these is superseded or amended by this record, not edited
|
||||
credential and seals nothing stands.
|
||||
- [ADR 0085](0085-a-secret-is-a-provision.md) is amended: the vault makes every shared secret, own
|
||||
secrets are retired, and its rejection of vault-only minting is answered by genesis delivering the
|
||||
first secrets. "The vault stores no plaintext, ever" stands.
|
||||
first secrets. Genesis seals its values to the control-node's key as well as to the operator key, so
|
||||
the controller can deliver them unattended. "The vault stores no plaintext, ever" and the
|
||||
operator-sealed break-glass copies stand.
|
||||
- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md) is amended: an operator delivers a secret
|
||||
to the vault. Genesis's values reach the vault by delivery too, but are recorded as the mesh's own,
|
||||
so 0092's rule that an operator's value is never replaced does not apply to them.
|
||||
@@ -201,9 +210,14 @@ On acceptance, each of these is superseded or amended by this record, not edited
|
||||
- [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: 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
|
||||
requirement says whether its recipient applies it or reads it at start, and the changeover is
|
||||
[ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s; 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 07](../03-DESIGN/01-to-be/07-the-foundation.md) is amended: genesis seals its values to
|
||||
the control-node's key as well as the operator key.
|
||||
- [To-be 12](../03-DESIGN/01-to-be/12-a-module-repository.md), [to-be 16](../03-DESIGN/01-to-be/16-module-coverage.md)
|
||||
and [to-be 18](../03-DESIGN/01-to-be/18-building-a-module.md) are amended: `own-secrets` is retired from the
|
||||
manifest they describe.
|
||||
- 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)
|
||||
@@ -217,7 +231,7 @@ On acceptance, each of these is superseded or amended by this record, not edited
|
||||
- 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.
|
||||
- A data provider's adapter gains a return value. What a credential provider's adapter must change for
|
||||
rotation is [ADR 0114](0114-a-shared-credential-rotates-over-two-logins.md)'s.
|
||||
rotation is [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s.
|
||||
- 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
|
||||
@@ -236,7 +250,7 @@ On acceptance, each of these is superseded or amended by this record, not edited
|
||||
| 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. |
|
||||
| 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. |
|
||||
| Genesis's values reach the vault unattended, and the operator keeps a copy | An installer test: each of genesis's values is sealed to the control-node's key and to the operator key; the controller delivers the first to the vault when it is installed, with no operator step; the operator's copy opens only with the operator key. |
|
||||
| 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 whose requirement is marked not rotatable by the mesh is refused, naming why. |
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: proposed
|
||||
date: 2026-09-26
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 0113-the-vault-makes-every-secret.md
|
||||
---
|
||||
|
||||
# 114. A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0113](0113-the-vault-makes-every-secret.md) decides who asks for a rotation (an operator, or the
|
||||
vault's policy) and who makes the new value (the vault). It leaves open how old and new change over.
|
||||
[Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md) read every
|
||||
credential provider in the catalogue against its code. There are nine:
|
||||
|
||||
- **all nine re-apply a password in place**, on the same login, every time they run. The controller's
|
||||
`rotate` command relies on that, and states the window it leaves: between the provider applying the
|
||||
new value and the consumer restarting with it, the consumer cannot authenticate;
|
||||
- **eight of nine name the consumer's resource after its login**: a database, a bucket, a virtual host,
|
||||
a key prefix, a topic prefix, a mailbox. Only the forge's npm registry keeps them apart, because an
|
||||
organisation owns the packages;
|
||||
- **five of nine destroy the consumer's data when they remove its login**: postgres, mssql and mongodb
|
||||
drop the database, lavinmq drops the virtual host with its queued messages, and mailu deletes the
|
||||
mailbox with its mail. In those adapters, *retire a login* and *delete the consumer's data* are one
|
||||
call. minio drops a bucket only if it is empty. The provisioner harness makes it worse: a consumer
|
||||
whose derived login changed is removed under the old login and created under the new one, in one pass;
|
||||
- **one backend holds two passwords on one login** (redis), and two hold several tokens beside one
|
||||
password (the forge and mailu);
|
||||
- **eight of nine can give two logins the same rights over one resource**. mailu cannot, because a mail
|
||||
user *is* its mailbox. It can give one user several tokens. In postgres, a second login is not enough
|
||||
on its own: objects belong to whichever login created them, so the resource must be owned by a role of
|
||||
its own;
|
||||
- **an administrative credential has one party and a fixed name.** The provider module both applies it
|
||||
and reads it. Five backends take it only at first initialisation: postgres, mssql, mongodb, mosquitto
|
||||
and lavinmq. Their credential file is mounted directly into both the server and the provisioner, so
|
||||
replacing the file recreates the provisioner holding only the new value, which the backend does not
|
||||
know yet. The provisioner is then locked out;
|
||||
- **no module watches a secret.** Every reader reads at start, and the host recreates a container when
|
||||
a file it read at creation changes
|
||||
([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)).
|
||||
|
||||
A first draft of 0113 chose to overlap old and new "through the adapter's existing create and
|
||||
remove". In five providers, that remove deletes the consumer's data. The mechanism has to be chosen on
|
||||
what the providers do, and the danger has to be closed whichever mechanism is chosen.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. In place for everything, as today.** Works with every provider unchanged. Rejected for credentials
|
||||
two parties hold. The window cannot be closed, only shortened, and the two ends are on different
|
||||
machines with nothing ordering them. For an administrative credential, it locks the provisioner out.
|
||||
|
||||
**2. Two secrets on one login.** Rejected as the mechanism. It works for three providers out of nine,
|
||||
and using it there and something else elsewhere would put the difference in the mesh instead of in the
|
||||
adapter.
|
||||
|
||||
**3. Two logins over one resource.** Rejected as the mechanism. It works for eight of nine, and not for
|
||||
mailu.
|
||||
|
||||
**4. Two credentials over one resource, with the adapter choosing what a credential is.** A credential
|
||||
is what a consumer presents, a login and a secret. The mesh alternates between two of them. Each adapter
|
||||
makes the second one the way its backend can: a second login for eight providers, a second token on the
|
||||
same login for mailu. A credential a single party holds is staged in place instead, and retiring a
|
||||
credential is separated from removing a consumer before either is used. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
### Retiring a credential never removes what it reached
|
||||
|
||||
**A provider's adapter keeps two things apart that today are one:** the consumer's *resource* (its
|
||||
database, bucket, virtual host, key or topic prefix, mailbox) and a *credential* that reaches it.
|
||||
They get separate operations:
|
||||
|
||||
- **ensure the resource**, named after the consumer;
|
||||
- **ensure a credential** with a value, holding the consumer's rights over its resource;
|
||||
- **retire a credential**. Anything it owns moves first to the resource's owner, and any session it has
|
||||
open is ended. Then the credential is removed, and nothing else;
|
||||
- **remove the consumer**, which is what removes the resource, and retires every credential it has.
|
||||
|
||||
**Remove the consumer runs only when the consumer no longer requires the provision from this provider.**
|
||||
That happens when its assignment goes, when its definition drops the requirement, or when re-resolution
|
||||
sends it to another provider. It never runs because a login or a value changed. The harness keys what it
|
||||
applied by the consumer, not by the login, so a changed login is a credential change and never a removal.
|
||||
What removing a resource does with the data in it stays
|
||||
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)'s, and re-resolving to another provider
|
||||
moves no data.
|
||||
|
||||
**The resource is named after the consumer, and owned by the resource, not by a login.** A consumer's
|
||||
identity is derived from its assignment ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)),
|
||||
and today its login is that same string, so **no existing resource is renamed**. Where a backend makes
|
||||
whatever a login creates the login's own, as postgres does, the resource is owned by a role that cannot
|
||||
log in, and each credential works as that role. Ownership of an existing resource moves to it once. A
|
||||
credential is retired by handing what it owns to that role, never by dropping what it owns.
|
||||
|
||||
### A credential two parties hold rotates over two credentials
|
||||
|
||||
**Two parties** means an applier and a reader that are different modules, or a module and a node's
|
||||
host. The vault's custody copy does not count, because the vault holds every secret. So this covers a
|
||||
credential between a consumer and a provider, and every bus account: a module's or a host's, applied by
|
||||
the broker's provisioner and read by its owner. **Each consumer has two credentials, one in use at a
|
||||
time**, both holding the same rights over the one resource. For eight providers the second is a second
|
||||
login, derived by the mesh as the consumer's identity with a short fixed suffix. For mailu it is a
|
||||
second token on the same login.
|
||||
|
||||
**The vault drives each rotation and records every step durably.** A provisioner learns which
|
||||
credentials to hold from what it receives: both of them, for as long as a rotation is under way. It
|
||||
never learns them from its own memory, so a provisioner restarted mid-rotation resumes from the step the
|
||||
vault has recorded.
|
||||
|
||||
1. **The vault makes the new value.**
|
||||
2. **Each applier ensures the unused credential with it**, with the consumer's rights, and leaves the
|
||||
one in use untouched. It verifies that the new credential authenticates and the old one still does,
|
||||
and confirms. It repeats the confirmation on every reconcile pass until the vault acknowledges it, so
|
||||
a lost message costs one pass.
|
||||
3. **Only then does the vault release the new credential to the readers.** The mesh delivers it and the
|
||||
value together, and the host recreates each reader, because a file it read at creation changed. A
|
||||
node's host is its own reader: it reconnects to the bus with the new login, and confirms over it.
|
||||
4. **Each reader confirms by authenticating with the new credential.** It shows this through its
|
||||
health check, where its definition declares one, or the applier sees the new credential in use,
|
||||
where its backend reports that. A reader for which neither is possible is confirmed by an operator.
|
||||
It is never assumed from the reader having restarted.
|
||||
5. **Only when every reader has confirmed is the old credential retired**, as above, and verified to no
|
||||
longer authenticate.
|
||||
|
||||
**A reader that goes away leaves the rotation.** A reader unassigned, or re-resolved to another
|
||||
provider, is no longer waited for. A consumer removed mid-rotation has both of its credentials retired
|
||||
with it.
|
||||
|
||||
**A rotation can be abandoned until the old credential is retired.** An operator abandons it. Readers
|
||||
that moved are given the old credential back, and recreated. The new credential is retired. Nothing is
|
||||
lost, because the old one was never removed.
|
||||
|
||||
`status` shows a rotation as waiting on whichever applier or reader has not moved, and it is not done
|
||||
until the old credential is gone. A reader that cannot be reached keeps working on the old credential
|
||||
until it can, and the rotation waits for it. That wait is shown, never hidden.
|
||||
|
||||
**Queues and permissions belong to the consumer, not to a login.** A module's queue on the bus is named
|
||||
for the module on its node, and both of its logins get the same permissions over it
|
||||
([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)). An MQTT client
|
||||
identifier is chosen by the consumer and is independent of its login. A reader recreated with a new
|
||||
login keeps it, and the broker hands the session over.
|
||||
|
||||
### A credential a single party holds rotates in place, staged
|
||||
|
||||
This covers a provider's administrative credential and a module's own secret, which only that module
|
||||
reads. The vault makes the new value, and the one party takes it:
|
||||
|
||||
- **applied**: the vault delivers the new value **staged, beside the current one**, and the current file
|
||||
is left as it is. The party's provisioner changes the backend using the current value, verifies the
|
||||
new one, and confirms. Only then does the vault make the new value current. This is the only form for
|
||||
a backend that takes its administrative credential only at first initialisation. Replacing the file
|
||||
first would lock the provisioner out;
|
||||
- **read at start**: the vault delivers the new value as current, and the host recreates the party.
|
||||
|
||||
There is no window between two parties, because there is only one. Where neither form can change the
|
||||
value, the requirement is marked not rotatable by the mesh, and a rotation is refused, saying why
|
||||
([ADR 0113](0113-the-vault-makes-every-secret.md)).
|
||||
|
||||
### One rule decides which
|
||||
|
||||
**The number of parties decides, never the provider.** The resolver knows it from the requirement's
|
||||
recipients, leaving out the vault's custody copy, so no definition declares it.
|
||||
|
||||
### Until an adapter can
|
||||
|
||||
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
|
||||
rotate in place, as today, and the window is stated when the rotation is asked for. So does a
|
||||
two-party credential whose backend has one fixed name and no second credential for it. These are listed
|
||||
by a check, and the list is meant to shrink. Separating *retire a credential* from *remove the
|
||||
consumer*, and keying the harness by consumer, come first. They close a data-loss path that exists
|
||||
today, whatever rotation does.
|
||||
|
||||
## What this changes in earlier records
|
||||
|
||||
On acceptance, each of these is amended by this record, not edited:
|
||||
|
||||
- [ADR 0113](0113-the-vault-makes-every-secret.md): the changeover it left open is decided here.
|
||||
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): a consumer's identity leaves room
|
||||
for the second login's suffix within the tightest backend it reaches, and both logins are checked
|
||||
against it.
|
||||
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md): a module's broker
|
||||
account is two logins with the same permissions over the same queue, one in use at a time. Its scoping
|
||||
is unchanged.
|
||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), already superseded by 0113:
|
||||
a provider now ensures and retires credentials over a resource it owns separately.
|
||||
- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation of a two-party
|
||||
credential is no longer all-or-nothing with a window. It overlaps, with each step confirmed.
|
||||
A single-party credential is staged, not replaced.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Every credential provider's adapter changes**, in two steps. The first separates *retire a
|
||||
credential* from *remove the consumer*. It names and owns the resource after the consumer, which
|
||||
keeps the name it has but moves ownership once in postgres and mssql, and the harness is keyed by
|
||||
consumer. The second ensures a second credential with the same rights.
|
||||
- **The vault gains rotation state**: each rotation's step, per applier and reader, recorded durably.
|
||||
Staged delivery is added for single-party secrets. The SDK harness carries the alternation and the
|
||||
repeated confirmation, so no adapter implements them.
|
||||
- **No consumer module changes.** It reads one credential at start, as today, and is recreated by the
|
||||
host when it changes. The exception is a reader that has neither a health check nor a backend that
|
||||
reports use: its rotations wait for an operator until it declares one.
|
||||
- **The derived identity is two characters tighter** in the tightest backend, a minio access key of 20
|
||||
characters.
|
||||
- **What got harder:**
|
||||
- a provider briefly holds two credentials per consumer;
|
||||
- a rotation lasts until its slowest reader moves, so an unreachable reader keeps the old credential
|
||||
valid until it is reached;
|
||||
- an adapter has four operations where it had two;
|
||||
- retiring a login in mssql has to end its sessions first.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Retiring a credential never removes a resource | A provider test per credential provider: retiring one of a consumer's credentials leaves its resource and data intact, reachable through the other. |
|
||||
| What a retired login owned survives it | A postgres and an mssql test: objects created under login A, tables included, are still there and alterable under login B after A is retired. |
|
||||
| A changed login is not a removal | A harness test: changing a consumer's derived login ensures a credential and never calls remove. |
|
||||
| Remove runs only when the requirement goes | Harness tests: unassigning, dropping the requirement and re-resolving each remove the consumer once; a rotation and a login change never do. |
|
||||
| No existing resource is renamed | A provider test: a consumer created before the change keeps its resource, with ownership moved to the resource's own role where the backend needs one. |
|
||||
| Both credentials hold the same rights | A provider test per credential provider: data and structure created under one credential are read, changed and altered under the other. |
|
||||
| Readers move only after the applier confirms | A rotation test: readers receive nothing until both credentials authenticate at every applier. |
|
||||
| A reader confirms by authenticating | A rotation test: a reader recreated but failing to authenticate with the new credential does not confirm, and the old credential is not retired. |
|
||||
| The old credential is retired only after every reader confirms | A rotation test with one reader's node unreachable: it keeps authenticating with the old credential, the rotation shows waiting on it, and it completes when the reader returns and confirms. |
|
||||
| A reader that goes away leaves the rotation | A rotation test: unassigning a waiting reader lets the rotation complete; removing the consumer mid-rotation retires both credentials. |
|
||||
| A rotation can be abandoned | A rotation test: abandoning after readers moved gives them the old credential back and retires the new one. |
|
||||
| Rotation state survives a restart | A test restarting the applier's provisioner, and then the vault, between steps: the rotation resumes from the recorded step. |
|
||||
| A single-party applied secret is staged | A rotation test on a first-initialisation administrative credential: the provisioner receives the new value beside the current one, applies it, and only then does the new value become current. At no point does it lose its connection. |
|
||||
| The number of parties decides | A resolution test: a secret with an applier and a reader in different parties is marked for two credentials, and one held by one module for in place. The vault's copy is not counted. |
|
||||
| Both logins fit the tightest backend | A controller test: both derived logins for the longest node and module names fit the limit ADR 0049 sets. |
|
||||
| A host rotates its bus login | A rotation test on a node's bus account: the host reconnects with the new login and confirms over the bus before the old one is retired. |
|
||||
| What still rotates in place is listed | A catalogue test lists every adapter that cannot yet ensure a second credential, and every two-party credential with one fixed name. A rotation of these states its window. |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): the survey this
|
||||
rests on, provider by provider
|
||||
- [ADR 0113](0113-the-vault-makes-every-secret.md): who asks and who makes
|
||||
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md), [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md),
|
||||
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): identity, bus accounts, and data outliving
|
||||
its declaration
|
||||
- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation as implemented
|
||||
- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md):
|
||||
why a reader's restart can be derived
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: proposed
|
||||
date: 2026-09-26
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 0113-the-vault-makes-every-secret.md
|
||||
---
|
||||
|
||||
# 114. A credential two parties hold rotates over two logins; one a single party holds rotates in place; and retiring a login never removes what it reached
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0113](0113-the-vault-makes-every-secret.md) decides who asks for a rotation (an operator, or the
|
||||
vault's policy) and who makes the new value (the vault). It leaves open how old and new change over.
|
||||
[Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md) read every provider
|
||||
in the catalogue against its code:
|
||||
|
||||
- **all eight credential providers re-apply a password in place**, on the same login, every time they
|
||||
run. The controller's `rotate` command relies on that, and states the window it leaves: between the
|
||||
provider applying the new value and the consumer restarting with it, the consumer cannot authenticate;
|
||||
- **seven of eight name the consumer's resource after its login**: a database, a bucket, a virtual host,
|
||||
a key prefix, a topic prefix. Only the forge's npm registry keeps them apart, because an organisation
|
||||
owns the packages;
|
||||
- **five of eight destroy the resource when they remove the login**: postgres, mssql, mongodb, minio and
|
||||
lavinmq. In today's adapters, *retire a login* and *delete the consumer's data* are one call;
|
||||
- **only one backend holds two passwords on one login**, redis. A second, the forge, holds several
|
||||
tokens beside one password;
|
||||
- **every backend can give two logins the same rights over one resource**: a group role, a database
|
||||
role, a shared policy, shared permissions, a shared role, a shared key prefix, a shared team. No
|
||||
adapter does it today;
|
||||
- **administrative credentials have one party and a fixed name.** The provider module is both the one
|
||||
that applies the value and the only one that reads it. Three backends take it only at first
|
||||
initialisation, so it can only be changed through a command run with the old value;
|
||||
- **no module watches a secret.** Every reader reads at start, and the host already recreates a
|
||||
container when a file it read at creation changes
|
||||
([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)).
|
||||
|
||||
A first draft of 0113 chose to overlap old and new "through the adapter's existing create and
|
||||
remove". In five providers, that remove deletes the consumer's data. The mechanism has to be chosen on
|
||||
what the providers do, and the danger has to be closed whichever mechanism is chosen.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. In place for everything, as today.** Works with every provider unchanged. Rejected for credentials
|
||||
two parties hold. The window cannot be closed, only shortened, and the two ends are on different
|
||||
machines with nothing ordering them. A reader the mesh cannot reach, but which still reaches its
|
||||
provider, is locked out until the mesh reaches it again.
|
||||
|
||||
**2. Two secrets on one login.** The applier adds the new password beside the old one. Rejected. It
|
||||
works for one provider out of eight. Using it where it exists and something else elsewhere is a
|
||||
mechanism per provider, which is what the mesh is trying to stop having.
|
||||
|
||||
**3. Two logins over one resource for everything.** Rejected for credentials a single party holds.
|
||||
The applier and the reader are the same module, so there is no second party to keep working while the
|
||||
other moves. A fixed administrative name has no second name to alternate with.
|
||||
|
||||
**4. Split by the secret, not by the provider.** A credential two parties hold rotates over two logins;
|
||||
one a single party holds rotates in place; and retiring a login is separated from removing a consumer
|
||||
before either is used. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
### Retiring a login never removes what it reached
|
||||
|
||||
**A provider's adapter keeps two things apart that today are one:** the consumer's *resource* (its
|
||||
database, bucket, virtual host, key or topic prefix) and the *login* that reaches it. They get separate
|
||||
operations:
|
||||
|
||||
- **ensure the resource**, named after the consumer;
|
||||
- **ensure a login** with a value, holding the consumer's rights over its resource;
|
||||
- **retire a login**, which removes the login and nothing else;
|
||||
- **remove the consumer**, which is what removes the resource. It is run only when the consumer is
|
||||
unassigned, as today, and never by a rotation. Whether the resource's data is kept beyond that stays
|
||||
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)'s.
|
||||
|
||||
**The resource is named after the consumer, not after a login.** A consumer's identity is derived from
|
||||
its assignment ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)), and today its login
|
||||
is that same string. So **no existing resource is renamed**. The login every consumer holds today
|
||||
becomes the first of its two, under the name it already has.
|
||||
|
||||
### A credential two parties hold rotates over two logins
|
||||
|
||||
This covers a credential between a consumer and a provider, and a bus account, which the broker's
|
||||
provisioner applies and its module reads. **Each consumer has two logins, derived by the mesh**: its
|
||||
identity, and its identity with a short fixed suffix. It uses one at a time, and both hold the same
|
||||
rights over the one resource.
|
||||
|
||||
1. **The vault makes the new value.**
|
||||
2. **Each applier ensures the unused login with it**, with the consumer's rights, and leaves the login
|
||||
in use untouched. It verifies that the new login authenticates and the old one still does, and
|
||||
confirms. It repeats the confirmation on every reconcile pass until the vault acknowledges it, so a
|
||||
lost message costs one pass.
|
||||
3. **Only then are the readers given the new login and value, together.** The host recreates each
|
||||
reader, because a file it read at creation changed.
|
||||
4. **Each reader confirms** by being recreated 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 retires it, the
|
||||
login and nothing else, and verifies that it no longer authenticates.
|
||||
|
||||
`status` shows a rotation as waiting on whichever applier or reader has not moved, and it is not done
|
||||
until the old login is gone. A reader that cannot be reached keeps working on the old login until it
|
||||
can, and the rotation waits for it. That wait is shown, never hidden.
|
||||
|
||||
**Where a backend identifies a connection separately from a login, the two are kept apart.** An MQTT
|
||||
client identifier must be unique per connection, so the mosquitto adapter derives the connection's
|
||||
identifier from the consumer and the login in use, and two logins never collide.
|
||||
|
||||
### A credential a single party holds rotates in place
|
||||
|
||||
This covers a provider's administrative credential and a module's own secret, which only that module
|
||||
reads. **The vault makes the new value, and the one party takes it:**
|
||||
|
||||
- **applied**: the party's provisioner changes it using the old value, then confirms. This is the only
|
||||
form for a backend that takes its administrative credential only at first initialisation, where a
|
||||
restart would change nothing;
|
||||
- **read at start**: the host recreates the party.
|
||||
|
||||
There is no window between two parties, because there is only one party. Where neither form can change
|
||||
the value, the requirement is marked not rotatable by the mesh, and a rotation is refused, saying why
|
||||
([ADR 0113](0113-the-vault-makes-every-secret.md)).
|
||||
|
||||
### One rule decides which
|
||||
|
||||
**The number of parties that hold the credential decides, never the provider.** A requirement whose
|
||||
secret has an applier and a reader in different modules rotates over two logins. A secret held by one
|
||||
module rotates in place. The resolver knows which from the requirement's recipients, so no definition
|
||||
declares it.
|
||||
|
||||
### Until an adapter can
|
||||
|
||||
**An adapter that cannot yet ensure a second login says so**, and the credentials it applies rotate in
|
||||
place, as today, with the window stated when the rotation is asked for. That is a migration state, not
|
||||
a second mechanism. It is listed by a check, and the list shrinks to empty. Separating *retire a
|
||||
login* from *remove the consumer* comes first in every adapter, because it closes a data-loss path that
|
||||
exists today, whatever rotation does.
|
||||
|
||||
## What this changes in earlier records
|
||||
|
||||
On acceptance, each of these is amended by this record, not edited:
|
||||
|
||||
- [ADR 0113](0113-the-vault-makes-every-secret.md): the changeover it left open is decided here.
|
||||
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): a consumer's identity leaves room
|
||||
for the second login's suffix within the tightest backend it reaches, and both logins are checked
|
||||
against it.
|
||||
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md): a module's broker
|
||||
account is two logins with the same permissions over the same queue, one in use at a time. Its scoping
|
||||
is unchanged.
|
||||
- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation of a two-party
|
||||
credential is no longer all-or-nothing with a window. It overlaps, with each step confirmed.
|
||||
Single-party rotation keeps the form to-be 13 describes.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Every credential provider's adapter changes**, in two steps. The first separates *retire a login*
|
||||
from *remove the consumer*, and names the resource after the consumer, which is the name it already
|
||||
has. The second ensures a second login with the same rights. That is seven adapters for the second
|
||||
step, since the forge's already holds its packages apart from the user.
|
||||
- **The SDK's provider harness** carries the alternation, the verification of both logins, and the
|
||||
repeated confirmation, so no adapter implements them. Its record of what was applied has to survive a
|
||||
restart of the provisioner mid-rotation. Today it is kept in memory.
|
||||
- **No consumer module changes.** It reads one login and a value at start, as today, and is recreated
|
||||
by the host when they change.
|
||||
- **The derived identity is two characters tighter** in the tightest backend, a minio access key of 20
|
||||
characters.
|
||||
- **What got harder:** a provider briefly holds two logins per consumer. A rotation of a two-party
|
||||
credential lasts until its slowest reader moves, so an unreachable reader keeps the old login valid
|
||||
until it is reached. And an adapter has four operations where it had two.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Retiring a login never removes a resource | A provider test per credential provider: retiring one of a consumer's logins leaves its resource and data intact, reachable through the other. |
|
||||
| Removing a consumer is not a rotation | A harness test: no rotation step calls remove; remove runs only when a contribution goes. |
|
||||
| No existing resource is renamed | A provider test: a consumer created before the change keeps its resource, and its existing login becomes the first of its two. |
|
||||
| Both logins hold the same rights | A provider test per credential provider: data written under one login is read and changed under the other. |
|
||||
| Readers move only after the applier confirms | A rotation test: readers receive nothing until both logins authenticate at every applier. |
|
||||
| The old login is retired only after every reader confirms | A rotation test with one reader's node unreachable: it keeps authenticating with the old login, the rotation shows waiting on it, and it completes when the reader returns and confirms. |
|
||||
| A lost confirmation costs one pass | A harness test dropping the first confirmation: the next pass repeats it. |
|
||||
| A restarted provisioner resumes a rotation | A harness test restarting the provisioner between steps: it resumes from the step it reached. |
|
||||
| A single-party secret rotates in place | A vault test: a provider's administrative credential is applied by its own provisioner with the old value; a module's own secret recreates the module; neither has a second login. |
|
||||
| The number of parties decides | A resolution test: a secret with an applier and a reader in different modules is marked for two logins, and one held by one module for in place, with nothing declared. |
|
||||
| Both logins fit the tightest backend | A controller test: both derived logins for the longest node and module names fit the limit ADR 0049 sets. |
|
||||
| A connection identifier never collides | A mosquitto provider test: two connections under a consumer's two logins are both accepted. |
|
||||
| Adapters still in place are listed | A catalogue test lists every credential provider that cannot yet ensure a second login. The list shrinks to empty, and a rotation of their credentials states its window. |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): the survey this
|
||||
rests on, provider by provider
|
||||
- [ADR 0113](0113-the-vault-makes-every-secret.md): who asks and who makes
|
||||
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md), [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md),
|
||||
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): identity, bus accounts, and data outliving
|
||||
its declaration
|
||||
- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation as implemented
|
||||
- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md):
|
||||
why a reader's restart can be derived
|
||||
@@ -159,7 +159,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) *(proposed)*
|
||||
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
|
||||
- **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)*
|
||||
- **0114** — [A credential two parties hold rotates over two logins; one a single party holds rotates in place; and retiring a login never removes what it reached](0114-a-shared-credential-rotates-over-two-logins.md) *(proposed)*
|
||||
- **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md) *(proposed)*
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
Reference in New Issue
Block a user