Graduates research 016. Retiring a login is separated from removing a consumer, which closes a data-loss path in five providers; single-party secrets rotate in place; the number of parties decides, not the provider.
13 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| what runs on it | proposed | 2026-09-26 | jochen | false | 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 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 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
rotatecommand 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).
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's.
The resource is named after the consumer, not after a login. A consumer's identity is derived from its assignment (ADR 0049), 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.
- The vault makes the new value.
- 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.
- 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.
- Each reader confirms by being recreated with the new login and passing its health check, where its definition declares one.
- 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).
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: the changeover it left open is decided here.
- ADR 0049: 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: 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: 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. |