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
357 lines
18 KiB
Markdown
357 lines
18 KiB
Markdown
# 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 |
|
||
| --- | --- |
|
||
| 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. |
|
||
|
||
**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.
|