The recovery document¶
Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.
A map, and never key material. The daemon holds no key and cannot serve one, so this feature splits in two and the split is the whole design of it:
| What | Where it moves | |
|---|---|---|
| the instructions | what exists, who holds what, where the sealed kit is, what to do first | this route, gated by recovery.read |
| the key material | archive keys, Shamir shares, the break-glass identity | never through the daemon — paper in the cold kit, or wrapped to a recipient's own key through /keys/claims |
The document contains no secret and is still the single most useful artefact in the estate to somebody who already has a foothold: one page naming every holder, every recovery path and every account that cannot be re-minted. That is why it needs a capability of its own.
Routes¶
| Method | Path | Cmdlet | Cap | Write |
|---|---|---|---|---|
| POST | /recovery/documents |
— | recovery.read |
✓ |
| GET | /recovery/documents |
— | recovery.read |
— |
| GET | /recovery/documents/claim/{sha} |
— | — | — |
Cap is the required capability (— = public, no token); Write (✓) marks routes that pass
the write gate. Paths are relative to /v1/api; the cross-cutting contract is on the
API reference index.
Generated, never stored¶
A static recovery document rots, and a stale one is worse than none for the same reason an
untested kit is: it is trusted. POST generates from the current tree, watermarks the copy with
its requester and the time, records the issuance, and answers a one-time claim path. The body
lives only in the claim stash, under the same hour-stale sweep every other delivery uses. Nothing
keeps a document.
The supplied half — the credential inventory, the share holders, the cold kit, the account
list, the manual-provision escape and the first actions — arrives in the request body as map.
The daemon owns no credential inventory (sejf/inventory.yaml is in Nerthus.Core.Infra.Common),
so the credential section is marked reconciled: false and names its authority. A section with
nothing supplied is present and carries its reason in blind_spots rather than being omitted: a
recovery map missing its largest region in silence is the one failure a map cannot survive.
It refuses to carry a key¶
Every section is built by naming the fields that go in, and the finished document is scanned for
key-shaped strings — an age recipient or identity, a PEM or OpenSSH private key, a SOPS ENC[…]
value, a GitLab or runner token, an api key, a Discord webhook, a tunnel token. A match is a
422, and the message names the shape and never the text that matched it. A recovery document
containing a key is not a recovery document; it is a second copy of the vault.
Every issuance notifies the other holders¶
A recovery document pulled at three in the morning is either an emergency or a compromise, and the people who can tell which should find out at the time. The notice names who asked and when; it does not carry the claim path, because the sha is the secret that fetches the document.
Delivery can fail. The record of the attempt cannot: every outcome is a row — delivered,
no_channel, failed with its reason — in the issuance record, before the document can be
claimed. notify_state is delivered, partial, unnotified, or no_other_holder for an
estate with a single reader, which is not the same as nobody having been told.
Who holds recovery.read¶
Namiestnik and Zarządca by role, and grantable to a named person besides. The reader list
resolves through the admin.all short-circuit: no role bundle carries the string, so a census
over recovery.read alone answers zero in an estate where the Namiestnik reads it whenever they
ask. A holder whose via includes admin.all is marked revocable: false — revoking the string
leaves admin.all in the set and the gate keeps answering full.
The honest limit¶
If the fleet is dark, this route is dark with it. The paper copy in the cold recovery kit is the real one; this keeps it current.
Its value is in the ordinary cases: a fresh copy after a roster or recipient change, onboarding a share holder, answering "who can open what" during a review. It is not the disaster path.