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.
199 lines
13 KiB
Markdown
199 lines
13 KiB
Markdown
---
|
|
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
|