|
|
|
@@ -0,0 +1,143 @@
|
|
|
|
|
---
|
|
|
|
|
topic: what runs on it
|
|
|
|
|
status: accepted
|
|
|
|
|
date: 2026-10-06
|
|
|
|
|
deciders: jochen
|
|
|
|
|
reconstructed: false
|
|
|
|
|
extends: 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](0114-a-shared-credential-rotates-over-two-credentials.md),
|
|
|
|
|
[to-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)). 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](0113-the-vault-makes-every-secret.md) 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](0114-a-shared-credential-rotates-over-two-credentials.md) 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](0092-an-operator-delivers-a-pair-credential.md)): 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](0113-the-vault-makes-every-secret.md): the decision this extends — what the vault cannot
|
|
|
|
|
replace is what an outside party issued
|
|
|
|
|
- [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md): read at start and applied, and
|
|
|
|
|
the staged rotation an applied secret waits for
|
|
|
|
|
- [ADR 0092](0092-an-operator-delivers-a-pair-credential.md): a delivered pair credential, unchanged
|
|
|
|
|
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md): an adopted machine, and the take
|
|
|
|
|
- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): the design this amends
|
|
|
|
|
- [To-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §7: the hand-act log a rotation's why is recorded in
|