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.
104 lines
9.4 KiB
Markdown
104 lines
9.4 KiB
Markdown
# 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 providers were found by
|
|
listing every definition that provides something and has a provisioner, not from memory. A first pass
|
|
of this survey worked from memory and missed one, mailu.
|
|
|
|
**The provisioner harness** in `mesh-sdk` calls `create` for a consumer when its contribution appears
|
|
or changes (its login, password or values), and after the provisioner restarts. It calls `remove` for
|
|
a login it applied earlier in the same process that is no longer contributed. Its record of what was
|
|
applied is kept in memory and keyed by login. Two things follow:
|
|
|
|
- a consumer whose derived login changes is removed under the old login and created under the new one,
|
|
in one pass;
|
|
- a contribution that disappears while the provisioner is down is never removed, and is left behind.
|
|
|
|
## 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, but only through a role that cannot log in owning the database, with each login working as it. Otherwise whatever one login creates is its own, and dropping that login means handing its objects over first. 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`. A user owning a schema cannot be dropped, and a login with an open session cannot. 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; the bucket **only if empty**. A bucket holding objects is left, and the failure logged | yes, by removing the access key and adding it again, which leaves a moment with no key | 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, with any queued messages, 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. The MQTT client identifier is chosen by the consumer, not tied to the login; a duplicate one takes the older session over |
|
|
| mailu | a mailbox `login@domain`, unless the consumer contributes its own account name | the mailbox with its mail, for a login-named one; a contributed name is left for an operator | yes, the password is set when the user exists | no for the password; a user can hold several authentication tokens, per the backend's documentation | **no**: a mail user *is* its mailbox |
|
|
| 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 |
|
|
|
|
verdaccio provides the npm registry too, and has no provisioner.
|
|
|
|
## Each provider's own administrative credential
|
|
|
|
| provider | identity | how the backend takes it |
|
|
|---|---|---|
|
|
| postgres | a fixed superuser | from a file **only at first initialisation** |
|
|
| 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 |
|
|
| mosquitto | a fixed admin client | seeded into the broker's dynamic-security file **once**; the seeding step skips when the file exists |
|
|
| lavinmq | a fixed admin name | per its own bootstrap code, **only on a first boot** with an empty data directory. No resource in the definition runs that bootstrap; what sets it on a running mesh is outside the catalogue |
|
|
| 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 |
|
|
|
|
**In five of seven, a new administrative value takes effect only through a command run with the old
|
|
one.** The credential file is mounted directly into both the server and the provisioner. So replacing
|
|
it recreates the provisioner, which then holds only the new value while the backend still expects the
|
|
old one, and the provisioner is locked out. That is worse than changing nothing.
|
|
|
|
Every provider module also has its own bus account, an own secret, read at start.
|
|
|
|
## What the tables show
|
|
|
|
1. **Every credential provider already rotates in place.** All nine 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. **Eight of nine 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 nine destroy the consumer's data when they remove the 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. minio drops only an empty bucket, and redis leaves the keys. In
|
|
those five, *retire a login* and *delete the consumer's data* are one call. With the harness keyed
|
|
by login, a changed login triggers it too.
|
|
4. **One backend holds two passwords on one login** (redis). Two hold several tokens beside one
|
|
password (gitea and mailu). A rotation built on two secrets per login would work for three
|
|
providers out of nine.
|
|
5. **Eight of nine 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. mailu cannot, because its
|
|
user is its mailbox, but it can give one user a second token. So every provider can hold **two
|
|
credentials** over one resource, though not every one as two logins. No adapter does either today.
|
|
6. **Ownership is a trap in two backends.** In postgres whatever a login creates is that login's, so a
|
|
second login cannot alter the first one's tables, and the first cannot be dropped while it owns
|
|
them. The one-step way out deletes them. In mssql, a login cannot be dropped with a session open,
|
|
nor its user while it owns a schema.
|
|
7. **The administrative credentials have one party and a fixed name**, and five backends take them
|
|
only at first initialisation. The provisioner needs the old and the new value at once to change
|
|
them. Today nothing can give it both.
|
|
8. **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.
|
|
|
|
## Seen on the way
|
|
|
|
The redis configuration names no ACL file, so a consumer's ACL user exists only in memory. A restart
|
|
of the redis server erases every consumer's user. The provisioner does not create them again until it
|
|
restarts itself, because its in-memory record says they are done. That is not a rotation finding, but
|
|
it is a live fault, and it is recorded here so it is not lost.
|