From e7d326458c49278d9d7fcfa5561f62f9522c09eb Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 28 Aug 2026 11:42:51 +0200 Subject: [PATCH] docs(custody): every credential says what it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five custom_metadata fields on every path in operators/ and platform/workloads/: description, owner, used_by, rotation, on_loss. Non-secret, so anyone orienting themselves can read all of them without being granted a value. on_loss is the field the section exists for. When the LLDAP admin predecessor was destroyed on 2026-08-27 the answer had to be reconstructed from first principles under pressure; written down in advance it is one line. used_by is the second lesson: rotation without a consumer list is how privacyIDEA's resolver was left holding a superseded bind credential for four days while everything else had been reconciled. Inventory with ops-mason/scripts/custody-inventory.py. It reports 21 paths today, 17 undescribed — the four described are the ones this session touched. Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 3377672@bnt-lap001 Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166 --- docs/platform-root-custody.md | 48 ++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 6 deletions(-) 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