From 8b57d270de50e34e4d3929c78e18515273cc6e3d Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 31 Oct 2026 14:21:35 +0100 Subject: [PATCH] RPF-WP-0069: answer NK-WP-0056-T02; add workload-kv-write-next-version; CCR-2026-0058 (proposed) Owner answer for the bounded executor (hub 9d08288d): one Kubernetes role per lane for sso/nk-bind-executor; new contract-only request type for a CAS next-version write (no create, metadata or delete, 15m, agents fenced); CCR-2026-0058 drafted as proposed, not approved; audit sender in the CCR-2026-0021/0022 shape once audit-core admits it. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 994874@bnt-lap001 Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87 --- WORK-RECORDS.md | 7 + ...gdom-bind-executor-svc-authelia-write.yaml | 128 ++++++++++++++++++ .../bounded-executor.md | 107 +++++++++++++++ ...ngdom-bind-executor-svc-authelia-write.hcl | 16 +++ scripts/credential-change.py | 17 +++ scripts/workload_kv_write_next_version.py | 119 ++++++++++++++++ tests/test_workload_kv_write_next_version.py | 60 ++++++++ .../RPF-WP-0069-bounded-executor-lanes.md | 99 ++++++++++++++ 8 files changed, 553 insertions(+) create mode 100644 credential-change-requests/CCR-2026-0058-net-kingdom-bind-executor-svc-authelia-write.yaml create mode 100644 docs/credential-lane-designs/bounded-executor.md create mode 100644 openbao/policies/net-kingdom-bind-executor-svc-authelia-write.hcl create mode 100644 scripts/workload_kv_write_next_version.py create mode 100644 tests/test_workload_kv_write_next_version.py create mode 100644 workplans/RPF-WP-0069-bounded-executor-lanes.md diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index a53b2c9..c6fc4f5 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -48,6 +48,7 @@ | workplan | RPF-WP-0066 | proposed | — | workplans/RPF-WP-0066-openbao-durable-audit-snapshots-conformance.md | | workplan | RPF-WP-0067 | active | — | workplans/RPF-WP-0067-netkingdom-identity-receipt-and-metadata-answers.md | | workplan | RPF-WP-0068 | active | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md | +| workplan | RPF-WP-0069 | active | — | workplans/RPF-WP-0069-bounded-executor-lanes.md | | task | RPF-WP-ADHOC-2026-10-10-T01 | done | — | workplans/ADHOC-2026-10-10.md | | task | RPF-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md | | task | RPF-WP-ADHOC-2026-10-11-T02 | done | — | workplans/ADHOC-2026-10-11.md | @@ -225,6 +226,12 @@ | task | RPF-WP-0068-T02 | done | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md | | task | RPF-WP-0068-T03 | wait | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md | | task | RPF-WP-0068-T04 | todo | — | workplans/RPF-WP-0068-email-connect-platform-smtp.md | +| task | RPF-WP-0069-T01 | done | — | workplans/RPF-WP-0069-bounded-executor-lanes.md | +| task | RPF-WP-0069-T02 | wait | — | workplans/RPF-WP-0069-bounded-executor-lanes.md | +| task | RPF-WP-0069-T03 | wait | — | workplans/RPF-WP-0069-bounded-executor-lanes.md | +| task | RPF-WP-0069-T04 | wait | — | workplans/RPF-WP-0069-bounded-executor-lanes.md | +| task | RPF-WP-0069-T05 | wait | — | workplans/RPF-WP-0069-bounded-executor-lanes.md | +| task | RPF-WP-0069-T06 | wait | — | workplans/RPF-WP-0069-bounded-executor-lanes.md | | intake | RPF-IN-0001 | closed | — | intakes/intakes.md | | intake | RPF-IN-0002 | open | — | intakes/intakes.md | | intake | RPF-IN-0003 | open | — | intakes/intakes.md | diff --git a/credential-change-requests/CCR-2026-0058-net-kingdom-bind-executor-svc-authelia-write.yaml b/credential-change-requests/CCR-2026-0058-net-kingdom-bind-executor-svc-authelia-write.yaml new file mode 100644 index 0000000..449b320 --- /dev/null +++ b/credential-change-requests/CCR-2026-0058-net-kingdom-bind-executor-svc-authelia-write.yaml @@ -0,0 +1,128 @@ +id: CCR-2026-0058 +kind: credential-change-request +schema_version: 1 +request_type: workload-kv-write-next-version +title: NetKingdom bounded executor writes the next svc-authelia bind version (pilot) +status: proposed +created: '2026-10-31' +updated: '2026-10-31' +requester: + agent: claude-code + reason: 'NK-WP-0056-T02 owner request (hub 9d08288d), for NK-WP-0056-T06: the bounded + executor service:nk-bind-executor rotates the svc-authelia LLDAP bind unattended + under one founder admission (Bounded Execution Admission v0.1, proposed). The + executor must write the next KV version of the bind with CAS and read it back for + its checks and revert. Drafted by the railiance-platform worker; not approved.' +review: + required: true + required_approvers: + - platform-owner + - net-kingdom-owner + comments: [] +target: + domain: infotech + tenant: platform + workload: net-kingdom-bind-executor + environment: production + purpose: Let only the executor ServiceAccount sso/nk-bind-executor read and write the + next version of platform/workloads/net-kingdom/directory-binds/svc-authelia, with + CAS on the current version; no create, metadata, delete, destroy, list or sibling + path +admission: + standard: 'net-kingdom canon/standards/bounded-execution-admission_v0.1.md (proposed)' + executor_identity: service:nk-bind-executor + example: net-kingdom examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml + note: The OpenBao role does not evaluate the admission. The admission (approval-engine + consume, window, plan digest) is enforced by the executor and its owners; this + request bounds what a compromised or unadmitted executor can reach to one value. +openbao: + mount: platform + kv_path: platform/workloads/net-kingdom/directory-binds/svc-authelia + fields: + - LDAP_BIND_PASSWORD + policy_name: net-kingdom-bind-executor-svc-authelia-write + policy_file: openbao/policies/net-kingdom-bind-executor-svc-authelia-write.hcl + write: + mode: next-version + cas_required: true + create: false + metadata_read: false + delete: false + token_self_lifecycle: true + auth: + method: kubernetes + mount: kubernetes + role: net-kingdom-bind-executor-svc-authelia-write + audience: openbao + ttl: 15m + token_max_ttl: 15m + token_explicit_max_ttl: 15m + token_no_default_policy: true + policies: + - net-kingdom-bind-executor-svc-authelia-write + bound_claims: + service_account_names: + - nk-bind-executor + service_account_namespaces: + - sso + bound_claims_confirmed: false +access_frontdoor: + type: platform-owner + catalog_id: net-kingdom-bind-executor-svc-authelia-write + readiness: pending-review + resolvable: false +agent_fence: + data_and_metadata_denied: true + note: The svc-authelia data and metadata paths are already denied to coding agents + since CCR-2026-0053; setup re-checks the deny before the role is created. +lifecycle: + deactivate: Delete the role, then the policy; the KV entry and the ESO reader lane + (CCR-2026-0053) are unchanged. The bind falls back to founder-run rotation. + rotate: Executor reads the current version N, sets the new password in LLDAP, writes + version N+1 with options.cas=N; ESO projects it, Authelia restarts, the synthetic + journey proves sign-in. Revert writes the prior value as version N+2 with options.cas=N+1. + compromised: Delete the role first (stops new logins; existing tokens expire within + 15m, or revoke by accessor), then reset svc-authelia in LLDAP and rotate attended + as in CCR-2026-0053. +delivery: + surface: native-workload-login + namespace: sso + service_account: nk-bind-executor + reader: 'Executor runtime (net-kingdom source, rapp-* package per ADR-0015, NK-WP-0056-T03); + logs in with its projected ServiceAccount token, audience openbao; no Secret is + created by this request' +risk: + classification: medium + notes: + - A standing write role on the svc-authelia bind is new; until now only the attended + platform-admin wrote this path. A compromised executor ServiceAccount can replace + the bind value (Authelia sign-in outage on next restart) but cannot read any other + path, create paths, delete versions or read metadata. + - cas_required on the key makes every write state the version it replaces; a racing + or replayed write is refused by the engine, not by the executor. + - The executor also reads the current value (needed for CAS and revert). svc-authelia + is a read-only directory bind (lldap_strict_readonly). + - The role is a separate role from the ESO reader (CCR-2026-0053) and from the journey + read lane (NK-WP-0056-T10); policies are never combined in one role. + - Node root, ESO administration, cluster admins able to mint tokens for sso/nk-bind-executor + and platform-admin remain inside this credential boundary. +verification: + positive: + - Exact policy and role read back; login as sso/nk-bind-executor (audience openbao) + gets only this policy with a lease of at most 15m + - Write of the next version with options.cas equal to the current version succeeds + in a rehearsal on a synthetic sibling-free test path or under the T06 admission; + the value is compared in memory only + negative: + - Write without options or with a stale cas is refused + - Create of a new sibling path, metadata read, delete, destroy, undelete and list + are refused + - Wrong ServiceAccount, wrong namespace and wrong audience logins are denied + - Coding-agent boundary denies data and metadata of the path + activation_conditions: + - Explicit platform-owner and net-kingdom-owner approval of this exact request. + - ServiceAccount sso/nk-bind-executor exists from the reviewed rapp-* package + (bound_claims_confirmed then set to true by the platform-owner). + - Attended platform-admin (aal2) session sets cas_required on the key and applies + policy and role from a reviewed commit. + - Positive and negative results recorded in value-free receipts. diff --git a/docs/credential-lane-designs/bounded-executor.md b/docs/credential-lane-designs/bounded-executor.md new file mode 100644 index 0000000..a3a6ccd --- /dev/null +++ b/docs/credential-lane-designs/bounded-executor.md @@ -0,0 +1,107 @@ +# Bounded executor: OpenBao lanes and audit sender (owner answer, NK-WP-0056-T02) + +Owner answer from railiance-platform to NetKingdom's request +(net-kingdom `docs/proposals/2026-10-31-nk56-t02-owner-requests.md` §railiance-platform; +State Hub message `9d08288d`). Basis: Bounded Execution Admission v0.1 (proposed) and its pilot +example `bind-rotation-svc-authelia.yaml`. Workplan: `workplans/RPF-WP-0069-bounded-executor-lanes.md`. + +Everything here is source and proposals. No role, policy, Secret or KV value was created or read. +CCR-2026-0058 is `proposed`; approvals and the founder's choices below stay open. + +## Q1. Kubernetes auth role for `sso/nk-bind-executor`: which request types? + +**Answer: one role per lane, all bound to the one ServiceAccount, two request types.** + +The existing `workload-kv-read` contract requires a role to carry exactly its own policy +(`scripts/credential-change.py` `validate_workload_kv_read`, "openbao.auth.policies must contain exactly +the policy used by the auth method"; applier check "must contain only openbao.policy_name"). We keep that +rule. The executor logs in once per lane with the same projected ServiceAccount token (audience +`openbao`); each token carries one policy and lives at most 15 minutes. A combined role would make every +lane's review depend on every other lane. + +| Executor need | Request type | Status | +| --- | --- | --- | +| Write the next version of `platform/workloads/net-kingdom/directory-binds/svc-authelia` (CAS) and read it for checks/revert (T06) | `workload-kv-write-next-version` (**new**, `scripts/workload_kv_write_next_version.py`) | Type in source; CCR-2026-0058 `proposed` | +| Read `platform/workloads/net-kingdom/synthetic-journey/syn-journey` (`PASSWORD`, `TOTP_SEED`) for the journey (T10) | `workload-kv-read`, native workload login (`token_self_lifecycle: true`, `metadata_read: false`), as the native reader proof in CCR-2026-0053 | Not drafted; see below | +| LLDAP credential to set `svc-authelia`'s password | `workload-kv-read` of a new custody entry, after secrets-engine answers its Q2 | Not drafted | +| audit-core sender bearer (receipts) | `workload-kv-read` with ESO delivery, as CCR-2026-0021/0022 | Not drafted; see Q2 | + +### The new type `workload-kv-write-next-version` + +Prepared the way `attended-service-credential-delivery` was added: module +`scripts/workload_kv_write_next_version.py`, tests `tests/test_workload_kv_write_next_version.py`, dispatch in +`scripts/credential-change.py` (validate, plan, operator-commands, runbook). It is **contract-only**: there +is no delegated applier (it is not in `LANE_MODULES`). The attended platform-admin applies policy and role +from a reviewed commit, like CCR-2026-0052..0055. + +The contract refuses anything wider than: + +- one exact `platform/workloads/...` path on mount `platform`; no wildcard, no sibling; +- policy (generated by `policy_hcl`, artifact `openbao/policies/.hcl`): + `read` + `update` on `platform/data/` only, `required_parameters = ["options"]`, + `allowed_parameters = {data, options}`; plus `lookup-self`/`revoke-self`. No `create` (a missing path is + refused, so the executor cannot mint new entries), no `metadata/`, `delete/`, `destroy/`, `undelete/`, list; +- key-level `cas_required = true`, set by the attended platform-admin at setup. The engine then refuses + every write that does not name the version it replaces; races and replays fail closed in OpenBao, not in + executor code. (`required_parameters = ["options"]` alone cannot force `options.cas`; the key setting does.) +- Kubernetes auth: one ServiceAccount, one namespace, audience `openbao`, `ttl`/`token_max_ttl`/ + `token_explicit_max_ttl` = 15m, `token_no_default_policy: true`, exactly one policy; +- `access_frontdoor.resolvable: false`; `agent_fence.data_and_metadata_denied: true` (coding agents fenced); +- `admission` names the standard and executor identity; full `verification` and `lifecycle`; explicit + approval by every `review.required_approvers` role from `approved` onward. + +Revert (`restore-kv` in the example) is a further next-version write of the prior value with CAS. Reading +an older version is a `read` on `data/?version=N`, which the policy already grants; no extra +capability is needed. + +**What the role does not do.** OpenBao does not evaluate the admission. The role is standing: a compromised +`nk-bind-executor` ServiceAccount can replace the svc-authelia bind (sign-in outage on Authelia's next +restart) but reach nothing else. The admission window, consume-once and plan digest are enforced by the +executor, approval-engine and access-engine. If that is not enough, the founder can choose a +window-scoped role instead (created by the attended platform-admin at the window start, removed after); +that removes the "unattended" property of T06 for the platform side. Recommendation: standing role for the +pilot, because the reachable damage is one read-only directory bind already covered by an attended +recovery path (CCR-2026-0053 `lifecycle.compromised`). + +### Journey secrets (T10) + +CCR-2026-0057 is `attended-service-credential-delivery` with `runtime_access: false`; its contract forbids +any role or policy on that path. A `workload-kv-read` request for the executor therefore changes +CCR-2026-0057's stated boundary. It must say so, cite CCR-2026-0057, and be approved by both +platform-owner and net-kingdom-owner. CCR-2026-0057's attended procedure stays valid. Shape: role +`net-kingdom-bind-executor-syn-journey-read`, policy `read` on the data path only, native login, 15m. +Draft it when T10 is scheduled and the ServiceAccount exists. + +## Q2. Can the executor pod receive an ESO-projected audit-core sender bearer? + +**Yes, with the CCR-2026-0021/0022 shape, once audit-core admits the sender.** The receipt is an event to +audit-core; per RPF-WP-0067-T01 the wire is `POST /v1/events` to +`http://audit-core.audit-core.svc.cluster.local:8080`, with a per-sender bearer from the registry at +`platform/workloads/audit-core/senders`. `Idempotency-Key` is the envelope `id` and `correlation_id` is the +`run_id`. The envelope `source` equals the sender name. + +Shape (as `docs/credential-lane-designs/factory-audit-senders-review.md`): + +- custody `platform/workloads/net-kingdom/bounded-executor/audit-sender` (`AUDIT_TOKEN`), generated CAS=0 in + the attended platform process and merged into the registry as one exact sender (proposed name + `nk-bounded-executor`, `tenant:platform`, write true, read false, `evidence_kind: load-bearing`, + `secret_policy: redact`); no existing sender changes; +- exact-path policy, Kubernetes role for `external-secrets/external-secrets` only, ClusterSecretStore + restricted to namespace `sso`; ExternalSecret into `sso/nk-bind-executor-audit` key `audit-token`, + mounted as a file in the executor pod; +- coding-agent fence on the new data and metadata paths. + +The bearer lets the executor append its own receipts only. It is not an OpenBao identity. The ESO +manifests belong in the `rapp-*` package, as in CCR-2026-0053, which points to net-kingdom +`bind-eso.yaml`. Prerequisites: audit-core says yes to its Q1 (net-kingdom request §audit-core) and +names the sender. Then a `workload-kv-read` CCR follows here. + +## Open items for the founder and other owners + +1. Review and approve or change CCR-2026-0058 (platform-owner, net-kingdom-owner). It stays `proposed`. +2. Standing vs window-scoped write role (recommendation: standing for the pilot). +3. Accept that a journey-read CCR widens CCR-2026-0057's `runtime_access: false`. +4. secrets-engine: the LLDAP credential lane shape (its Q2) before a custody CCR can be drafted. +5. audit-core: sender admission and name, before the sender CCR. +6. railiance-apps / rapp package: the ServiceAccount `sso/nk-bind-executor` must exist before + `bound_claims_confirmed` can be set. diff --git a/openbao/policies/net-kingdom-bind-executor-svc-authelia-write.hcl b/openbao/policies/net-kingdom-bind-executor-svc-authelia-write.hcl new file mode 100644 index 0000000..9b2370f --- /dev/null +++ b/openbao/policies/net-kingdom-bind-executor-svc-authelia-write.hcl @@ -0,0 +1,16 @@ +path "platform/data/workloads/net-kingdom/directory-binds/svc-authelia" { + capabilities = ["read", "update"] + required_parameters = ["options"] + allowed_parameters = { + "data" = [] + "options" = [] + } +} + +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +path "auth/token/revoke-self" { + capabilities = ["update"] +} diff --git a/scripts/credential-change.py b/scripts/credential-change.py index f407c84..92cec1d 100755 --- a/scripts/credential-change.py +++ b/scripts/credential-change.py @@ -520,6 +520,8 @@ def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]: errors.extend(owner.validate_contract(ccr)) elif request_type == "attended-service-credential-delivery": errors.extend(attended_service_module().validate_contract(ccr)) + elif request_type == "workload-kv-write-next-version": + errors.extend(write_next_version_module().validate_contract(ccr)) elif request_type in LANE_MODULES: errors.extend(lane_module(ccr).validate(ccr, REPO_DIR)) else: @@ -728,6 +730,8 @@ def render_plan(ccr: dict[str, Any]) -> str: return "Use docs/credential-lane-designs/railiance-clock-host-delivery.md through the attended owner procedure. No workload policy or auth role is created." if ccr.get("request_type") == "attended-service-credential-delivery": return attended_service_module().procedure_note(ccr) + if ccr.get("request_type") == "workload-kv-write-next-version": + return write_next_version_module().procedure_note(ccr) openbao = ccr["openbao"] auth = openbao["auth"] payload = auth_payload(ccr) @@ -761,6 +765,8 @@ def render_operator_commands(ccr: dict[str, Any]) -> str: return "Use docs/credential-lane-designs/railiance-clock-host-delivery.md through the attended owner procedure. No workload policy or auth role is created." if ccr.get("request_type") == "attended-service-credential-delivery": return attended_service_module().procedure_note(ccr) + if ccr.get("request_type") == "workload-kv-write-next-version": + return write_next_version_module().procedure_note(ccr) openbao = ccr["openbao"] auth = openbao["auth"] auth_path = f"auth/{auth['mount']}/role/{auth['role']}" @@ -850,6 +856,15 @@ def attended_service_module(): return module +def write_next_version_module(): + """Owner contract for workload-kv-write-next-version (bounded executor, NK-WP-0056).""" + import importlib.util + spec = importlib.util.spec_from_file_location("workload_kv_write_next_version", REPO_DIR / "scripts/workload_kv_write_next_version.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def lane_module(ccr: dict[str, Any]): """Lazy-load the applier module for a lane request type, or None.""" name = LANE_MODULES.get(ccr.get("request_type")) @@ -1132,6 +1147,8 @@ def runbook_readiness_blockers(ccr: dict[str, Any]) -> list[str]: return ["use the attended owner procedure in docs/credential-lane-designs/railiance-clock-host-delivery.md"] if ccr.get("request_type") == "attended-service-credential-delivery": return ["use the attended procedure: " + attended_service_module().procedure_note(ccr)] + if ccr.get("request_type") == "workload-kv-write-next-version": + return ["use the attended procedure: " + write_next_version_module().procedure_note(ccr)] blockers: list[str] = [] status = ccr.get("status") if status not in RUNBOOK_ALLOWED_STATUSES: diff --git a/scripts/workload_kv_write_next_version.py b/scripts/workload_kv_write_next_version.py new file mode 100644 index 0000000..8572eab --- /dev/null +++ b/scripts/workload_kv_write_next_version.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""workload-kv-write-next-version CCRs: a workload writes the next version of one KV entry. + +NetKingdom NK-WP-0056 (bounded executor, hub request 9d08288d). A non-human +executor rotates one existing value under a founder admission: it reads the +current version, writes the next version with CAS on that version, and may +revert by writing the prior value as a further version. It never creates a +path, reads metadata, deletes, destroys or lists. + +The contract is source-only: validation, the exact policy HCL and an attended +procedure note. The policy, Kubernetes auth role and key-level ``cas_required`` +are applied by the attended platform-admin from a reviewed commit, as with +attended-service-credential-delivery. Nothing here calls OpenBao or Kubernetes, +or reads, prints or accepts a secret value. +""" +from __future__ import annotations + +import re +from typing import Any + +REQUEST_TYPE = "workload-kv-write-next-version" +DOC = "docs/credential-lane-designs/bounded-executor.md" +APPROVED = {"approved", "applied", "verified", "active"} +KV_RE = re.compile(r"^platform/workloads/[a-z0-9][a-z0-9-]*(/[a-z0-9][a-z0-9-]*)+$") +NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$") +FIELD_RE = re.compile(r"^[A-Z][A-Z0-9_]*$") +OPENBAO_KEYS = {"mount", "kv_path", "fields", "policy_name", "policy_file", "write", "auth"} +WRITE = {"mode": "next-version", "cas_required": True, "create": False, "metadata_read": False, + "delete": False, "token_self_lifecycle": True} +TTL = "15m" +AUTH_FIXED = {"method": "kubernetes", "mount": "kubernetes", "audience": "openbao", "ttl": TTL, + "token_max_ttl": TTL, "token_explicit_max_ttl": TTL, "token_no_default_policy": True} +DISALLOWED_POLICIES = {"root", "default", "platform-admin"} + + +def policy_hcl(ccr: dict[str, Any]) -> str: + """Exact policy: read and update of one data path; no create, metadata, delete or list.""" + openbao = ccr["openbao"] + suffix = openbao["kv_path"][len("platform/"):] + return (f'path "platform/data/{suffix}" {{\n' + ' capabilities = ["read", "update"]\n' + ' required_parameters = ["options"]\n' + ' allowed_parameters = {\n "data" = []\n "options" = []\n }\n' + "}\n\n" + 'path "auth/token/lookup-self" {\n capabilities = ["read"]\n}\n\n' + 'path "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n') + + +def _approvals(ccr: dict[str, Any]) -> set[str]: + comments = (ccr.get("review") or {}).get("comments") or [] + return {str(c.get("reviewer", "")).removeprefix("User (").removesuffix(")") + for c in comments if c.get("decision") == "approved"} + + +def validate_contract(ccr: dict[str, Any]) -> list[str]: + errors: list[str] = [] + if ccr.get("request_type") != REQUEST_TYPE: + errors.append("unsupported write-next-version contract") + openbao = ccr.get("openbao") + if not isinstance(openbao, dict): + return errors + ["openbao must be an object"] + extra = set(openbao) - OPENBAO_KEYS + if extra: + errors.append("openbao declares unsupported keys: " + ", ".join(sorted(extra))) + kv_path = str(openbao.get("kv_path", "")) + if openbao.get("mount") != "platform" or not KV_RE.match(kv_path): + errors.append("openbao.kv_path must be one exact platform/workloads/... path on mount platform") + fields = openbao.get("fields") + if not isinstance(fields, list) or not fields or not all(isinstance(f, str) and FIELD_RE.match(f) for f in fields): + errors.append("openbao.fields must list upper-case field names") + policy = openbao.get("policy_name") + if not isinstance(policy, str) or not NAME_RE.match(policy) or policy in DISALLOWED_POLICIES: + errors.append("openbao.policy_name must be a dedicated lower-case policy name") + elif openbao.get("policy_file") != f"openbao/policies/{policy}.hcl": + errors.append("openbao.policy_file must be openbao/policies/.hcl") + if openbao.get("write") != WRITE: + errors.append("openbao.write must be next-version with cas_required, no create, metadata or delete, " + "and token self-lifecycle") + auth = openbao.get("auth") + if not isinstance(auth, dict): + errors.append("openbao.auth must be an object") + else: + for key, value in AUTH_FIXED.items(): + if auth.get(key) != value: + errors.append(f"openbao.auth.{key} must be {value}") + if not NAME_RE.match(str(auth.get("role", ""))): + errors.append("openbao.auth.role must be a dedicated role name") + if auth.get("policies") != [policy]: + errors.append("openbao.auth.policies must contain exactly openbao.policy_name") + claims = auth.get("bound_claims") or {} + for key in ("service_account_names", "service_account_namespaces"): + values = claims.get(key) + if not (isinstance(values, list) and len(values) == 1 and NAME_RE.match(str(values[0]))): + errors.append(f"openbao.auth.bound_claims.{key} must name exactly one value") + admission = ccr.get("admission") + if not isinstance(admission, dict) or not admission.get("standard") or not admission.get("executor_identity"): + errors.append("admission must name the admission standard and executor identity") + if (ccr.get("access_frontdoor") or {}).get("resolvable") is not False: + errors.append("runtime credential frontdoor must remain disabled") + if (ccr.get("agent_fence") or {}).get("data_and_metadata_denied") is not True: + errors.append("coding agents must be fenced from the data and metadata paths") + if ccr.get("status") in APPROVED: + approved = _approvals(ccr) + for role in (ccr.get("review") or {}).get("required_approvers") or []: + if role not in approved: + errors.append("explicit role approval missing: " + role) + for section, keys in {"verification": ["positive", "negative", "activation_conditions"], + "lifecycle": ["deactivate", "rotate", "compromised"]}.items(): + for key in keys: + if not (ccr.get(section) or {}).get(key): + errors.append(f"{section}.{key} required") + return errors + + +def procedure_note(ccr: dict[str, Any]) -> str: + openbao = ccr["openbao"] + return (f"Attended platform-admin applies policy {openbao['policy_name']} and Kubernetes role " + f"{openbao['auth']['role']} from a reviewed commit, sets cas_required on {openbao['kv_path']} " + f"and proves the negatives ({DOC}). The workload may only write the next version with CAS.") diff --git a/tests/test_workload_kv_write_next_version.py b/tests/test_workload_kv_write_next_version.py new file mode 100644 index 0000000..a86cb2f --- /dev/null +++ b/tests/test_workload_kv_write_next_version.py @@ -0,0 +1,60 @@ +"""workload-kv-write-next-version contract (NetKingdom NK-WP-0056 bounded executor); no values.""" +import copy +import importlib.util +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location("wkw", ROOT / "scripts/workload_kv_write_next_version.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +CCR = yaml.safe_load((ROOT / "credential-change-requests/CCR-2026-0058-net-kingdom-bind-executor-svc-authelia-write.yaml").read_text()) + + +def test_checked_in_request_is_valid(): + assert m.validate_contract(CCR) == [] + + +def test_policy_artifact_matches_contract(): + path = ROOT / CCR["openbao"]["policy_file"] + assert path.read_text() == m.policy_hcl(CCR) + hcl = m.policy_hcl(CCR) + assert '"read", "update"' in hcl and "create" not in hcl and "metadata" not in hcl + assert 'required_parameters = ["options"]' in hcl + + +def test_write_shape_cannot_be_widened(): + for change in ({"cas_required": False}, {"create": True}, {"metadata_read": True}, {"delete": True}, + {"mode": "any"}): + ccr = copy.deepcopy(CCR); ccr["openbao"]["write"].update(change) + assert m.validate_contract(ccr), change + ccr = copy.deepcopy(CCR); ccr["openbao"]["kv_path"] = "platform/workloads/net-kingdom/*" + assert m.validate_contract(ccr) + ccr = copy.deepcopy(CCR); ccr["openbao"]["metadata_paths"] = ["x"] + assert m.validate_contract(ccr) + + +def test_auth_is_one_serviceaccount_one_policy_15m(): + for key, value in (("ttl", "1h"), ("token_no_default_policy", False), ("audience", "x")): + ccr = copy.deepcopy(CCR); ccr["openbao"]["auth"][key] = value + assert m.validate_contract(ccr), key + ccr = copy.deepcopy(CCR); ccr["openbao"]["auth"]["policies"].append("other") + assert "openbao.auth.policies must contain exactly openbao.policy_name" in m.validate_contract(ccr) + ccr = copy.deepcopy(CCR); ccr["openbao"]["auth"]["bound_claims"]["service_account_names"].append("x") + assert m.validate_contract(ccr) + + +def test_fence_frontdoor_and_admission_required(): + ccr = copy.deepcopy(CCR); ccr["agent_fence"]["data_and_metadata_denied"] = False + assert "coding agents must be fenced from the data and metadata paths" in m.validate_contract(ccr) + ccr = copy.deepcopy(CCR); ccr["access_frontdoor"]["resolvable"] = True + assert "runtime credential frontdoor must remain disabled" in m.validate_contract(ccr) + ccr = copy.deepcopy(CCR); del ccr["admission"] + assert m.validate_contract(ccr) + + +def test_approved_stage_needs_every_required_approval(): + ccr = copy.deepcopy(CCR); ccr["status"] = "approved" + ccr["review"]["comments"] = [{"reviewer": "platform-owner", "decision": "approved"}] + assert m.validate_contract(ccr) == ["explicit role approval missing: net-kingdom-owner"] diff --git a/workplans/RPF-WP-0069-bounded-executor-lanes.md b/workplans/RPF-WP-0069-bounded-executor-lanes.md new file mode 100644 index 0000000..16ec347 --- /dev/null +++ b/workplans/RPF-WP-0069-bounded-executor-lanes.md @@ -0,0 +1,99 @@ +--- +id: RPF-WP-0069 +type: workplan +title: "Bounded executor lanes: write-next-version type, journey read and audit sender (NK-WP-0056)" +domain: financials +repo: railiance-platform +status: active +flavor: implementation +owner: claude +topic_slug: railiance +created: "2026-10-31" +updated: "2026-10-31" +related: + - NK-WP-0056 + - NK-WP-0040 + - RPF-WP-0067 +--- + +# RPF-WP-0069 — Bounded executor lanes (NK-WP-0056-T02 owner answer) + +Receiving workplan for NetKingdom's owner request (State Hub message +`9d08288d-e866-4e1c-9c0b-f1cc0968bd4d`; net-kingdom +`docs/proposals/2026-10-31-nk56-t02-owner-requests.md`). Owner answer: +`docs/credential-lane-designs/bounded-executor.md`. No live, cluster or OpenBao action is +part of T01; every CCR stays `proposed` until its approvers act. + +## Answer the request and add the write-next-version request type + +```task +id: RPF-WP-0069-T01 +status: done +priority: high +``` + +Answer doc written; new contract-only request type `workload-kv-write-next-version` +(`scripts/workload_kv_write_next_version.py`, `tests/test_workload_kv_write_next_version.py`, +dispatch in `scripts/credential-change.py`); CCR-2026-0058 drafted `proposed` with policy artifact +`openbao/policies/net-kingdom-bind-executor-svc-authelia-write.hcl`. + +## Review CCR-2026-0058 (founder gate) + +```task +id: RPF-WP-0069-T02 +status: wait +priority: high +needs_human: true +``` + +platform-owner and net-kingdom-owner approve or change CCR-2026-0058. The founder chooses +standing or window-scoped write role (recommendation: standing for the pilot). Binding is +confirmed only after `sso/nk-bind-executor` exists from the reviewed rapp-* package. + +## Attended setup and verification of CCR-2026-0058 + +```task +id: RPF-WP-0069-T03 +status: wait +priority: high +``` + +After T02: an attended platform-admin (aal2) session sets `cas_required` on the svc-authelia key, +re-checks the coding-agent fence, applies policy and role, and records value-free positive and +negative receipts. This needs an owner setup script modelled on net-kingdom `bind-lane.py` +setup/verify; write it once approval exists. The founder runs it. + +## Journey-secret read lane for the executor (NK-WP-0056-T10) + +```task +id: RPF-WP-0069-T04 +status: wait +priority: medium +``` + +Draft a `workload-kv-read` CCR (native login, data read only, 15m) for +`platform/workloads/net-kingdom/synthetic-journey/syn-journey`, stating that it widens +CCR-2026-0057's `runtime_access: false`. Waits for T10 scheduling and the ServiceAccount. + +## Executor audit-core sender (NK-WP-0040) + +```task +id: RPF-WP-0069-T05 +status: wait +priority: medium +``` + +After audit-core admits the sender and names it, draft a `workload-kv-read` CCR in the +CCR-2026-0021/0022 shape: custody `platform/workloads/net-kingdom/bounded-executor/audit-sender`, +ESO into `sso/nk-bind-executor-audit`, ClusterSecretStore restricted to `sso`. + +## LLDAP credential custody for the executor + +```task +id: RPF-WP-0069-T06 +status: wait +priority: medium +``` + +After secrets-engine answers the lane shape for the executor's LLDAP credential (its Q2), +draft the custody CCR here.