All checks were successful
admission_memo composes the present_existing envelope (admission YAML as packet, three required highlights, intent = binding without actor, key nk-bind-executor.<approval_id>); InformedDecision.submit posts it with the projected intake token and checks the receipt. Exit 7 when the memo is refused (approval stays usable). Envelope validated by informed-decision's own validate_submission with the present_existing profile. Service-identity declaration notes key-cape's prepared approval:read. 61 executor tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
1048 lines
56 KiB
Markdown
1048 lines
56 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: done
|
|
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.
|
|
|
|
**Done 2026-10-31.** Proposed `canon/standards/bounded-execution-admission_v0.1.md` with
|
|
`canon/schemas/bounded-execution-admission_v0.1.schema.json`, `tools/bounded-execution-admission/validate.py`
|
|
(rules BEA-1..14 on the request, BEA-C1..C7 on a carrier record; 22 tests) and four examples in
|
|
`examples/bounded-execution-admission/`: the bind split's read-only verify (low), create `svc-*` accounts and switch
|
|
LLDAP's super-admin (root-class, high), and one-consumer bind rotation (delegable, medium, the T06 shape). Step 10 is
|
|
refused as human-intrinsic. Design choices: the request is immutable and bound by digest; the founder's approval
|
|
lives in a carrier (Approval Engine preferred, founder SSH signature transitional), so no second approval system;
|
|
`trust_requires` uses the Playbook Capability Contract's trust states; `settle_seconds` carries the T08
|
|
privacyIDEA-cache finding; recurring admissions are excluded from v0.1. The Approval Engine mapping is a proposal for T02.
|
|
|
|
## Owner agreement on the components
|
|
|
|
```task
|
|
id: NK-WP-0056-T02
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
needs_human: true
|
|
decision_id: 6d1adce9-40e2-472a-92bb-0ae942218ae8
|
|
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.
|
|
|
|
**2026-10-31, founder decision (hub `6d1adce9`): net-kingdom writes the executor runtime** (source and tests next
|
|
to the admission validator, bind lanes and synthetic journey); a new `rapp-*` package (working name
|
|
`rapp-bounded-executor`, ADR-0015) builds and deploys it in `sso`; glas-harness is the later path for agent
|
|
executors. Owner requests sent the same day through the hub to approval-engine, access-engine, secrets-engine,
|
|
railiance-platform, audit-core, glas-harness and railiance-apps
|
|
([requests and answer table](../docs/proposals/2026-10-31-nk56-t02-owner-requests.md)). Waiting on their answers.
|
|
|
|
**Done 2026-10-31.** All seven owners answered (worker agents in each repository, founder instruction); answers and
|
|
the collected founder decisions are in the [answer table](../docs/proposals/2026-10-31-nk56-t02-owner-requests.md).
|
|
Key results: approval-engine already carries binding, single consume and revoke (aal2 recorded, not yet enforced);
|
|
access-engine accepts the pilot without the PDP gate if the receipt says `not-evaluated`; railiance-platform added
|
|
request type `workload-kv-write-next-version` and drafted CCR-2026-0058; secrets-engine prefers self-rotation, which
|
|
NetKingdom proved on the pinned LLDAP; audit-core holds receipts plus a `run.started` event; railiance-apps: repo-manager
|
|
creates the rapp, the image is built in net-kingdom, one Job per admission; glas-harness: agent executors after
|
|
GLAS-WP-0018-T04.
|
|
|
|
**2026-10-31, founder decisions after the answers** (hub c29c6eda, 3d0c8f86, 19afd15e):
|
|
|
|
- The executor's OpenBao write role is window-scoped. CCR-2026-0058 was amended, approved and pushed.
|
|
- The bind rotates its own password, so `svc-executor-pw` is dropped and RPF-WP-0069-T06 is cancelled.
|
|
- The pilot runs without the policy gate (`not-evaluated`); later admissions need the gate or a recorded exception.
|
|
- Packaging is `rapp-bounded-executor`, one Job per admission in `sso`, with the image built in net-kingdom.
|
|
- Receipts are a started event plus one receipt per run.
|
|
- These owner workplans are approved: APPROVAL-WP-0007, FLEX-WP-0040, SECRETS-WP-0017, RPF-WP-0069, AUDIT-WP-0018 and
|
|
GLAS-WP-0018. glas-harness owns agent executors later.
|
|
|
|
Runtime work is split into T11 (core runner) and T12 (pilot operations and packaging).
|
|
|
|
## Executor identity and authority ceiling
|
|
|
|
```task
|
|
id: NK-WP-0056-T03
|
|
status: done
|
|
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.
|
|
|
|
**Done 2026-10-31.** `tools/service-identity/declarations/bounded-executor.yaml` declares the executor's credentials
|
|
(all `planned`, all sharing `executor_subject: service:nk-bind-executor`): the Kubernetes ServiceAccount, its OpenBao
|
|
role, `svc-executor-pw` (`lldap_password_manager`: LLDAP cannot narrow to one account, the admission narrows it),
|
|
the read-only bind `svc-executor-read`, and `svc-executor-admin` (`lldap_admin`, root-class only, per-run activation
|
|
open). `tools/service-identity` gained convention `executor` with rule SI-15 (no person, not interactive, every grant
|
|
`APPROVED`, no `SOVEREIGN`/`GOVERN`/`CONSTRUCT`) and `--admissions`, which checks that an admission's ceiling lies inside
|
|
the executor's declared grants: the pilot (BEA-2026-0001) is covered; the root-class and read-only examples are
|
|
correctly reported as needing grants the pilot does not hold. `sso-mfa/k8s/bounded-executor/rbac.yaml` (not applied):
|
|
ServiceAccount without automount, a Role with `get/watch/patch` on the authelia Deployment and `get/patch` on the
|
|
authelia ExternalSecret only (no read of credential objects: the pilot's delivery check became "ExternalSecret Ready
|
|
and refreshed"), and its RoleBinding; `test_rbac.py` ties Role = declaration = pilot ceiling. 44 identity tests,
|
|
5 RBAC tests.
|
|
|
|
## 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: done
|
|
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.
|
|
|
|
**Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t09-synthetic-identity/README.md)): CCR-2026-0057
|
|
approved; case accepted; client registered; founder-run `lane.py setup` and `verify` passed. The live journey
|
|
reached aal2 (`pwd`+`otp`, signature verified) against `kc.coulomb.social`. Two lane fixes on the way: privacyIDEA
|
|
tokens are looked up by serial, and enrollment uses a privacyIDEA-generated key, because `pi-admin`'s `action=*`
|
|
enables `totp_force_server_generate`, which silently replaced the supplied key (reproduced on 3.12.2). One
|
|
partial run was removed with `lane.py teardown`. Next: T10 needs the executor identity (T03) and a
|
|
`workload-kv-read` request for it.
|
|
|
|
## Unattended live journey
|
|
|
|
```task
|
|
id: NK-WP-0056-T10
|
|
status: done
|
|
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).
|
|
|
|
**2026-10-31, prerequisites:**
|
|
|
|
- Done: the `sso` ingress rules for executor pods were applied by the founder and read back live.
|
|
- Done: railiance-platform CCR-2026-0059 drafted (`c9b68ea`, `proposed`). It is the journey read with its own role,
|
|
`net-kingdom-bind-executor-syn-journey-read`. It widens CCR-2026-0057; standing reader (founder decision).
|
|
- Two executor gaps found while drafting it:
|
|
1. **Two roles.** The executor logs in to OpenBao once (`openbao.k8s_role`) and reads the bind and the journey with
|
|
that token. railiance-platform keeps one role per lane and never combines policies, so the executor needs a
|
|
second login for the journey read: a `journey.k8s_role` and its own `OpenBao` client, revoked like the first.
|
|
2. **Audience.** CCR-2026-0058 and CCR-2026-0059 bind audience `openbao`. The rapp Job projects only the default
|
|
API-audience token, so it also needs an `openbao`-audience projected token, and `openbao.jwt_file` must point
|
|
to it.
|
|
- Both need an executor change, a new image and a new pin.
|
|
|
|
**2026-10-31, gaps fixed** ([evidence](../docs/evidence/2026-10-31-nk56-t10-openbao-lanes/README.md)):
|
|
|
|
- `1cdc18f`: one OpenBao client per lane, and one role for both lanes is refused.
|
|
- Every login is revoked at exit, also when setup fails.
|
|
- A third gap was found and fixed: the 15m token is shorter than the 20m budget, so clients now log in again before
|
|
the lease ends, re-reading the rotating JWT.
|
|
- Synthetic pilot with per-lane tokens: completed and reverted, `all_met`. Examples re-pinned.
|
|
- rapp `368fee4`:
|
|
- a second projected token with audience `openbao`;
|
|
- the config names both CCR roles;
|
|
- the renderer refuses other roles and the API token;
|
|
- tests tie the roles to the platform CCRs.
|
|
- The founder chose a standing journey reader. CCR-2026-0059 records it (railiance-platform `98a8b08`).
|
|
|
|
Done since:
|
|
|
|
- The image for `4815c58` is `sha256:716dccef6707deb570e19cc2b3edfda4298f9bce580685896bc9320f186bf6b3`.
|
|
- The identity-journeys executor tests passed in CI.
|
|
- The executed files in the image are byte-identical to the repo and to the admission pins.
|
|
- It is pinned in the rapp (`c107afc`, pushed).
|
|
- CCR-2026-0059 was approved by the founder as platform-owner and net-kingdom-owner (railiance-platform `831a0f9`).
|
|
|
|
**2026-10-31, lanes live** ([evidence](../docs/evidence/2026-10-31-nk56-t10-executor-lanes/README.md)), founder-run
|
|
with `T10-LANES-RUNBOOK.md`:
|
|
|
|
- The rapp's standing objects are deployed. `sso/nk-bind-executor` has uid `bd15569c…`, and the Role matches the
|
|
ceiling.
|
|
- The bindings of CCR-2026-0058 and CCR-2026-0059 are confirmed (railiance-platform `b4bafb0`).
|
|
- `executor-lanes.py setup`:
|
|
- both policies applied;
|
|
- `cas_required` set;
|
|
- the standing journey-read role created;
|
|
- the write role absent;
|
|
- the agent fence holds.
|
|
- The first setup stopped because platform-admin has no `patch` on `platform/*`. It now uses a metadata write that
|
|
names only `cas_required`, with a before/after guard (`12c9b2d`).
|
|
- `verify`: the journey token reads only its path, with a lease of at most 15m. A wrong ServiceAccount, a wrong
|
|
audience and a login to the write role are all denied.
|
|
|
|
**2026-10-31, journey-only mode** ([evidence](../docs/evidence/2026-10-31-nk56-t10-journey-only/README.md)):
|
|
|
|
- `cli.py --journey-only` is read-only and needs no admission:
|
|
- it logs in with the journey-read role only;
|
|
- it builds no Kubernetes or directory client and makes no approval or audit call;
|
|
- exit codes: `0` pass, `1` fail closed, `4` refused.
|
|
- The admission check and this mode share `operations.journey_from_kv`. 45 executor tests.
|
|
- Synthetic proof on the T08 stack with a journey-only OpenBao token:
|
|
- healthy: pass;
|
|
- broken svc-authelia bind: fail closed at `authelia_first_factor`;
|
|
- restored: pass;
|
|
- the journey token cannot read the bind;
|
|
- `all_met`, no values.
|
|
|
|
This is T10's fail-closed proof.
|
|
- rapp: `templates/journey-job.yaml` and `render_job.py --journey-only`:
|
|
- the config holds only `openbao` (no role) and `journey`;
|
|
- the Job mounts only the openbao-audience token;
|
|
- founder-run with `make journey`.
|
|
|
|
The image for `9a81123` is `sha256:550bb9d0db89716835b501b4bc822b9588aa63ea15d0e59c61f6a1768b3f16ee`. Its executed
|
|
files are byte-identical to the repo, and it is pinned in the rapp (`9cb19d9`).
|
|
|
|
The first live `make journey` was refused at setup (`URLError`). `make netcheck` showed that k3s applies
|
|
NetworkPolicy to a new pod about 3 s late. The executor now waits up to 30 s for OpenBao's `sys/health` before its
|
|
first login (`15c9cdb`). The image `sha256:71da4a51e331cb82a5bde1e44025dc3b1b7ea4019dd9dbd64e7196cd3dd21471` is
|
|
verified and pinned in the rapp.
|
|
|
|
**Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t10-journey-only/README.md)):
|
|
|
|
- The founder started `make journey`, which created Job `sso/bounded-executor-journey-20261031-194719`.
|
|
- It ran unattended against live with no person in the journey and passed all eight steps to an `aal2` ID token
|
|
(`pwd` + `otp`), with the signature verified. `effects: false`. The runner's hash equals the repo.
|
|
- The fail-closed half is the synthetic proof: a broken svc-authelia bind fails at `authelia_first_factor`.
|
|
- CCR-2026-0059 is now `verified`.
|
|
|
|
For T06, separately: the approval-engine and audit-core replies, and the token Secret.
|
|
|
|
## Executor runtime: core runner
|
|
|
|
```task
|
|
id: NK-WP-0056-T11
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T01, NK-WP-0056-T03]
|
|
state_hub_task_id: "24121a14-92d8-577c-a651-992afcbdb4cc"
|
|
```
|
|
|
|
The executor itself, in net-kingdom (founder decision 6d1adce9):
|
|
|
|
- consume one admission through the approval-engine carrier (independent GET, digest check, aal2 check, consume once);
|
|
- refuse outside the window or ceiling;
|
|
- run the pinned steps one consumer at a time, with checks after each mutating step, settle times, revert on
|
|
failure, the budget (wall time, attempts) and an `on_unknown: stop`;
|
|
- emit `execution.run.started` and the receipt (PDP `not-evaluated` for the pilot).
|
|
|
|
Tested against in-process fakes for the carrier, operations and audit sink.
|
|
|
|
**Done when:** the runner passes its tests, including revert on a failed check, budget exhaustion, a stale or
|
|
reused approval, and a step outside the ceiling. Every refusal must happen before any effect.
|
|
|
|
**Done 2026-10-31.** `bounded-executor/executor.py` with 23 tests (`bounded-executor/tests/test_executor.py`; fakes
|
|
for carrier, operations, checks, trust, audit, clocks), modelled on approval-engine's real object and consume
|
|
response. Refusals before any effect cover:
|
|
|
|
- an invalid admission;
|
|
- another executor;
|
|
- source drift;
|
|
- unsatisfied trust;
|
|
- an unknown operation;
|
|
- outside the window, or a window shorter than the budget;
|
|
- a binding digest or actor mismatch;
|
|
- a non-human, aal1 or unlisted approver;
|
|
- an already-consumed approval.
|
|
|
|
Consume:
|
|
|
|
- an unclear consume is read back and never repeated;
|
|
- an idempotent response is refused;
|
|
- with no audit custody, no run.
|
|
|
|
Failure handling:
|
|
|
|
- a failed check reverts in reverse order, each revert proving the restored state with its own checks;
|
|
- a clean revert retries within `attempts`;
|
|
- a failed revert, or a revert check that fails, stops and alerts;
|
|
- an unknown outcome stops without reverting;
|
|
- budget exhaustion and the kill switch revert;
|
|
- settle times are waited and counted;
|
|
- a receipt-custody failure is reported.
|
|
|
|
Receipts carry no operation values.
|
|
|
|
Standard and validator change: BEA-9 now applies to revert steps, which name their own checks; examples regenerated.
|
|
|
|
## Executor runtime: pilot operations and packaging
|
|
|
|
```task
|
|
id: NK-WP-0056-T12
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T11, NK-WP-0056-T08]
|
|
state_hub_task_id: "815cc63a-a2a0-5b8f-83ae-2b80e37ea8f5"
|
|
```
|
|
|
|
The pilot's operations:
|
|
|
|
- OpenBao read and CAS write of the next version;
|
|
- LLDAP self-rotation of the bind;
|
|
- ExternalSecret force-sync and its status check;
|
|
- Deployment restart and rollout check;
|
|
- the synthetic journey.
|
|
|
|
Run them on the T08 synthetic stack, including a revert. Add the Containerfile and image workflow in
|
|
net-kingdom. The `rapp-bounded-executor` package is created with `rmgr rapp wrap`, and the founder creates the
|
|
Forgejo remote. It runs one Job per admission in `sso` with an egress-only NetworkPolicy.
|
|
|
|
**Done when:** a synthetic rotation completes and a deliberately failing one reverts, with receipts.
|
|
|
|
**Done 2026-10-31** ([evidence](../docs/evidence/2026-10-31-nk56-t12-synthetic-pilot/README.md)).
|
|
`bounded-executor/operations.py`:
|
|
|
|
- the OpenBao KV v2 CAS client;
|
|
- LLDAP self-rotation, the same operation forward and in revert;
|
|
- the Kubernetes ExternalSecret force-sync and status check, and the rollout restart that raises `RolloutFailed`;
|
|
- the pilot checks.
|
|
|
|
`bounded-executor/synthetic_pilot.py`: rotation `completed`, and a one-off wrong delivery `reverted` in declared order
|
|
with the earlier value restored. 29 tests.
|
|
|
|
Findings:
|
|
|
|
- CCR-2026-0058's `required_parameters` refused reads. Corrected in railiance-platform; needs re-approval.
|
|
- Revert steps now run in declared order (BEA-10).
|
|
- Definite failures revert; unknown outcomes stop.
|
|
- Receipts carry error types.
|
|
|
|
The image part moved to T13 (split agreed 2026-10-31).
|
|
|
|
## Executor runtime: packaging
|
|
|
|
```task
|
|
id: NK-WP-0056-T13
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T12]
|
|
state_hub_task_id: "7355e4a2-95cc-5860-a0b0-52978b8501e0"
|
|
```
|
|
|
|
The CLI entry point for one admission:
|
|
|
|
- reads the admission, the approval id and the configuration;
|
|
- uses Kubernetes auth to OpenBao;
|
|
- uses the in-cluster Kubernetes and audit sender;
|
|
- ships the pinned `lldap_set_password` in the image.
|
|
|
|
Also: the Containerfile and the Forgejo image workflow in net-kingdom, and the `rapp-bounded-executor` package
|
|
(`rmgr rapp wrap`, founder creates the Forgejo remote). The package holds one Job per admission in `sso` with the
|
|
T03 ServiceAccount, Role and an egress-only NetworkPolicy.
|
|
|
|
**Done when:** the image builds in the workflow, the rapp renders and passes `server-dry-run` against a synthetic
|
|
manifest check, and nothing is applied.
|
|
|
|
**2026-10-31, in progress.** Done in net-kingdom:
|
|
|
|
- `bounded-executor/cli.py` (exit codes 0/3/4/5/6), `clients.py` (approval-engine GET/consume, audit-core
|
|
`POST /v1/events` with `Idempotency-Key`);
|
|
- events now carry `occurred_at`, which audit-core requires;
|
|
- `Containerfile`: pinned Python and LLDAP stages, hash-locked requirements, UID 10001; builds locally;
|
|
- `.forgejo/workflows/bounded-executor-image.yaml`, and the executor tests are in `identity-journeys.yaml`;
|
|
- the pilot example pins the executed files (`executor.py`, `operations.py`, `journey.py`). 35 tests.
|
|
|
|
**2026-10-31, rapp reshaped.** `~/rapp-bounded-executor` is at `forgejo-remote:coulomb/rapp-bounded-executor` (`4517aaf`; rebased onto the Forgejo stub, so local hashes changed):
|
|
|
|
- Standing objects in `sso`:
|
|
- `manifests/rbac.yaml`, a byte-identical copy of `sso-mfa/k8s/bounded-executor/rbac.yaml`. A test fails on
|
|
drift; `make ceiling-sync` copies it.
|
|
- `manifests/networkpolicy.yaml`: no ingress. Egress only to DNS, the API server (node `/32`, 6443), LLDAP 17170,
|
|
KeyCape 8080, Authelia 9091, OpenBao 8200, approval-engine 8080 and audit-core 8080.
|
|
- No Namespace, Deployment, Service, probes or database.
|
|
- `templates/job.yaml` and `tools/render_job.py` render one ConfigMap (admission + config) and one Job per admission,
|
|
named `bounded-executor-<id>`:
|
|
- `make run` uses `kubectl create`, so a second run of the same admission fails;
|
|
- `backoffLimit: 0`; `activeDeadlineSeconds` = min(budget + 300 s, time left in the window);
|
|
- projected SA token, no automount;
|
|
- approval-engine and audit-core bearers come from an ESO Secret `bounded-executor-tokens`;
|
|
- the renderer refuses a placeholder digest, a window that is not open, another executor, secret-like config
|
|
keys and token paths that differ from the mounts.
|
|
- `declarations/rapp.yaml` names no database. `rmgr rapp validate` passes. 12 tests.
|
|
- `docs/runtime-contract.md` and `docs/platform-handoffs.md` list what must exist before T06.
|
|
|
|
Server dry run with the limited `ops-mason-builder` identity:
|
|
|
|
- ServiceAccount and NetworkPolicy: admitted.
|
|
- Role, RoleBinding and the per-admission ConfigMap and Job: `Forbidden` for that identity, so the founder runs them.
|
|
- The SSH path (`kubectl` on railiance01) can't read the k3s kubeconfig. The Makefile therefore takes
|
|
`KUBECTL=...`.
|
|
|
|
Left:
|
|
|
|
- DONE 2026-10-31, founder: full server dry run with the admin kubeconfig. The ServiceAccount, Role, RoleBinding and
|
|
NetworkPolicy were applied server-side as a dry run, and the synthetic BEA-2026-0001 ConfigMap and Job were created
|
|
as a dry run. Nothing was persisted.
|
|
|
|
**Done 2026-10-31.**
|
|
|
|
- The image workflow for `ba9d266` succeeded, and the digest
|
|
`sha256:8f40f998738789aec21898fe2a36fda56f33c6ae2bb5749f88583d4d70f070c2` is pinned in the rapp (`e9a943b`).
|
|
- `check-oci-image.sh` found the manifest, and `rmgr rapp validate` passes. 13 rapp tests.
|
|
- `rmgr rapp pin-image` only scans `manifests/`, so the pin went in by hand. A test keeps the Makefile, Job template
|
|
and declaration pins equal.
|
|
- Nothing was applied.
|
|
|
|
Follow-up outside the done criteria:
|
|
|
|
- DONE 2026-10-31: the founder created the Forgejo repo with a README stub. The local commits were rebased onto it
|
|
(the local README kept) and pushed; HEAD is `e779a22`.
|
|
|
|
Follow-up 2026-10-31 for T10/T06 reachability:
|
|
|
|
- `netpol-sso.yaml` gained `allow-bounded-executor-to-{lldap-api,keycape,authelia}`, which admit only executor pods,
|
|
on 17170, 8080 and 9091; never LDAP 3890. Tests: `test_netpol.py`, plus a rapp test that its in-namespace
|
|
egress equals these admissions. Prepared in source, not applied.
|
|
- Live reads (limited identity):
|
|
- OpenBao's chart policy admits every namespace on 8200, so nothing is needed there.
|
|
- approval-engine is deployed but admits only secrets-engine and namespaces labelled
|
|
`railiance.io/approval-engine-client=true`, and none are.
|
|
- audit-core has no ingress from `sso`.
|
|
- Both owners were asked for pod-scoped rules; see the rapp's `docs/platform-handoffs.md`.
|
|
|
|
## Executor approval identity: service-client case type
|
|
|
|
```task
|
|
id: NK-WP-0056-T14
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T13]
|
|
state_hub_task_id: "ec87ed9f-9604-5843-af08-bc771a58712d"
|
|
```
|
|
|
|
approval-engine only accepts an approval request whose `binding.actor` equals the caller's KeyCape token subject
|
|
(approval-engine `docs/bounded-execution-admission-carrier.md`). The executor therefore files its own request, the
|
|
founder approves it, and the executor consumes it. For that it needs its own KeyCape confidential client.
|
|
|
|
KeyCape has no generic service-client registration. Each existing client was registered by its own script
|
|
(`approval-clients-rollout.py`, `secrets-engine-openbao-client-rollout.py`). The canon route is a new security case
|
|
type, `oidc-service-client` 0.1, next to `oidc-browser-relying-party`:
|
|
|
|
- `client_credentials` grant, resource audience, service subject, tenant, roles, scopes, a token lifetime of at most
|
|
15m;
|
|
- the client secret minted straight into OpenBao custody, readable only by KeyCape and the consuming service;
|
|
- `keycape-client.py` support for building the client from such a case;
|
|
- the first case: `nk-bind-executor-approval` (audience `approval-engine`; scopes `approval:create`,
|
|
`approval:read` and `approval:consume`; subject `service:nk-bind-executor`; tenant `tenant:platform`).
|
|
|
|
The split of T14 into T14, T18 and T19 is a founder decision (2026-10-31).
|
|
|
|
**Done when:**
|
|
|
|
- the type, its tests and the first case validate;
|
|
- `keycape-client.py` renders the service client and is tested;
|
|
- an acknowledgement packet is ready for the founder.
|
|
|
|
Nothing is applied.
|
|
|
|
**Done 2026-10-31.** SecurityCaseType `oidc-service-client` 0.1 is `proposed` (`security-case-types/oidc-service-client/`):
|
|
|
|
- type, README and two test files;
|
|
- the first case `examples/nk-bind-executor-approval.yaml`: audience `approval-engine`; scopes `approval:create`,
|
|
`approval:read` and `approval:consume`; subject `service:nk-bind-executor`; tenant `tenant:platform`; 15m;
|
|
- the consumer reads the secret natively through its own bound role (`sso/nk-bind-executor`, audience `openbao`),
|
|
with no Secret copy;
|
|
- new capability declaration `net-kingdom.keycape-service-client-registration`.
|
|
|
|
`keycape-client.py` renders service clients in key-cape's canonical shape. Adopting `secrets-engine-approval`
|
|
reproduces key-cape's example record exactly. Both builders refuse missing parameters.
|
|
|
|
Tests: 138 pass (type, browser type, validator, keycape-client). Memo `nk-wp-0056-t14-oidc-service-client-r1`
|
|
(`decision/`) passes informed-decision's intake validator and State Hub's dispatcher contract. Hub decision
|
|
`f8044f81`.
|
|
|
|
Next:
|
|
|
|
- reviews from ops-mason, ops-warden, key-cape and railiance-platform;
|
|
- the operator queues the memo;
|
|
- the founder acknowledges;
|
|
- the case is accepted and goes live, which makes the type `implemented` (as part of T18/T19).
|
|
|
|
## Executor approval client and request phase
|
|
|
|
```task
|
|
id: NK-WP-0056-T18
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T14]
|
|
state_hub_task_id: "37186772-2749-573f-bc68-1ccf6840b60b"
|
|
```
|
|
|
|
Executor code:
|
|
|
|
- a fresh KeyCape token exchange per approval call (as secrets-engine does), with the client secret read through
|
|
an executor OpenBao lane (the case's custody);
|
|
- a `request` phase that creates the approval (`action: bounded-execution`, target admission id and digest,
|
|
`human_control: true`, `pdp_path: false`) and prints its id;
|
|
- the existing run phase then consumes it.
|
|
|
|
Presenting the approval to the founder follows the alignment review's "present an existing approval" contract
|
|
([review](../docs/proposals/2026-10-31-decision-services-alignment-review.md), S1 informed-decision).
|
|
|
|
**Done when:** the request and run phases are tested on fakes, and the new image is pinned.
|
|
|
|
**2026-10-31, built:**
|
|
|
|
- `b71f509`: `KeyCapeClientCredentials` (`client_secret_basic`, one scope per token, public Host for the in-cluster
|
|
URL, no redirects).
|
|
- `ApprovalEngine` takes a per-scope token provider and can create approvals.
|
|
- `cli.py --request` files the approval as `service:nk-bind-executor`:
|
|
- principal is the first approver's subject;
|
|
- target is the admission id and digest;
|
|
- validity is the admission window;
|
|
- `human_control` is on;
|
|
- `required_count` is the number of listed approvers;
|
|
- the client secret comes through its own OpenBao lane, is read once, and that login is revoked at once.
|
|
- audit-core's review found that the audit client rejected 202, the status of a first accept. The executor would
|
|
have stopped after consuming its approval. Fixed: 202 and 200 count as stored, and 409 is a distinct defect.
|
|
58 executor tests.
|
|
- rapp `48566a6`:
|
|
- the config names the executor's own client, and the renderer requires it exactly;
|
|
- the run Job mounts only the audit-core bearer;
|
|
- `make request` adds a request Job.
|
|
|
|
**Done 2026-10-31.** The image `sha256:c67cb04485ee53b910c2443f43eada0e80273ea879de6853e120a32415194a04`
|
|
(`7bd6033`) has executed files byte-identical to the repo. It is pinned in the rapp (`b90951a`). A live request
|
|
also needs T20 and T21.
|
|
|
|
## Executor approval exercise against the real services
|
|
|
|
```task
|
|
id: NK-WP-0056-T19
|
|
status: todo
|
|
priority: medium
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T18]
|
|
state_hub_task_id: "dd9c9500-40a7-550e-9672-14b5705e99ed"
|
|
```
|
|
|
|
Run create, approve, consume, idempotent retry, a different digest, revoke-before-consume and a wrong scope against
|
|
approval-engine's real source and KeyCape's pinned image with synthetic keys (secrets-engine
|
|
`tools/exercise_approval_identity.py` pattern). Value-free receipt.
|
|
|
|
**Done when:** the exercise passes and its receipt is recorded.
|
|
|
|
## Executor audit sender
|
|
|
|
```task
|
|
id: NK-WP-0056-T15
|
|
status: todo
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T13]
|
|
state_hub_task_id: "a3e764ab-e545-5102-a4a7-a07e91ce3f6b"
|
|
```
|
|
|
|
audit-core accepts the `started` event and the receipt only from a registered sender (AUDIT-WP-0018-T02). Needed:
|
|
|
|
- the versioned receipt schema, validator and fixtures (NK-WP-0040-T02 scope);
|
|
- the sender credential's custody, preferably an OpenBao read role for the executor rather than an ESO Secret;
|
|
- audit-core's registry entry and ingress rule. Facts sent 2026-10-31 (hub `ed03ba30`).
|
|
|
|
**Done when:** a synthetic run's events are accepted by audit-core's real ingestion (202, duplicate 200, conflict
|
|
409, wrong source refused), and the live seed is prepared for the founder.
|
|
|
|
## Owner ingress for the executor pod
|
|
|
|
```task
|
|
id: NK-WP-0056-T16
|
|
status: wait
|
|
priority: medium
|
|
assignee: claude-code
|
|
blocked_on: message-from:approval-engine
|
|
depends_on: [NK-WP-0056-T10]
|
|
state_hub_task_id: "a08c6b82-be3f-5dca-82f7-b2a0ced6b335"
|
|
```
|
|
|
|
approval-engine (hub `39a3bffa`) and audit-core (hub `deea5bf8`, `ed03ba30`) each admit only namespace `sso` with
|
|
pod label `app.kubernetes.io/name=bounded-executor` on 8080. Our side: the rapp's egress policy and `make netcheck`.
|
|
|
|
**Done when:** both rules are live and `make netcheck` (extended to both endpoints) shows them reachable from an
|
|
executor pod.
|
|
|
|
## Founder approval path at aal2
|
|
|
|
```task
|
|
id: NK-WP-0056-T17
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T10]
|
|
state_hub_task_id: "0964e610-cad3-500c-bd75-4d2b0323e886"
|
|
```
|
|
|
|
The founder must add a human approval entry with an `aal2` token carrying `approval:approve` for audience
|
|
`approval-engine`. approval-engine names `informed-decision` as the approver surface.
|
|
|
|
Find out whether that path exists and is usable today, and what a founder approval of one admission looks like end
|
|
to end. Then name the missing pieces and their owners.
|
|
|
|
**Done when:** the approval path is described with evidence (works today, or what is missing and who owns it).
|
|
|
|
**Done 2026-10-31** ([finding](../docs/proposals/2026-10-31-nk56-t17-founder-approval-path.md)): it does not work
|
|
today.
|
|
|
|
- informed-decision is the live approver surface. It posts the human `approval:approve` entry.
|
|
- But its intake always creates the approval itself with `actor: informed-decision`, and refuses a pinned
|
|
`approval_id`.
|
|
- approval-engine only lets the actor create.
|
|
- The executor and BEA require the actor `service:nk-bind-executor`.
|
|
|
|
Options:
|
|
|
|
- A, recommended: informed-decision presents an existing approval filed by a registered submitter.
|
|
- B: relax the actor binding in the executor and BEA.
|
|
- C: a founder CLI approver.
|
|
|
|
Founder decision needed before T14.
|
|
|
|
## Service-client type revision r2 from the owner reviews
|
|
|
|
```task
|
|
id: NK-WP-0056-T20
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T14]
|
|
state_hub_task_id: "e09162c4-01ea-540c-bbab-e8772f0f7ef5"
|
|
```
|
|
|
|
The 2026-10-31 reviews of `oidc-service-client` 0.1:
|
|
|
|
- ops-mason: `ops-mason` `3c69a25`;
|
|
- ops-warden: `ops-warden` `95d31c3`, `history/2026-10-31-oidc-service-client-follow-through-review.md`;
|
|
- key-cape: `key-cape` `655d8d3`, `docs/oidc-service-client-type-review.md`;
|
|
- railiance-platform: `railiance-platform` `6858bef`, `docs/credential-lane-designs/oidc-service-client-review.md`.
|
|
|
|
They ask for these changes before the founder acknowledges the type:
|
|
|
|
- The ESO role gets `audience: openbao` and `token_explicit_max_ttl`; the ESO policy gets `lookup-self` and
|
|
`revoke-self`.
|
|
- The consumer role name must differ from its policy name: `keycape-{client_id}-{consuming_service}-workload-kv-read`.
|
|
- Refuse the `default` ServiceAccount and the `external-secrets` and `openbao` namespaces.
|
|
- **Same namespace:** KeyCape's ESO target sits in `sso`, the consumer's namespace. Add the verification
|
|
`consumer-cannot-read-delivered-secret` (an RBAC can-i denial) and correct the README.
|
|
- An excess scope is refused with `invalid_profile_usage`, not `invalid_scope` (key-cape R3).
|
|
- **Rotation:** the mint step only creates. Rotation is an attended platform-admin write, then an ESO refresh, then
|
|
a KeyCape restart (railiance-platform R5, ops-warden R5, key-cape R5).
|
|
- The catalog entry body gets the fields ops-warden R6 asks for.
|
|
- The `custody-allowed-to-consumer` wording must mention the two self-lifecycle grants.
|
|
- One restart per case: bind-env and register-client in one rollout (key-cape R4).
|
|
- Open questions:
|
|
- the workload name, `bounded-executor` or `net-kingdom-bind-executor` (ops-mason R9);
|
|
- `consumer_owner_repo` as `rapp-bounded-executor` (ops-warden R10);
|
|
- an independent claims check (ops-warden R8).
|
|
|
|
Per-case CCRs are still required. railiance-platform drafted CCR-2026-0060 (KeyCape ESO reader) and CCR-2026-0061
|
|
(the executor's reader). The broker must admit the type after acknowledgement (railiance-platform R1, ops-warden
|
|
R1).
|
|
|
|
**Done when:** the type is at r2 with tests green, the memo r2 is validated, and the CCR names agree with the type.
|
|
|
|
**Done 2026-11-01.** `oidc-service-client` is at 0.2 (`proposed`) with every review change applied (README history
|
|
lists them). Settled here:
|
|
|
|
- `consuming_service` stays `bounded-executor`;
|
|
- the first case's `consumer_owner_repo` is `rapp-bounded-executor`.
|
|
|
|
The consumer role is `keycape-{client_id}-{consuming_service}-workload-kv-read`. A new test checks CCR-2026-0060 and
|
|
CCR-2026-0061 against the rendered role and policy names (they agree). 141 related tests pass.
|
|
|
|
Memo `nk-wp-0056-t14-oidc-service-client-r2` passes informed-decision's intake validator and State Hub's dispatcher
|
|
contract (hub decision `f8044f81`). The rapp follows the role rename (`828992e`).
|
|
|
|
Next for the founder: queue memo r2 through the State Hub memo dispatcher and acknowledge the type.
|
|
|
|
## Executor submits its admission memo to informed-decision
|
|
|
|
```task
|
|
id: NK-WP-0056-T21
|
|
status: done
|
|
priority: high
|
|
assignee: claude-code
|
|
depends_on: [NK-WP-0056-T18]
|
|
state_hub_task_id: "ae922bf8-067c-5d83-8129-4507639cfa24"
|
|
```
|
|
|
|
informed-decision built `present_existing` (`0e66a33`, INFD-WP-0010). The executor submits a memo through
|
|
`/v1/memos` that pins `approval_id` and the binding digest and carries the admission YAML as its packet.
|
|
informed-decision checks the approval by GET and presents it to the founder.
|
|
|
|
Needed:
|
|
|
|
- a projected token with audience `informed-decision-intake` in the request Job;
|
|
- a NetworkPolicy path to informed-decision's private listener on 8081;
|
|
- the request phase submitting the memo after creating the approval.
|
|
|
|
Outside net-kingdom:
|
|
|
|
- access-engine: review policy v13 hard-codes the requester `informed-decision`;
|
|
- key-cape: the informed-decision requester client needs `approval:read`;
|
|
- informed-decision: its registry entry and release (INFD-WP-0010-T02).
|
|
|
|
**Done when:** the request phase files the approval and the memo, tested against informed-decision's real intake
|
|
validator.
|
|
|
|
**Done 2026-11-01.** After creating the approval, `cli.py --request` composes one present_existing memo:
|
|
|
|
- the packet is the admission YAML itself (informed-decision's choice);
|
|
- three required highlights cover the scope ceiling, the window and budget, and the plan with its revert;
|
|
- the intent restates the binding without the actor;
|
|
- the idempotency key is `nk-bind-executor.<approval_id>`.
|
|
|
|
It is submitted to informed-decision's private intake with the projected `informed-decision-intake` token. Exit
|
|
codes: 0 presented; 7 requested but the memo was refused (the approval stays usable, and resubmitting is idempotent).
|
|
|
|
Tests:
|
|
|
|
- the envelope passes informed-decision's own `validate_submission` with the present_existing profile;
|
|
- the receipt is checked against the memo;
|
|
- a refused memo exits 7.
|
|
|
|
61 executor tests.
|
|
|
|
rapp (T21 commit): the request Job's projected token, the exact config section, and egress to informed-decision:8081.
|
|
|
|
The service-identity declaration notes key-cape's prepared `approval:read` for informed-decision's requester.
|
|
|
|
Live (founder-run, after the owners' releases):
|
|
|
|
- access-engine policy v14;
|
|
- key-cape `approval:read`;
|
|
- informed-decision's registry row and its ingress for the executor pod;
|
|
- the executor image pin.
|
|
|
|
## 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, NK-WP-0056-T13, NK-WP-0056-T14, NK-WP-0056-T15, NK-WP-0056-T16, NK-WP-0056-T17, NK-WP-0056-T18, NK-WP-0056-T19, NK-WP-0056-T20, NK-WP-0056-T21]
|
|
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.
|