docs(custody): every credential says what it is
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3377672@bnt-lap001
Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
This commit is contained in:
tegwick 2026-08-28 11:42:51 +02:00
parent 3252e68a84
commit e7d326458c

View file

@ -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 |
| --- | --- |
| S0S4 | 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. |
| S0S4 | 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