net-kingdom/docs/platform-root-custody.md
tegwick e7d326458c
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
docs(custody): every credential says what it is
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
2026-08-28 11:42:51 +02:00

357 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Platform Root Custody
Status: active bootstrap policy
Date: 2026-05-24
## Purpose
This document defines the bootstrap trust model for NetKingdom platform-root
authority and the Railiance OpenBao bootstrap. It deliberately stores only
identity, policy, and procedure. It must never contain passwords, OpenBao
unseal keys, root tokens, recovery keys, one-time codes, private keys, or
screenshots of secret output.
The design goal is practical security: early infrastructure can be assembled
by a low-trust setup operator, then handed over to a dedicated king credential
when the control plane is ready. After handover, bootstrap-era material is
rotated, reset, or retired before live workloads depend on it.
## Bootstrap Roles
| Role | Purpose | Initial assignment |
| --- | --- | --- |
| Setup operator | Runs early MVP/prototype setup, receives notifications, operates Git/Gitea and day-to-day tooling | `tegwick` / `bernd.worsch@gmail.com` / Gitea `tegwick` |
| King credential | Dedicated platform-root break-glass and final custody credential, independent from day-to-day accounts | To be created before OpenBao live bootstrap |
| Future escrow holders | Later independent recovery control, preferably two-of-three custody | To be named after the first safe bootstrap path exists |
`tegwick` is the initial accountable setup operator and contact. That identity
is useful for notifications, work tracking, Git access, and early operations,
but it is not the desired long-term platform root of trust.
The king credential must be independent from ordinary Gitea and email access.
Email may receive notifications, but secrets, reset links, root tokens, unseal
shares, private keys, OTP seeds, and recovery codes must not travel through
email.
## King Credential
The king credential is the rare platform-root credential used to accept final
custody after the bootstrap substrate exists. It should be represented as a
dedicated identity such as `platform-root` or `king`, not as a personal
day-to-day account.
Minimum first version:
- stored in an offline or local password safe controlled by the human operator;
- protected by a separate strong passphrase;
- protected by an OTP factor or hardware-backed factor where the selected IAM
implementation supports it; the QR code or setup key must come from that
verifier, not from the local metadata console;
- not used for normal Git, browsing, chat, or daily administration;
- not recoverable through the day-to-day email account alone; and
- documented only by non-secret metadata in Git/State Hub.
Preferred later version:
- two-of-three independent custody or equivalent multisig-style approval;
- at least one independent human or institutional escrow holder;
- documented rotation and recovery drills; and
- break-glass use logged for review without exposing secret values.
## Custody Meaning
For this bootstrap, "master credential" means the offline recovery authority
needed to recover or administer the platform before normal identity,
authorization, and secret systems are fully online. It includes OpenBao
unseal/recovery material and, only if retained, the initial OpenBao root token.
The root token is not the normal admin credential. It is one-time bootstrap or
break-glass material. The preferred end state is:
- OpenBao is initialized and unsealed.
- Audit devices, mounts, auth methods, and policies are configured.
- A non-root `platform-admin` operator path exists.
- The initial root token is revoked, or is stored offline as sealed
break-glass material under the king credential custody policy.
- Routine access flows through NetKingdom IAM claims and scoped OpenBao
policies.
## Where The King Credential Lives
The king credential is an identity and custody record, not an OpenBao password.
In the current lightweight NetKingdom stack, the practical placement is:
| Part | Current home | Notes |
| --- | --- | --- |
| User record | LLDAP | dedicated `platform-root` or `king` user, separate from `tegwick` |
| Password login | Authelia over LLDAP | day-to-day email is notification-only |
| TOTP / token enrollment | privacyIDEA | QR/setup key comes from privacyIDEA self-service |
| privacyIDEA administration | `pi-admin` | setup and repair account, not the king credential |
| IAM Profile / OIDC token | KeyCape | target issuer for normal platform identity claims |
| Secret custody and audit | OpenBao | initialized after custody approval; not the human identity provider |
OpenBao enters the story after custody approval: it holds platform secrets,
unseal/recovery material handling, audit, policies, and temporary or future
admin auth methods. It should not be used as the place where the human king
password or OTP seed lives.
LLDAP deliberately has no public registration flow. The first-user process is
administrator-provisioned:
1. Log in to `https://lldap.coulomb.social` as `admin`.
2. Retrieve `LLDAP_LDAP_USER_PASS` from operator custody —
`operators/lldap/admin` in OpenBao (see *Operator Credential Custody*).
Until 2026-08-28 this step named a password-safe entry that did not exist;
see that section for what that cost.
3. Create the dedicated `platform-root` or `king` user.
4. Add the user to `net-kingdom-admins` for the current lightweight path.
5. Store the new account password in operator custody (`operators/`) or, for a
bootstrap identity, in the offline packet — never only in a browser
password manager.
6. Use `pi-admin` in privacyIDEA to confirm that the LLDAP resolver can see
the new user and that self-enrollment is allowed.
7. Log in to privacyIDEA self-service as `platform-root` and enroll the TOTP
token there. The QR/setup key belongs only in the authenticator and custody
storage, never in this repo, State Hub, chat, or the local control surface.
8. Verify the OIDC login path through a registered KeyCape client. KeyCape is
an issuer, not a dashboard; the root URL may return `404` while discovery,
health, authorize, token, and userinfo endpoints are healthy.
The local control surface records this as non-secret progress: account
reference, group assignment confirmation, password-safe confirmation, and later
login-path verification. It does not read or store the password, OTP seed, QR
code, or recovery codes.
Do not use a successful `platform-admin` login as proof of `platform-root`
custody unless the recorded king credential was intentionally changed to
`platform-admin`. `platform-admin` is the later non-root operator path; it is
useful, but it is not the same as proving the platform-root custody identity.
## Operator Credential Custody
*Decided 2026-08-28, after `NK-WP-0033`.*
Every credential a human operator holds has exactly one recorded home. Before
this was written, this document named a password-safe entry
(`net-kingdom/LLDAP/admin`) that had never been created. The consequence was not
theoretical: the LLDAP admin credential was rotated on 2026-08-23 with nowhere
to put the new value, the outgoing value survived only in a browser password
manager, and it was destroyed on 2026-08-27 when that entry was overwritten with
the replacement. A full working session went into diagnosing a system whose
predecessor credential no longer existed anywhere.
### Two stores, and the line between them
**`operators/` in OpenBao — everything reachable once you are logged in.**
Service account passwords, provider PATs, the LLDAP admin bind, privacyIDEA
`pi-admin`, machine identities like `state-hub-svc`. Auditable, backed up with
the rest of the platform, reachable from any machine, and writable through
`warden desk --act paste_once_provision` so the value never reaches a terminal
or shell history.
Path convention: `operators/<subsystem>/<account>` — for example
`operators/lldap/admin`, `operators/privacyidea/pi-admin`,
`operators/forgejo/state-hub-svc`. Distinct from `platform/workloads/…`, which
holds credentials that *workloads* consume; `operators/` holds credentials that
*people* use.
**The offline packet — what you need when OpenBao is unreachable.** This list is
deliberately short, closed, and enumerated here rather than left to judgement:
- `platform-root`'s own password and TOTP recovery codes
- OpenBao unseal shares and root token
- the `break-glass` account password (`sso-mfa/k8s/lldap/break-glass.sh`)
Anything on that list in `operators/` would be a circular dependency: you would
need the store to reach the credential that opens the store. Anything *not* on
that list belongs in `operators/`, not in the packet — the packet is not a
general-purpose safe and does not scale.
### The rule that closes the gap
**Every rotation procedure must name, as a numbered step, where the outgoing and
incoming values go.** A rotation that changes a credential without saying what
happens to custody will lose one of the two, and which one it loses is a matter
of luck. `NK-WP-0033-T04` rotated four credential classes and had no such step;
that is the root cause of the session that followed, not any of the four
software defects it also surfaced.
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
section describes intent rather than practice, and the same failure remains
available. Building it is a `railiance-platform` act, not an ops-mason one.
## Trust Progression
The platform moves through explicit trust stages:
| Stage | Name | Meaning |
| --- | --- | --- |
| S0 | MVP/prototype | Day-to-day accounts and local operator access may have created state. Nothing here is assumed clean enough for live custody. |
| S1 | Low-trust assembly | Setup operator can deploy infrastructure, but does not become platform root. No live secrets are stored. |
| S2 | King credential creation | Dedicated king credential, second factor, and offline recovery storage are created through guided UX. |
| S3 | OpenBao bootstrap | OpenBao is initialized, configured, and moved to non-root admin paths. |
| S4 | Cleanup and hardening | Bootstrap passwords, database credentials, tokens, and access paths are reset or rotated; hosts and workloads are scanned and reviewed. |
| S5 | Reopen under custody | The platform becomes usable under king credential oversight, with day-to-day admins delegated scoped access. |
| S6 | Multi-custodian upgrade | Custody moves to two-of-three or equivalent independent recovery control. |
This prevents early bootstrap convenience from silently becoming permanent
platform sovereignty.
### Secret disclosure by stage
*Decided 2026-08-27, during `NK-WP-0033`.* Diagnosis stalls when two people
cannot establish whether they are discussing the same credential. What follows
is what may be said aloud — in a terminal, a transcript, a chat message, or a
ticket — about a secret's *value*, by stage.
| Stage | Permitted about a value |
| --- | --- |
| 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. |
**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
not otherwise entitled to read.
## Required Bootstrap Use Cases
The guided bootstrap experience must cover at least:
- create or import the king credential;
- verify king credential second factor and recovery storage;
- initialize OpenBao without exposing secret output to unsafe channels;
- onboard a new user;
- temporarily lock a user;
- permanently lock and offboard a user;
- review and rotate user credentials;
- create a new fabric with its own admin;
- transfer a fabric to a new admin;
- perform break-glass access;
- rotate or rekey platform-root material;
- add later two-of-three custody; and
- run cleanup checks before reopening the platform for live use.
## Target IAM Claims
When key-cape or Keycloak provisions the first king/admin identity, it should
receive a platform-root identity envelope similar to:
| Claim or group | Value |
| --- | --- |
| `sub` | stable subject for `platform-root` or the selected king identity |
| `email` | notification address only; no secret transfer |
| `tenant` | `platform` |
| `principal_type` | `human` or `break_glass` depending on IAM support |
| `groups` | `platform-root`, `platform-admin`, `netkingdom-admin`, `railiance-platform-admin` |
| `roles` | `platform-root-custodian`, `openbao-admin`, `identity-admin` |
| `assurance` | MFA-backed for privileged actions |
The setup operator `tegwick` can receive scoped admin roles for day-to-day
operations later, but those roles must be delegated from the king custody model
and must be revocable without losing recovery authority.
Tenant administration must remain separate from platform-root claims. Being an
admin for `tenant:coulomb` or another tenant must not imply OpenBao root,
NetKingdom identity-admin, flex-auth platform-policy, or Railiance
platform-service authority.
## OpenBao Bootstrap Sequence
The Railiance OpenBao runbook owns the live commands. NetKingdom owns the
identity and custody semantics.
Before OpenBao initialization:
1. Use the guided bootstrap UX or checklist to decide the current trust stage.
2. Record `tegwick` as setup operator/contact, not as final root custodian.
3. Create or import the dedicated king credential and verify its second factor.
4. Choose whether this is temporary single-custodian king custody or preferred
independent escrow.
5. Prepare offline recovery bundle locations for that strategy.
6. Prepare the OpenBao custody packet for that strategy, including share
assignment rows, quorum plan, root-token disposition, and signoff line.
7. Approve the selected custody strategy in the NetKingdom control surface.
8. Run Railiance `make openbao-status` and `make openbao-verify`.
During initialization:
1. Run the OpenBao init command only in the approved maintenance window.
2. Route each unseal share directly to the king credential custody bundle or
approved escrow holder.
3. Use the initial root token only for audit, mounts, auth methods, policies,
and creating the non-root admin path.
4. Do not persist secret output in shell history, Git, State Hub, chat, email,
tickets, screenshots, or issue trackers.
After initialization:
1. Create and store a non-root `platform-admin` operator credential.
2. Revoke the initial root token, or seal it offline as break-glass material
under king custody.
3. Reset or rotate bootstrap-era database credentials, admin passwords,
service tokens, and access paths before live use.
4. Run host/workload checks, vulnerability scans where available, and the
OpenBao snapshot plus isolated restore drill.
5. Log non-secret progress with the custody posture and verification outcome.
## Completion Gates
The platform-root custody path is ready for live secrets only when:
- `tegwick` is recorded as setup operator/contact, not root of trust;
- the dedicated king credential exists and is second-factor protected;
- OpenBao init/unseal ceremony is complete;
- routine admin access no longer depends on the initial root token;
- root token disposition is recorded without storing the token value;
- bootstrap-era credentials and databases have been reset or rotated as needed;
- backup, audit, restore, and scan evidence exists;
- NetKingdom IAM claims are ready to become the normal human admin path; and
- two-of-three custody remains an explicit, low-friction upgrade path.