Files
hq/02-DECISIONS/0228-a-value-given-by-hand-lives-only-until-its-modules-first-good-start.md
T
jochen 9c7ec6ba75 ADR 0228: a value given by hand lives only until its module's first good start
The controller read 0113 as refusing to replace any value given to it, so
two leaked secrets only their own modules read could not be rotated without
a person handling a new value. Amends to-be 13 and points 0113 at it.
2026-10-06 12:15:29 +02:00

10 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-10-06 jochen false 02-DECISIONS/0113-the-vault-makes-every-secret.md

228. A value given by hand lives only until its module's first good start

Context

Two of a module's own secrets leaked into logs on 2026-10-06, and the operator approved replacing both. Each module's definition says it reads the secret when it starts (taken: at-start), which is the form the mesh rotates by making a new value and starting the module again (ADR 0114, to-be 13). The controller refused both:

not rotated: … holds "…" as a value given to the mesh, not made by it, and the mesh will not replace what it cannot read (ADR 0113). Change it where it lives, then secret accept … with the new value

Both values had been given by hand long before, when the modules were moved from an older setup, and nothing outside the mesh uses either. The way out the refusal names has a person or an agent make a value and feed it to secret accept — a secret passing through hands, which is the thing the mesh exists to avoid.

The refusal reads ADR 0113 wider than it decides. 0113 says "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". What the vault cannot replace is a value only an outside party can issue — a vendor's key, a bot's token, a licence — because no value of the mesh's would work in its place. A secret the module reads at start and nobody else holds is not that: the old value is not needed to replace it, because the module is the only reader and it reads the new one when it starts. The controller applied the rule to every value it had been given, because the store records only made or accepted, and nothing in a module's definition says who issued a value.

The operator, the same day: "Our mesh should most definitely be able to rotate 'custom provided' passwords — in fact, ideally we immediately rotate them after assigning and running the module for the first time so the 'custom pwd' is gone", and "a custom pwd is only useful in case we're adopting an existing running container into our mesh."

Counted in the catalogue on 2026-10-06: 48 own secrets across 32 modules; 6 say taken: at-start, none says applied and 42 say neither. Of the 6 at-start ones, one is an outside party's key (an OpenAI key). Of the rest, at least ten are keys or tokens an outside party issues.

Considered Options

  1. Keep the refusal; add a separate verb that turns a given value into a made one (secret mint, with a reason). Rejected: it keeps a step whose only effect is to say "yes, really" to a rotation the definition already permits, and it leaves every given value in force until a person remembers to take it. The operator asked for the opposite default.
  2. Rotate a given at-start value like a made one, and stop there. Better, and still leaves the given value in force indefinitely after an adoption — the value a person handled stays the live one until somebody asks.
  3. Rotate it like a made one, and replace a newly given value on its own once the module has started on it, with the exception said where it belongs: in the module's definition, for a value an outside party issues. Chosen.
  4. Refuse secret accept for a secret the mesh may make, except on an adopted machine. Rejected: the mesh cannot tell a fresh install from one carrying data in from elsewhere on a converged machine — a restored volume holds the password it was made with — and the replacement after the first good start already bounds what a needless given value costs. It is accepted and said.

Decision

A module's own secret says who may make its value. By default the mesh may. An entry says "issued-by": "outside" when only a party outside the mesh can issue the value — a vendor's API key, a bot's token, a licence. The parser refuses any other word.

A secret the mesh may make — read at start, and not issued outside — is the mesh's to replace, whoever gave the value it holds:

  • secret rotate replaces a given value as it replaces a made one: made anew, sealed to the machine and the operator, recorded as made, and the machine sent so the module starts on it. A rotation may say why, and the why is recorded in the hand-act log.
  • A value given by hand lives only until the module's first good start under the mesh. Given through secret accept, it is marked; when the machine's clean report arrives — every resource applied, nothing failed or refused — for the declaration it was last sent, sent after the value was given, the controller replaces the value with one it makes, sends the machine, says so in its log and states secret-replaced as the controller seat's fact, never the value. On an adopted machine the module must also be taken: until then the mesh runs nothing of it. The mark is cleared as the value is replaced, so it happens once.
  • A given value exists to adopt something already running that holds it. A module the mesh installs fresh needs none; secret accept for such a secret is accepted, says that the value lives until the first good start, and says on a converged machine that a fresh install needs no value.

What stays as given, refused with the reason:

  • a value issued outside the mesh — never replaced; secret rotate names the issuer as the one to ask and says how old the given value is, and secret accept delivers the new one;
  • a value the module applies to a backend that takes it once, until the staged rotation of ADR 0114 exists;
  • a value the mesh's own code accepted — a bus account it issued is its word to a broker, and is never marked;
  • a value given before this record, which is not marked: the mesh does not decide for a person that something given long ago is used nowhere else. secret rotate replaces it when asked.

What this does not change. A pair credential an operator delivers is still never replaced by a made one (ADR 0092): it is held at both ends of a provision, and this record is about a secret one module holds. A secret whose definition says neither at-start nor applied is still not rotated. That the vault makes every shared secret, and its custody, stand as 0113 decided.

Consequences

  • Rotating a leaked secret a module reads at start is one call, whoever gave it, with no value in anybody's hands.
  • An adoption leaves no hand-given value in force once the module runs under the mesh. A person who needs the new value — a password typed at a login page — recovers it with the operator key, as for any value the mesh makes.
  • The catalogue must mark every secret an outside party issues. An at-start secret left unmarked is one the mesh will replace with a random value. The two OpenAI keys in the catalogue are marked with this change; the other outside keys say neither at-start nor applied, so they are not rotated either way, and marking them is tidying, not a prerequisite.
  • The controller ships before the catalogue marks anything. The parser refuses a field it does not know, so a definition saying issued-by is refused by a controller older than this record.
  • What got harder: the controller now acts on a machine's report by itself, sending it once more after the first good start of a module given a value. A replacement that cannot be sent is kept sealed and carried by the next push, and said.

How it is checked

Rule Checked by
A given value read at start rotates like a made one Controller inventory test: a value accepted for an at-start secret rotates, and is recorded as made.
A given value is replaced once, after the first good start on it Controller inventory test: a report of a declaration sent before the value was given replaces nothing; a report of an older declaration replaces nothing; the clean report of the declaration sent after it replaces it; the same report again, and the next declaration's report, replace nothing.
An outside party's value is never replaced Controller inventory tests: a value accepted for a secret marked issued-by: outside is not marked and not replaced after a start; rotate refuses it naming the issuer and the value's age; a definition changed to say outside after the value was given keeps it.
An applied value, and one the mesh's own code accepted, stay as given Controller inventory test: neither is marked or replaced after a start; rotate of an applied value is refused as not stageable.
An adopted machine waits for the take Controller inventory test: a module held as found keeps its given value through a clean report; after the take, the next clean report replaces it. A module no longer assigned is never replaced.
A good start is a clean account of a declaration Controller test: a refusal, a failure, or a bare word that the machine is there is not one.
The definition says who issues a value Catalogue test: issued-by is read, written back as read, and any word but outside is refused.
The fact is stated, and permitted Broker test: the controller's grant and its seat's emits both name secret-replaced, and nothing else is added.
A rotation through the console carries why Controller test: the rotate verb passes why and cause to secret rotate; why beside a provision is refused as passed over.

References

  • ADR 0113: the decision this extends — what the vault cannot replace is what an outside party issued
  • ADR 0114: read at start and applied, and the staged rotation an applied secret waits for
  • ADR 0092: a delivered pair credential, unchanged
  • ADR 0100: an adopted machine, and the take
  • To-be 13: the design this amends
  • To-be 45 §7: the hand-act log a rotation's why is recorded in