diff --git a/docs/platform-root-custody.md b/docs/platform-root-custody.md index 6f3fa15..b4d1280 100644 --- a/docs/platform-root-custody.md +++ b/docs/platform-root-custody.md @@ -180,6 +180,34 @@ The outgoing value is retained until its successor is verified *and* the predecessor is proven rejected. Only then is it destroyed, and its destruction is recorded. +### Every credential says what it is + +A store of unlabelled values is the drawer of unmarked keys it was supposed to +replace. Every path — in `operators/` and in `platform/workloads/` alike — +carries five `custom_metadata` fields. They are not secret, so anyone orienting +themselves can read all of them without being granted a single value. + +| Field | Answers | +| --- | --- | +| `description` | what this credential *is*, in a sentence, including what it is **not** — the distinction between an account's password and the token it issues has already cost a session | +| `owner` | which repo or subsystem decides about it | +| `used_by` | what breaks when it changes. Rotation without this list is how a resolver gets left holding a superseded value for four days | +| `rotation` | the procedure, by document path — not a description of one | +| `on_loss` | what you do when the value is gone: is it resettable, by whom, and what does the reset cost | + +`on_loss` is the field this whole section exists for. On 2026-08-27 the LLDAP +admin predecessor was destroyed and the answer had to be reconstructed from +first principles under pressure. Written down in advance it is one line. + +Read the inventory with `ops-mason/scripts/custody-inventory.py`, which walks +both mounts, prints these fields, and marks any path missing them. It reads +metadata only and runs under `ops-mason-build`, so it is safe to hand to anyone. + +**As of 2026-08-28 that inventory reports 21 paths, 17 of them undescribed.** +The four that are described are the ones this session touched. The backlog is +real and is not urgent in the way a broken login is urgent — but each of those +seventeen is a credential nobody can identify without reading it. + ### Not yet built The `operators/` mount and its policy do not exist yet. Until they do, this @@ -212,14 +240,22 @@ ticket — about a secret's *value*, by stage. | Stage | Permitted about a value | | --- | --- | -| S0–S4 | A fingerprint: length, last three characters, or `sha256 \| head -c 8`. Enough to confirm two parties hold the same credential and no more. | -| S5+ | Fingerprint by hash only (`sha256 \| head -c 8`). No plaintext characters, on the grounds that a live platform's credentials outlive the conversation about them. | +| S0–S4 | A fingerprint: last three characters, optionally with length. Enough to confirm two parties hold the same credential and no more. | +| S5+ | Same, but only where the value is already in an automated pipeline may a hash (`sha256 \| head -c 8`) be used instead; a human comparing by eye still uses the last three. | | Any | Never the whole value, never a working prefix, never a screenshot of one. | -Prefer the hash form at every stage: it disambiguates just as well, it is what -this document's own evidence rules already ask for elsewhere ("public-key -fingerprints"), and it does not become a liability when a transcript is read -back later by someone who was not in the room. +**Last three characters, not a hash — decided deliberately.** A hash looks like +the stricter choice and is the worse one in practice. Browsers and secret stores +have a reveal button; none of them can show you a digest. To compare hashes you +must first extract the plaintext into a shell, where it lands in history, in a +process listing, or in a temporary file — so the "safer" convention forces a +plaintext handling step that reading three characters off a screen avoids +entirely. The rule exists to reduce exposure, and the hash form increases it. + +Hashes remain the right form where the value is already flowing through a +pipeline — verifying a KV write from a script, comparing two stores +programmatically — because there the plaintext is in memory regardless and no +new handling step is created. This is a disclosure rule, not a handling rule. It says what may be *spoken* about a value; it does not license reading a value out of a store that a role is