net-kingdom/workplans/NK-WP-0056-bounded-executor-path.md
tegwick aedf43860e
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0056-T09: route the human gate (decision 91323969)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 994874@bnt-lap001
Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87
2026-10-31 12:44:24 +01:00

388 lines
23 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-31"
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: done
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.
**2026-10-31, founder decision: split into T05 (runner), T08 (synthetic stack on real pinned images),
T09 (synthetic user and seed in OpenBao) and T10 (unattended live run).** T05 is now the runner only:
a stdlib journey that drives KeyCape `/authorize` (PKCE, `acr_values=urn:netkingdom:aal2`), Authelia
`/api/firstfactor` and its OIDC authorization, the KeyCape callback and OTP form (KeyCape validates the
code through privacyIDEA), and the KeyCape token exchange, then verifies the ID token's signature and
`assurance` (aal2, `otp`). Secrets come from files or descriptors, never argv; the receipt is value-free
and names the first failed step. All three consumer binds are on the path (`svc-authelia` at first
factor, `svc-keycape` at the callback, `svc-privacyidea` at the OTP check); `svc-provisioner` is not.
**Done when:** the runner and its tests (in-process fakes per bind, fail-closed at the named step, no
secret in the receipt) pass in the identity-provider journey acceptance workflow.
**2026-10-31, implemented:** `sso-mfa/k8s/synthetic-journey/journey.py` (stdlib; RS256 verified against
KeyCape's JWKS without third-party packages; `host_map` for an in-cluster executor) with 26 tests in
`test_journey.py`. Each broken bind fails closed at its step (`svc-authelia` → `authelia_first_factor`,
`svc-keycape` → `keycape_callback`, `svc-privacyidea` → `keycape_otp`), and a sign-in without an OTP
challenge, an aal1 token or a tampered signature fails. Green locally and in the workflow's pinned
`python:3.12-slim` image with the network off; the job is added to `identity-journeys.yaml`.
**Done:** the workflow's `provider` job passed on `0b0c6c9` (run 719). Next: T08.
## Synthetic identity stack on pinned images
```task
id: NK-WP-0056-T08
status: done
priority: medium
assignee: claude-code
depends_on: [NK-WP-0056-T05]
state_hub_task_id: "02611880-9ea4-5c9d-a5ab-b50847608f92"
```
A throwaway stack on the pinned production images (LLDAP, Authelia, privacyIDEA, KeyCape), published on
127.0.0.1 only, with generated values and production-shaped binds (`svc-authelia`, `svc-keycape`,
`svc-privacyidea`), a synthetic user with a TOTP token, and the T05 runner against it. Extends the
`bind-split-rehearsal.py` pattern.
**Done when:** the journey passes on the healthy stack and fails closed, at the expected step, for a
deliberately broken bind of each of the three consumers, with value-free receipts.
**Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t08-synthetic-stack/README.md)):
`sso-mfa/k8s/synthetic-journey/stack.py` + `pi_fixture.py` on the four pinned images. Healthy passes
(aal2, `otp`, signature verified); `svc-authelia` fails closed at `authelia_first_factor`, `svc-keycape`
at `keycape_token` (KeyCape binds at token time, not at the callback: mapping corrected), and
`svc-privacyidea` at `keycape_otp`, **but only after privacyIDEA's resolver cache expired** (live
`CACHE_TIMEOUT` 120 s; the first journey after the break passed). Each scenario passes again after the
password is restored. Also: privacyIDEA refuses a reused TOTP time step, so the runner gained
`min_time_step`. Consequence for T06/T10: the between-consumers check for `svc-privacyidea` must wait out
or clear the resolver cache, and the attended `--rotate-bind` MFA proof has the same blind spot.
## Synthetic user and seed in OpenBao
```task
id: NK-WP-0056-T09
status: wait
priority: medium
assignee: claude-code
needs_human: true
decision_id: 91323969-43fa-460f-9e0a-2b8cd44bdc9a
intervention_note: "Founder runs the live setup (LLDAP user, privacyIDEA token, OpenBao KV) under a CCR."
depends_on: [NK-WP-0056-T08]
state_hub_task_id: "1f788455-f79e-5630-bede-ec3218f6969d"
```
An unprivileged live user (for example `syn-journey`, no groups), its own privacyIDEA TOTP token, and
password and seed held in OpenBao with coding agents fenced, a dedicated KeyCape public client for the
journey, prepared as `bind-lane.py`-style setup/verify helpers under a CCR. Founder-run.
**Done when:** setup and verify pass live and one founder-started journey run passes with a receipt.
**2026-10-31, prepared for founder decision** (`sso-mfa/k8s/synthetic-journey/T09-RUNBOOK.md`):
CCR-2026-0057 (railiance-platform, proposed; `attended-service-credential-delivery`, KV
`platform/workloads/net-kingdom/synthetic-journey/syn-journey` fields `PASSWORD`, `TOTP_SEED`, no workload
reader), SecurityCase `security-cases/nk-synthetic-journey.yaml` (public PKCE client, MFA pinned), and
`lane.py` setup/verify/teardown with 8 fake-backed tests. Identity route: attended owner trial by `admin`
(the route the NK-WP-0049 retirement names for test identities), not User Engine onboarding; name
`syn-journey` under a proposed `syn-<check>` convention (SI §5), declared `SERVICE`, status `planned`.
Open, recorded: KeyCape marks every token `principal_type: human`; KeyCape has no per-client group gate.
Waiting on: CCR approval, case acceptance, founder run of the runbook.
## Unattended live journey
```task
id: NK-WP-0056-T10
status: todo
priority: medium
assignee: claude-code
depends_on: [NK-WP-0056-T03, NK-WP-0056-T09]
state_hub_task_id: "a68252da-46f7-52eb-81d3-942ddd9ecb9b"
```
The executor identity (T03) reads the journey secrets from OpenBao itself and runs the journey as the
check between consumers; failure triggers the revert path.
**Done when:** the journey runs unattended against live with no person involved, and fails closed
against a deliberately broken bind on a synthetic directory (T08).
## 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, NK-WP-0056-T10]
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.