diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 0e90982..4c71137 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -370,7 +370,7 @@ | task | NK-WP-0057-T02 | todo | — | workplans/NK-WP-0057-decision-routing-by-responsibility.md | | task | NK-WP-0057-T03 | todo | — | workplans/NK-WP-0057-decision-routing-by-responsibility.md | | task | NK-WP-0057-T04 | todo | — | workplans/NK-WP-0057-decision-routing-by-responsibility.md | -| task | NK-WP-0058-T01 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | +| task | NK-WP-0058-T01 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T02 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T03 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T04 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | diff --git a/docs/bounded-execution-contract-table.md b/docs/bounded-execution-contract-table.md new file mode 100644 index 0000000..beec6bf --- /dev/null +++ b/docs/bounded-execution-contract-table.md @@ -0,0 +1,228 @@ +# Bounded execution: cross-repo contract table + +NK-WP-0058-T01 (part 1), as of 2026-11-05. Parts 2 (T09: approval, case and custody chain) and 3 (T10: operations +and the founder) add rows to this file. NK-WP-0057-T01 takes the rows marked **owner decision** as inputs and does +not inventory them again. + +Each row is one interface on the bounded-execution path. Every claim cites the file that defines it, at these +revisions: net-kingdom `23a99c1`, rapp-bounded-executor `3ecd462`, approval-engine `f41a9ca`, informed-decision +`77161fe`, audit-core `3157454`, key-cape `82b471f`, railiance-platform `31cf74d`. + +**Marks.** + +- **keep:** one owner, one source, so it only needs a contract test (T02). +- **merge:** the contract is written down in more than one place. Keep the named source and derive or check the + copies. +- **delete:** the piece is redundant. +- **owner decision:** the owner or the boundary is unclear. It goes to NK-WP-0057 or to the named owner. + +**Tested today.** The levels are: + +- **CI:** runs in a Forgejo workflow on every change; +- **local:** a test that needs a sibling checkout and skips without one; +- **manual:** an exercise script someone has to run; +- **live:** proven only by a founder-run step; +- **none.** + +Two facts frame this column: + +- net-kingdom CI (`.forgejo/workflows/identity-journeys.yaml`) tests a source tarball of net-kingdom alone, so every + sibling-dependent test skips there. +- rapp-bounded-executor has no CI workflow. + +## Part 1: the executor and its services + +| ID | Interface | Owner and source of truth | Consumer | Tested today | 2026-11-04/05 failure | Mark | +| --- | --- | --- | --- | --- | --- | --- | +| E1 | OpenBao: Kubernetes login and KV, three lanes | railiance-platform: CCR-2026-0058, -0059, -0061 and `openbao/policies/` | executor (`operations.py OpenBao`); `executor-lanes.py` creates and deletes the write role | CI: fake OpenBao. Local: `tests/test_executor_lanes.py` against the CCRs | Write role left open overnight; `window-close` failed as bare `LaneError` | **merge** | +| E2 | KeyCape `client_credentials` token | key-cape: `src/internal/server/oidc/token.go`. Client registration: net-kingdom `sso-mfa/k8s/keycape/` with `keycape-client.py` | executor (`clients.py KeyCapeClientCredentials`) | CI: fakes. Manual: `exercise_approval_identity.py` (T19) against KeyCape source/image | none (the request phase worked) | **keep**; registration ownership is an **owner decision** | +| E3 | approval-engine: create, get and consume | approval-engine: `approval_engine/api.py`, `store.py` | executor (`clients.py ApprovalEngine`, `executor.py _check_approval`) | CI: fakes. Manual: T19 against approval-engine source | `platform-root` approval refused as `approval_entry`; finding which field failed took diagnostic pod 4 | **keep** | +| E4 | informed-decision: memo intake (`present_existing`) | informed-decision: `informed_decision/intake_service.py`, `deploy/lifecycle/intake-submitters.json` | executor (`clients.py InformedDecision`, `cli.py admission_memo`) | Local: `test_approval_request.py`, path hard-coded to `/home/worsch/informed-decision` | Memo reached `platform-root` only, so one window was wasted (routing, E5) | **keep** | +| E5 | The approver's identity across repos | **None.** It is defined four times (below) | executor `_check_approval`, informed-decision routing, the founder | none | One wasted window: the admission named `admin`, routing went to `platform-root` | **merge**; **owner decision** (NK-WP-0057) | +| E6 | audit-core: event ingest | audit-core: `docs/event-envelope.md`, `audit_core/ingestion.py`, sender registry. Receipt schema: net-kingdom `canon/schemas/execution-attribution-receipt_v0.1.schema.json` | executor (`clients.py AuditCore`) | CI: fakes. Manual: `exercise_audit_sender.py` (T15) against audit-core source. Receipt validator not in a workflow | none (receipt accepted 2026-11-05) | **keep** | +| E7 | LLDAP: sign-in and self password change | LLDAP upstream. The runtime in `sso` has no owner in `docs/responsibility-map.md`; its reference manifests are in net-kingdom `sso-mfa/k8s/` | executor (`operations.py Directory`, the image's `lldap_set_password`) | CI: fakes. Nothing pins the image's tool version against the server | none | **keep**; runtime ownership is an **owner decision** | +| E8 | Kubernetes API: ESO force-sync and Deployment restart | API: railiance-cluster. Role content: net-kingdom `sso-mfa/k8s/bounded-executor/rbac.yaml`, copied to rapp `manifests/rbac.yaml` | executor (`operations.py KubeDelivery`, `KubeRestart`) | Make only: `test_rbac.py` needs no sibling, but no workflow runs it. Local: rapp `test_ceiling_is_the_net_kingdom_source` | none in the run (ESO server-side apply was a founder step, T10) | **merge** | +| E9 | Network path of the executor pod | rapp `manifests/networkpolicy.yaml` (egress). Ingress belongs to each target: net-kingdom `netpol-sso.yaml` (lldap, keycape), approval-engine `e9e9f93`, audit-core `7728fde`, informed-decision `:8081`, OpenBao | executor `wait_all_reachable`, `make netcheck` | Local: rapp `test_every_in_namespace_egress_is_admitted_by_net_kingdom`. Live: `make netcheck` | Two windows refused `setup:URLError`; four diagnostic pods; live approval-engine had a policy its file lacked | **keep**; add a cross-owner ingress test | +| R1 | Image build and pin chain | net-kingdom builds (`.forgejo/workflows/bounded-executor-image.yaml`); rapp pins (`Makefile IMAGE_DIGEST`); the admission pins source files (`plan.source.commit` and file sha256) | renderer (`tools/render_job.py`), the executor's checks | none: commit to digest is recorded only in the rapp commit message | A re-pin and a new admission after `c2a0f2f`, all by hand | **merge** | +| R2 | Runtime config, Job template and exit codes | net-kingdom `bounded-executor/cli.py` (config keys in its docstring, `EXIT`). Copies: rapp `examples/config.pilot.json`, `docs/runtime-contract.md`, `render_job.py` allow-lists | rapp renderer, `pilot-run.sh`, the founder | Local: rapp `test_packaging.py` reads `cli.py` (skips without sibling); no rapp CI | `pilot-run.sh` edits `trust_satisfied` into a temporary copy of the config | **merge** | + +## Row details + +### E1: OpenBao lanes + +- **Contract:** + - address `http://openbao.openbao.svc:8200`; + - login `POST /v1/auth/kubernetes/login` with the projected token, audience `openbao`, from + `/var/run/secrets/openbao/token`; + - one role per lane, with leases of at most 15 minutes, each revoked at exit: + - write: `net-kingdom-bind-executor-svc-authelia-write`, KV `platform/workloads/net-kingdom/directory-binds/svc-authelia`, field `LDAP_BIND_PASSWORD`, `cas_required`; + - journey: `net-kingdom-bind-executor-syn-journey-read`, KV `.../synthetic-journey/syn-journey`, fields `PASSWORD`, `TOTP_SEED`; + - approval client: `keycape-nk-bind-executor-approval-bounded-executor-workload-kv-read`, field `CLIENT_SECRET`, read once and revoked at once. +- **Window:** the write role exists only between `executor-lanes.py window-open` and `window-close`. +- **Duplicates:** role names, KV paths and fields appear in four places: the CCR files, `executor-lanes.py LANES`, + rapp `config.pilot.json` and the rapp renderer's role allow-list. `LANES` also carries the write and journey + policy text inline. +- **Proposal:** + - `LANES` reads names, paths and fields from the CCRs. The policy text is compared against railiance-platform's + policy file, not kept as a second copy. + - The renderer's allow-list is checked against the same source. + - `executor-lanes.py` names the lane error's cause. On 2026-11-05 `phase: identity, LaneError` hid + "no platform-admin login" (T03). + +### E2: KeyCape client + +- **Contract:** + - `POST http://keycape.sso.svc.cluster.local:8080/token` with Host `kc.coulomb.social`, `client_secret_basic`, + client `nk-bind-executor-approval`; + - one scope per token (`approval:create`, `approval:read`, `approval:consume`); + - the answer must be a bearer token with `expires_in` of at most 900; + - a scope outside the list must be answered `invalid_profile_usage`. +- **Owners:** KeyCape owns the token behaviour. The client's registration lives in net-kingdom `sso-mfa/k8s/keycape/` + (deployment env and `keycape-client.py`), and its case is `security-cases/nk-bind-executor-approval.yaml` + (oidc-service-client). +- **Open:** + - `excess-scope-refused` is not yet proven against live KeyCape. + - **Owner decision:** ADR-0015 moves runtime packages to `rapp-*`, but there is no KeyCape rapp, so the client + registration sits in net-kingdom's reference manifests. Who owns registering a KeyCape client? + +### E3: approval-engine + +- **Contract:** + - create: `POST /v1/approvals` answers 201. `binding.actor` must equal the token's `sub` (`api.py`); the executor + sends `human_control: true`, `pdp_path: false` and `required_count` = the number of approvers. + - read: `GET /v1/approvals/{id}`. + - consume: `POST /v1/approvals/{id}/consume` with `request_digest` and `decision_id`. + - the executor accepts an entry only if: + - `subject_id` is a listed approver; + - `principal_type` is human; + - the assurance is aal2; + - the subject is not the executor itself. + - approval-engine also has `required_assurance`. The executor doesn't send it, and checks aal2 itself + ("recorded, not enforced"). +- **Proposal:** + - `approval_entry` names the failed field: subject, principal type or assurance. That alone would have replaced + diagnostic pod 4. + - Whether to send `required_assurance` is a question for approval-engine, not a local change. + +### E4: informed-decision intake + +- **Contract:** + - `POST /v1/memos` on `informed-decision-workload.informed-decision.svc:8081`; + - the bearer is the projected ServiceAccount token, audience `informed-decision-intake`, checked by TokenReview; + - the registry row is `system:serviceaccount:sso:nk-bind-executor` → submitter `nk-bind-executor`, memo class + `bounded-execution`, review group `net-kingdom-admins`, `present_existing.approval_actor` + `service:nk-bind-executor`, daily limit 10; + - the receipt must echo `memo_id`, `approval_id` and `approval_binding_digest`; + - `cli.py --request` exits 7 when the memo is not accepted. Resubmitting is idempotent. +- **Test gap:** `bounded-executor/tests/test_approval_request.py:216` hard-codes `/home/worsch/informed-decision` + (T07: no hard-coded paths). + +### E5: approver identity, defined four times + +| Where | Value on 2026-11-05 | Owner | +| --- | --- | --- | +| net-kingdom `admissions/BEA-2026-0003.yaml` `spec.approval.approvers` | `[admin]` (a name) | net-kingdom (`prepare_admission.py`) | +| rapp `examples/config.pilot.json` `approver_subjects`, `approver_issuer` | `admin` → `uid=admin,ou=people,dc=netkingdom,dc=local`, `https://kc.coulomb.social` | rapp-bounded-executor | +| informed-decision `deploy/lifecycle/review-groups.json` | `net-kingdom-admins` → `uid=admin,…` (interim, decision `c9bedd9c`; it was `platform-root` until `77161fe`) | informed-decision | +| key-cape LLDAP adapter | `sub` = the LDAP DN | key-cape | + +Nothing checks that these four agree, and a mismatch shows only as a refused approval after a window has opened. +**Until NK-WP-0057 decides who routes:** the T03 readiness check compares them. The admission's approver names +resolve through the rapp config and must equal the routing recipient for the memo's review group. + +### E6: audit-core + +- **Contract:** + - `POST /v1/events` with `Idempotency-Key` equal to the event's `id`, and all eight envelope fields; + - answers: 202 accepted, 200 duplicate, 400 dead-lettered with a named reason, and 409 for a reused run id with a + different body (a defect, never retried); + - the bearer is the file `/var/run/secrets/bounded-executor/audit-core`, from `sso/bounded-executor-tokens` key + `audit-core` (custody in part 2); + - the source is checked against the sender registry; + - receipt schema v0.1 and its EAR rules (`tools/execution-attribution-receipt`). +- **Proposal:** fold the T15 exercise into the T02 suite, and run `make execution-attribution-receipt-test` in CI. + +### E7: LLDAP + +- **Contract:** + - `http://lldap.sso.svc.cluster.local:17170`; + - sign-in checks over its HTTP API; + - the password change runs the image's `/app/lldap_set_password` with `--base-url`, `--token`, `--username` + (OPAQUE). +- **Gap:** nothing checks that the tool in the executor image is compatible with the running LLDAP server. +- **Owner decision:** `docs/responsibility-map.md` names no owner for the LLDAP, Authelia or KeyCape runtimes in + `sso`. SCOPE says the `sso-mfa/k8s/` material is "migration inputs, not blanket authority". + +### E8: Kubernetes API + +- **Contract:** + - force-sync: merge-patch `metadata.annotations.force-sync` on `/apis/external-secrets.io/v1/namespaces/sso/externalsecrets/authelia-secrets`, then poll its status; + - restart: merge-patch `spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"]` on Deployment `authelia`, then wait for the rollout; + - Role: `get/watch/patch` on that Deployment and `get/patch` on that ExternalSecret. No Secret access. +- **Duplicate:** the Role is written in net-kingdom and copied by hand into rapp (`make ceiling-sync`: "copy, never + edit"). The compare test skips without the sibling checkout, and rapp has no CI. +- **Proposal:** keep net-kingdom as the source, and make the compare test a T02 contract test that runs in CI. + +### E9: network path + +- **Contract:** + - the executor pod is labelled `app.kubernetes.io/name=bounded-executor` in namespace `sso`; + - egress per the rapp policy; + - each target admits it on its own port: approval-engine and audit-core on 8080, informed-decision on 8081, + KeyCape and LLDAP through `netpol-sso.yaml`, and OpenBao on 8200; + - the executor waits up to 30 s for all five endpoints. A setup failure names the unreachable one (since + `c2a0f2f`). +- **Gap:** the ingress side lives in five repos, and only the sso part is compared. The live approval-engine + policy differed from its file. +- **Proposal:** a T02 test that renders every owner's ingress policy for this label, plus a live comparison in the + T03 readiness check. + +### R1: image and pins + +- **Chain:** + 1. a net-kingdom commit; + 2. the Forgejo image workflow builds a digest; + 3. the digest is pinned by hand in rapp's `Makefile IMAGE_DIGEST`, with the source commit only in the rapp commit + message; + 4. separately, `prepare_admission.py --commit` pins the admission to source file sha256s + (`plan.source.files`). +- **Gap:** no record links the digest to the commit to the admission pins, so "the admission's pins match the + pinned image" cannot be checked. +- **Proposal:** + - the image workflow writes a value-free pin record (commit, file sha256s, digest); + - rapp pins that record; + - the renderer refuses when the admission's file hashes are not the image's (T03). + +### R2: config, Job and exit codes + +- **Source:** the config keys and exit codes are defined in `cli.py`: 0 completed, 3 reverted, 4 refused, 5 + unknown, 6 not in custody, and 7 for a request whose memo was not accepted. +- **Copies:** + - rapp `docs/runtime-contract.md` (prose); + - `config.pilot.json` (values); + - `render_job.py` allow-lists (token paths, role names); + - `pilot-run.sh` sets `trust_satisfied` by editing a temporary copy of the config. +- **Proposal:** + - a versioned config schema file in net-kingdom, which the renderer validates against; + - the exit codes stated once, in that file; + - `trust_satisfied` becomes a run input recorded in the state file (T04), not a config edit. + +## Summary for part 1 + +- **keep:** E2, E3, E4, E6, E7, E9. Each needs its T02 contract test. The T15 and T19 exercises become CI tests. +- **merge:** + - E1: lane names and policy text, from the CCRs; + - E5: the approver, compared until it has one source; + - E8: the RBAC copy; + - R1: the pin record; + - R2: the config schema. +- **delete:** nothing yet. The candidates are the hand copies named under merge, once they are derived or checked. +- **owner decision** (to NK-WP-0057-T01 or the named owner): + - E5: who owns the approver and routing; + - E2: KeyCape client registration with no KeyCape rapp; + - E7: the LLDAP, Authelia and KeyCape runtime owner in `sso`; + - E3: whether approval-engine should enforce `required_assurance`. +- **Transparency findings for T03:** + - `executor-lanes.py` lane errors without a cause; + - `approval_entry` without the failed field. +- **Hard-coded path for T07:** `test_approval_request.py:216`. diff --git a/workplans/NK-WP-0058-bounded-execution-operability.md b/workplans/NK-WP-0058-bounded-execution-operability.md index 62c7e66..8ebb4e2 100644 --- a/workplans/NK-WP-0058-bounded-execution-operability.md +++ b/workplans/NK-WP-0058-bounded-execution-operability.md @@ -116,7 +116,7 @@ Inputs for other tasks: ```task id: NK-WP-0058-T01 -status: todo +status: done priority: high assignee: claude-code state_hub_task_id: "fe618717-0c28-51c5-8edb-e5d2f4ba2252" @@ -142,6 +142,27 @@ second time. **Done when:** the table exists with sources for every part 1 row, and each row is marked keep, merge, delete or needs an owner decision. +**Result, 2026-11-05.** The table is `docs/bounded-execution-contract-table.md`, with 11 rows (E1–E9, R1, R2) and +details per row. + +- **keep:** E2, E3, E4, E6, E7 and E9. +- **merge:** + - E1: lane names and policy text are copied from the CCRs; + - E5: the approver is defined four times; + - E8: the RBAC is copied into the rapp; + - R1: no record links the commit, the digest and the admission pins; + - R2: the config and exit codes are copied into the rapp. +- **owner decisions for NK-WP-0057:** + - E5: approver and routing; + - E2: KeyCape client registration, since there is no KeyCape rapp; + - E7: the runtime owner of LLDAP, Authelia and KeyCape in `sso`; + - E3: approval-engine enforcing `required_assurance`. +- **Framing fact for T02:** net-kingdom CI tests net-kingdom alone, so every sibling-dependent test skips there. + rapp-bounded-executor has no CI, and the T15 and T19 exercises are manual. +- **Inputs for other tasks:** + - T03: `executor-lanes.py` lane errors without a cause, and `approval_entry` without the failed field; + - T07: a hard-coded path in `test_approval_request.py:216`. + ### Interface and responsibility inventory, part 2: approval, case and custody chain ```task