Founder-run under CCR-2026-0055: setup, verify and realm_bootstrap.py --rotate-bind --from-openbao with the admin MFA proof. The first apply's check got a code that was not admin's, so both MFA prompts now name the user. Custody re-sealed after setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 896183@bnt-lap001 Assistant-Session: 3a18c01a-3205-4d02-94db-9efff25649a2
295 lines
18 KiB
Markdown
295 lines
18 KiB
Markdown
---
|
|
id: NK-WP-0056
|
|
type: workplan
|
|
title: "Bounded executor path: founder admits, executors run"
|
|
domain: infotech
|
|
repo: net-kingdom
|
|
status: proposed
|
|
flavor: design
|
|
owner: claude
|
|
topic_slug: netkingdom
|
|
created: "2026-10-30"
|
|
updated: "2026-10-30"
|
|
related:
|
|
- NK-WP-0054
|
|
- NK-WP-0040
|
|
- NK-WP-0031
|
|
- NK-WP-0055
|
|
state_hub_workstream_id: "5bc74ef8-136e-5157-9a7f-da765dbfc977"
|
|
---
|
|
|
|
# NK-WP-0056 — Bounded executor path
|
|
|
|
Intake from the founder's direction on 2026-10-30, before the live LLDAP bind split: top-level
|
|
decisions and guardrails stay with the founder; execution below that is delegated to automation with
|
|
limited expense budgets and risk control, and the security architecture is the means. Founder-attended
|
|
runbooks are a transitional state.
|
|
|
|
SI-15 (`canon/standards/service-identity-privilege_v0.1.md`) already names the golden path *prepare →
|
|
admit → execute → verify → retain* and allows "a bounded executor under the admission" at step 3.
|
|
Today preparation and verification are strong, admission is prose (a hub decision) and execution falls
|
|
to the founder, because no executor exists that has its own identity, its own secret delivery and an
|
|
authority ceiling the founder can admit and bound. SI-14 adds that an interactive agent session holds
|
|
everything its tools reach, so it cannot be that executor until a session contract exists
|
|
(glas-harness GLAS-WP-0018, proposed).
|
|
|
|
## Why the bind split needed the founder (2026-10-30 run, step by step)
|
|
|
|
| Runbook step | Needed the founder because | Class | Delegable once |
|
|
| --- | --- | --- | --- |
|
|
| Setup: decrypt custody copy | custody is a personal SOPS/age copy | delegable | service credentials live in OpenBao (T04) |
|
|
| 1 prepare custody (generate passwords) | writes the personal custody copy | delegable | executor generates into OpenBao (T04) |
|
|
| 2 plan, 4 verify, 10 verify | sign in as the human `admin` | delegable | read-only executor directory identity (T03) |
|
|
| 3 create `svc-*` accounts | needs `lldap_admin` (LLDAP has no narrower grant that can create users) | root-class | admitted per run; executor runs it (T03) |
|
|
| 5-8 switch one consumer | cluster-admin kubeconfig; secret values; founder sign-in check | delegable | scoped service account (T03), OpenBao/ESO (T04), synthetic MFA journey (T05) |
|
|
| 9 change LLDAP's configured super-admin | changes who administers the directory | root-class | admitted per run; executor runs it |
|
|
| 10 change `admin`'s own password; seal `ld-admin` in the password safe | the founder's own credential and crisis custody | human-intrinsic | never: stays with the founder |
|
|
| Afterwards: restore drill on the next generation | `admin` operates drills | delegable | executor identity with the drill's read scope |
|
|
|
|
Root-class means the founder still admits each run, but no longer types it. The run also showed why
|
|
personal custody does not scale: the custody copy had drifted from live (`AUTHELIA_KEYCAPE_CLIENT_SECRET`)
|
|
and step 6 had to be done by a one-off path.
|
|
|
|
## Admission contract
|
|
|
|
```task
|
|
id: NK-WP-0056-T01
|
|
status: todo
|
|
priority: high
|
|
assignee: claude-code
|
|
state_hub_task_id: "99df0ee2-2c17-55f2-9f3e-4277da52e01c"
|
|
```
|
|
|
|
Draft a NetKingdom standard for a **bounded-execution admission**: the founder approves, at `aal2`, an
|
|
exact plan by digest with a validity window, a scope ceiling (`MODE @ SCOPE` grants, SI terms), a
|
|
blast-radius rule (for example one consumer at a time), a stop-and-revert policy tied to named machine
|
|
checks, a risk class, and an expense budget (wall time, attempts, compute/egress cost). The executor
|
|
consumes the admission once. Map it to SI-15, the Playbook Capability Contract's trust-state
|
|
requirements and the execution-attribution receipt (NK-WP-0040). Include a validator and synthetic
|
|
fixtures.
|
|
|
|
**Done when:** the standard is proposed with a schema, a validator, and the bind-split steps expressed
|
|
as admissions in its examples.
|
|
|
|
## Owner agreement on the components
|
|
|
|
```task
|
|
id: NK-WP-0056-T02
|
|
status: todo
|
|
priority: high
|
|
assignee: claude-code
|
|
needs_human: true
|
|
intervention_note: "Founder chooses the executor runtime owner; owner answers are agreements, not agent decisions."
|
|
depends_on: [NK-WP-0056-T01]
|
|
state_hub_task_id: "7263417b-b227-54f7-9edc-3e7f97cf132f"
|
|
```
|
|
|
|
Ask the owners which part each carries: approval-engine (the admission as its consumable approval
|
|
object, plan digest and budget as bound fields, single consume), access-engine (reads the admission
|
|
as an input claim), secrets-engine and railiance-platform (executor credential delivery through
|
|
OpenBao), glas-harness (GLAS-WP-0018 session contract, so a harness-launched agent can be an executor),
|
|
audit-core (receipt holder, as in NK-WP-0040). The founder decides who builds the executor runtime
|
|
itself.
|
|
|
|
**Done when:** each component has a named owner and an answer, and the founder has chosen the executor
|
|
runtime owner.
|
|
|
|
## Executor identity and authority ceiling
|
|
|
|
```task
|
|
id: NK-WP-0056-T03
|
|
status: todo
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T02]
|
|
state_hub_task_id: "592231dd-1981-57a6-a0e8-5d6fc474c6e7"
|
|
```
|
|
|
|
Declare the executor as a `WORKLOAD` (or harness-launched `AGENT`) principal in `declarations/` with
|
|
grants no wider than the pilot: a Kubernetes service account whose Role names the `sso` Secrets,
|
|
ConfigMaps and Deployments it may patch or restart; a read-only directory bind for plan and verify;
|
|
`lldap_admin` only for root-class steps under an admission. Extend `tools/service-identity` to check
|
|
it.
|
|
|
|
**Done when:** the declaration validates and the Role is prepared in source with tests, not applied.
|
|
|
|
## Service credentials into OpenBao
|
|
|
|
```task
|
|
id: NK-WP-0056-T04
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
needs_human: true
|
|
intervention_note: "Founder runs the custody reconciliation and approves moving service binds out of personal custody."
|
|
depends_on: [NK-WP-0056-T02]
|
|
state_hub_task_id: "fd439c0c-9b5e-562c-9bfb-48dd0fb2c570"
|
|
```
|
|
|
|
With railiance-platform, move the LLDAP service binds (`LLDAP_SVC_*_PASS`) and the Authelia secrets
|
|
from the personal custody copy into OpenBao paths delivered by ESO, so that neither a person nor an
|
|
agent handles the values in a rotation. First reconcile the stale custody
|
|
`AUTHELIA_KEYCAPE_CLIENT_SECRET` with the live value (founder-run, value-free comparison as in the
|
|
2026-10-30 evidence). Root-class material (`ld-admin`, unseal shares) stays in founder custody.
|
|
|
|
**Done when:** the four service binds are ESO-delivered from OpenBao, and the custody copy no longer
|
|
needs to be decrypted for a bind rotation.
|
|
|
|
**2026-10-30:** reconciliation tool prepared: `sso-mfa/bootstrap/reconcile-keycape-client-secret.py`
|
|
(value-free; accepts the live KeyCape value only if it matches Authelia's live hash, writes custody through
|
|
`rotation-env.py`, no cluster writes; `tests/test_reconcile_keycape_client_secret.py`). **Founder ran it
|
|
2026-10-30:** live KeyCape value matches Authelia's hash; custody did not match before, was updated and matches
|
|
after (value-free output); custody re-sealed and shredded. The reconciliation step is done; the ESO move remains.
|
|
|
|
**2026-10-30, pilot `svc-provisioner` prepared (source only, nothing applied):** railiance-platform
|
|
CCR-2026-0052 (proposed) and policy `net-kingdom-svc-provisioner-bind-eso`; `sso-mfa/k8s/identity-provisioner/bind-eso.yaml`
|
|
(ServiceAccount, SecretStore, ExternalSecret with `creationPolicy: Merge` into the existing Secret, `password` only);
|
|
`tools/service-binds/bind-lane.py setup|verify` (approved sha256-bound CCR, attended platform-admin, custody must equal
|
|
live, coding-agent fence, KV CAS=0, value-free receipts); `tests/test_service_bind_lane.py` (7). Next: owner approval of
|
|
the CCR, then the founder-run setup, ESO apply and verify. Authelia, KeyCape and privacyIDEA follow one at a time.
|
|
|
|
**2026-10-30, pilot live and verified:** CCR-2026-0052 approved; founder-run setup, ESO apply and verify passed;
|
|
`svc-provisioner` now binds with the ESO-delivered value ([evidence](../docs/evidence/2026-10-30-nk56-svc-provisioner-bind/README.md)).
|
|
Finding: ESO targets need `template.metadata: {}` or the admission guard refuses the Secret update. Remaining:
|
|
`svc-authelia`, `svc-keycape`, `svc-privacyidea`.
|
|
|
|
**2026-10-31, `svc-authelia` lane prepared (source only, nothing applied):** railiance-platform CCR-2026-0053
|
|
(proposed) and policy `net-kingdom-svc-authelia-bind-eso`; `sso-mfa/k8s/authelia/bind-eso.yaml` (Merge of
|
|
`ldap_password` only into `authelia-secrets`, which also holds the session, storage and OIDC keys). `bind-lane.py`
|
|
gains per-lane Secret keys: Authelia keeps the bind user in `authelia-config`, so setup refuses unless the live
|
|
configuration binds `uid=svc-authelia`, and it decodes only the bind key. Authelia reads the file at start-up
|
|
only, so the identical projection needs no restart; a later rotation does. 12 lane tests. Next: owner approval
|
|
and binding confirmation, then the founder-run setup, ESO apply, verify and an Authelia restart with a login.
|
|
|
|
**2026-10-31, `svc-authelia` live and verified:** CCR-2026-0053 approved and binding confirmed; founder-run setup,
|
|
ESO apply (Ready first time), verify and Authelia restart passed, and an `admin` portal sign-in with OTP worked
|
|
([evidence](../docs/evidence/2026-10-31-nk56-svc-authelia-bind/README.md)). Two of four binds are ESO-delivered.
|
|
Remaining: `svc-keycape` (bind inside `sso/keycape-config`), `svc-privacyidea` (resolver password stored in the
|
|
privacyIDEA database, not a Secret key).
|
|
|
|
**2026-10-31, `svc-keycape` design proposal (for founder decision):** KeyCape's server reads `lldap.bindPW` only
|
|
from `config.yaml` inside `sso/keycape-config` (`internal/config/config.go`); unlike client secrets (`secretRef:
|
|
env:...` via `secretKeyRef`) it has no environment indirection. ESO Merge cannot own one field inside a YAML
|
|
document, and rendering all of `config.yaml` from OpenBao would move every KeyCape value at once. Proposed:
|
|
1. key-cape (owner change, new KEY-WP): `config.Load` takes `KEYCAPE_LLDAP_BIND_PW` (the name `lldap-export`
|
|
already uses). Env set and `bindPW` empty: use env. Both set and equal: use it and log that `bindPW` is
|
|
redundant. Both set and different: refuse to start. This allows a staged switch with a clean rollback.
|
|
Image through the owner pipeline; `check-config` reports which source is used, without the value.
|
|
2. NetKingdom: CCR-2026-0054 and a `svc-keycape` lane; ESO creates a new dedicated Secret `keycape-lldap-bind`
|
|
(`creationPolicy: Owner`, key `password`), so no Secret has two writers. Setup compares custody with the live
|
|
`bindPW` in `keycape-config`.
|
|
3. Deployment: add the env from `secretKeyRef keycape-lldap-bind/password` with the new image; values are
|
|
equal, so behaviour is unchanged. Verify, restart, admin login.
|
|
4. Remove `bindPW` from `keycape-config` (`directory-bind-rollout.py --operation clear-password`, guarded on
|
|
uid/resourceVersion); `create-secrets.py` stops writing it. Rollback at any step: re-add the value or drop
|
|
|
|
**2026-10-31, founder decision: environment override, implemented by claude-code in key-cape.** Done in source:
|
|
key-cape `e2b5fa1` (KEY-WP-0046-T01; image `main-e2b5fa1` = `sha256:9e082dca…6f8d`, built by the Forgejo workflow;
|
|
its acceptance job also fails on the previous commit `a1ab8ff`). NetKingdom: CCR-2026-0054 (proposed),
|
|
`sso-mfa/k8s/keycape/bind-eso.yaml` (dedicated Secret `keycape-lldap-bind`, `creationPolicy: Owner`),
|
|
`bind-lane.py svc-keycape` (setup reads `bindPW` from `keycape-config` and requires `bindDN` = `svc-keycape`;
|
|
verify reads the dedicated Secret and requires it to equal `bindPW` while present), `directory-bind-rollout.py
|
|
--operation clear-password` (only with the service bind and the delivered env; `switch` restores), and
|
|
`deployment.yaml` pinned to the new image with the `KEYCAPE_LLDAP_BIND_PW` env. The new image also carries
|
|
key-cape 67a0869..e2c5a94 (online token status, inactive while `tokenStatus` is unset) and identity-facts.
|
|
Founder order: approve, setup, ESO apply, verify, deployment apply (image + env), login, clear-password, restart, login.
|
|
|
|
**2026-10-31, `svc-keycape` live and verified:** CCR-2026-0054 approved and binding confirmed; founder-run setup, ESO
|
|
apply, verify, deployment rollout (image `main-e2b5fa1` + env; both sources equal), `admin` login, `clear-password`,
|
|
restart, env-only verify and a second `admin` login all passed
|
|
([evidence](../docs/evidence/2026-10-31-nk56-svc-keycape-bind/README.md)). Three of four binds are ESO-delivered;
|
|
`keycape-config` no longer holds the bind password. Remaining: `svc-privacyidea` (resolver password stored in the
|
|
privacyIDEA database, not a Secret key: needs its own design).
|
|
|
|
**2026-10-31, `svc-privacyidea` design proposal (for founder decision):** privacyIDEA keeps the resolver `BINDPW`
|
|
encrypted in its own database, and only the `pi-admin` API can change it (`realm_bootstrap.py`), so neither ESO nor
|
|
an unattended job can deliver it without `pi-admin`'s password standing in the cluster (rejected: SI rules).
|
|
Proposed: **OpenBao custody with attended write-through**, following CCR-2026-0028 (`attended-host-key-delivery`):
|
|
1. bind-lane `svc-privacyidea setup`: the custody value must sign in to LLDAP as `svc-privacyidea` (LLDAP is the
|
|
authority for the current password; the working resolver proves privacyIDEA holds the same one), then KV
|
|
`directory-binds/svc-privacyidea` CAS=0, coding agents fenced. No Kubernetes role, no ESO, `runtime_access: false`.
|
|
2. `realm_bootstrap.py --switch-bind --from-openbao`: reads the KV value in memory under the attended
|
|
`platform-admin` session and writes only `BINDPW` with the attended `pi-admin`; the existing preservation checks
|
|
apply, and the write must be proven by an MFA login through the resolver (`--verify-user`).
|
|
3. Rotation becomes: change the password in LLDAP, write the next KV version, run step 2, verify with an OTP login.
|
|
No custody decryption; the founder runs it and proves it with an MFA login.
|
|
4. CCR-2026-0055 needs a request type for attended delivery into a service; railiance-platform's
|
|
`credential-change.py` gains it, modelled on `attended-host-key-delivery`.
|
|
The done criterion of T04 then reads: three binds ESO-delivered, `svc-privacyidea` held in OpenBao with attended
|
|
write-through. Alternative: keep it in founder custody as a recorded exception until privacyIDEA can read the
|
|
resolver password from a file or environment.
|
|
|
|
**2026-10-31, founder decision: OpenBao custody with attended write-through.** Prepared in source:
|
|
railiance-platform `2d4a383` adds request type `attended-service-credential-delivery` (`scripts/attended_service_delivery.py`,
|
|
4 tests; no policy, auth role, ESO or runtime frontdoor allowed) and CCR-2026-0055 (proposed). `bind-lane.py
|
|
svc-privacyidea`: setup proves the custody value current by an LLDAP sign-in as `svc-privacyidea`, fences coding
|
|
agents and writes KV v1 (CAS=0); verify proves the KV value current the same way, the fence present and no workload
|
|
role. `realm_bootstrap.py --rotate-bind --from-openbao` rewrites only `BINDPW` on the service bind from OpenBao
|
|
(attended platform-admin) and requires `--verify-user`. Correction: `pi-admin` signs in with a password; the MFA
|
|
proof is the `--verify-user` login through the resolver.
|
|
|
|
**Done 2026-10-31.** `svc-privacyidea` live: CCR-2026-0055 approved; founder-run setup and verify passed (KV v1, value
|
|
proven current by LLDAP sign-in, no workload role) and `realm_bootstrap.py --rotate-bind --from-openbao` wrote the
|
|
resolver from OpenBao with an `admin` MFA proof ([evidence](../docs/evidence/2026-10-31-nk56-svc-privacyidea-bind/README.md);
|
|
a first apply wrote the same value but its check got a code that was not `admin`'s; both MFA prompts now name the user).
|
|
All four service binds are held in OpenBao: `svc-provisioner`, `svc-authelia` and `svc-keycape` ESO-delivered,
|
|
`svc-privacyidea` by attended write-through (founder decision 2026-10-31 amends the done criterion accordingly).
|
|
No bind rotation needs the custody copy decrypted. The custody copy keeps the current values as a fallback.
|
|
the env.
|
|
|
|
## Synthetic verification instead of a founder sign-in
|
|
|
|
```task
|
|
id: NK-WP-0056-T05
|
|
status: todo
|
|
priority: medium
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T04]
|
|
state_hub_task_id: "bb511d1e-6d5d-56d1-9030-2e0d94006a16"
|
|
```
|
|
|
|
A synthetic test user with an OTP seed held in OpenBao, and a journey that signs in through Authelia
|
|
and KeyCape with MFA and resolves through privacyIDEA. Reuse the identity-provider journey acceptance.
|
|
The executor runs it as the check between consumers and reverts on failure.
|
|
|
|
**Done when:** the journey runs unattended, and fails closed against a deliberately broken bind on a
|
|
synthetic directory.
|
|
|
|
## Pilot: unattended bind rotation under one admission
|
|
|
|
```task
|
|
id: NK-WP-0056-T06
|
|
status: todo
|
|
priority: high
|
|
assignee: claude-code
|
|
needs_human: true
|
|
intervention_note: "Founder signs the admission for the single live pilot rotation."
|
|
depends_on: [NK-WP-0056-T01, NK-WP-0056-T03, NK-WP-0056-T04, NK-WP-0056-T05]
|
|
state_hub_task_id: "6c3939f8-23a9-5f70-a3b1-94f4b291706b"
|
|
```
|
|
|
|
Rotate `svc-authelia`'s password with no person in the loop beyond the admission: generate into
|
|
OpenBao, set in LLDAP, let ESO deliver, restart, run the synthetic journey, revert on failure, emit a
|
|
receipt to audit-core. Rehearse on a synthetic directory first, then one live run under a
|
|
founder-signed admission.
|
|
|
|
**Done when:** the live rotation completes or reverts on its own, within its budget, with a verified
|
|
receipt, and the founder's only action was the admission.
|
|
|
|
## Budget and risk enforcement
|
|
|
|
```task
|
|
id: NK-WP-0056-T07
|
|
status: todo
|
|
priority: medium
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T06]
|
|
state_hub_task_id: "a87c5edb-7604-5eba-9261-b1ed2ed16655"
|
|
```
|
|
|
|
Enforce the admission's budget and blast radius at run time (attempt and time limits, a kill switch the
|
|
founder holds, refusal when a check is missing), and feed outcomes and drift into posture feedback
|
|
(NK-WP-0031) so that recurring admissions can be reviewed in aggregate rather than one by one.
|
|
|
|
**Done when:** a run that exceeds its budget or scope stops and reverts in a synthetic test, and the
|
|
pilot's receipts appear in a posture-feedback report.
|