From 6a602edafbb04b05a3b5463fefe783e860cb516e Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 7 Oct 2026 13:47:14 +0200 Subject: [PATCH] MASON-WP-0009-T04 done: live capability verification of the KeyCape secrets-engine-openbao lane Value-free evidence under the read-only ops-mason-survey grant (revoked after use). Adds T05 for live-token checks and the pointer route. Fixes bao-session.sh grant misreporting an unreadable policy as absent. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 132360@bnt-lap001 Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b --- ...e-secrets-engine-openbao-capabilities.json | 134 ++++++++++++++++++ scripts/bao-session.sh | 20 ++- tests/test_bao_session.py | 41 ++++++ ...e-secrets-engine-openbao-client-custody.md | 41 +++++- 4 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 docs/evidence/2026-10-07-keycape-secrets-engine-openbao-capabilities.json create mode 100644 tests/test_bao_session.py diff --git a/docs/evidence/2026-10-07-keycape-secrets-engine-openbao-capabilities.json b/docs/evidence/2026-10-07-keycape-secrets-engine-openbao-capabilities.json new file mode 100644 index 0000000..d8a49e6 --- /dev/null +++ b/docs/evidence/2026-10-07-keycape-secrets-engine-openbao-capabilities.json @@ -0,0 +1,134 @@ +{ + "workplan": "MASON-WP-0009", + "task": "MASON-WP-0009-T04", + "date": "2026-10-07", + "kind": "capability verification (metadata and policy text only; value-free)", + "status": "pass", + "grant": { + "policy": "ops-mason-survey", + "extra_policies": ["default"], + "display_name": "token-claude-MASON-WP-0009-T04", + "bao_addr": "http://127.0.0.1:18200", + "founder_session_used": false, + "revoked_self": true, + "lookup_after_revoke_failed": true, + "token_file_shredded": true + }, + "kv_path": "platform/workloads/keycape/secrets-engine-openbao", + "secret_value_read": false, + "checks": { + "1_policies": { + "result": "pass", + "workload-kv-read-keycape-secrets-engine-openbao-eso": { + "live_matches_repo_file": true, + "repo_file": "railiance-platform/openbao/policies/workload-kv-read-keycape-secrets-engine-openbao-eso.hcl", + "paths": { + "platform/data/workloads/keycape/secrets-engine-openbao": ["read"], + "auth/token/lookup-self": ["read"], + "auth/token/revoke-self": ["update"] + } + }, + "workload-kv-read-keycape-secrets-engine-openbao-operator": { + "live_matches_repo_file": true, + "repo_file": "railiance-platform/openbao/policies/workload-kv-read-keycape-secrets-engine-openbao-operator.hcl", + "paths": { + "platform/data/workloads/keycape/secrets-engine-openbao": ["read"], + "auth/token/revoke-self": ["update"] + } + } + }, + "2_roles": { + "result": "pass", + "auth/kubernetes/role/external-secrets-keycape-secrets-engine-openbao": { + "matches": "railiance-platform/credential-change-requests/CCR-2026-0033.yaml", + "bound_service_account_names": ["external-secrets"], + "bound_service_account_namespaces": ["external-secrets"], + "audience": "openbao", + "token_ttl_s": 900, "token_max_ttl_s": 900, "token_explicit_max_ttl_s": 900, + "token_no_default_policy": true, + "token_policies": ["workload-kv-read-keycape-secrets-engine-openbao-eso"] + }, + "auth/netkingdom/role/keycape-secrets-engine-openbao-operator-workload-kv-read": { + "matches": "railiance-platform/credential-change-requests/CCR-2026-0034.yaml", + "role_type": "oidc", + "bound_claims_keys": ["groups", "/assurance/level"], + "bound_groups": ["net-kingdom-admins"], + "bound_assurance_level": "aal2", + "bound_claims_type": "string", + "user_claim": "sub", "groups_claim": "groups", + "oidc_scopes": ["openid", "profile", "email", "groups"], + "token_ttl_s": 300, "token_max_ttl_s": 300, "token_explicit_max_ttl_s": 300, + "token_num_uses": 4, + "token_type": "service", + "token_no_default_policy": true, + "token_policies": ["workload-kv-read-keycape-secrets-engine-openbao-operator"], + "allowed_redirect_uris_match_ccr": true, + "note": "includes the known harmless retired redirect https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback, as in the CCR" + } + }, + "3_kv_metadata": { + "result": "pass", + "method": "bao kv metadata get (never kv get)", + "current_version": 1, + "version_1_deleted": false, + "version_1_destroyed": false, + "cas_required": true, + "max_versions": 2, + "delete_version_after": "0s", + "custom_metadata_keys": ["ccr", "classification", "client_id", "issuer", "owner", "plan", "readers", "rotation"], + "custom_metadata_matches_plan_section_3_1": true + }, + "4_circularity": { + "result": "pass", + "a_policies_reaching_data_path": { + "policies_listed": 81, + "policies_read": 79, + "not_readable": {"ops-mason-survey": "denied by its own explicit deny (by design)", "root": "built-in, not readable"}, + "lane_policies": ["workload-kv-read-keycape-secrets-engine-openbao-eso", "workload-kv-read-keycape-secrets-engine-openbao-operator"], + "broad_policies": { + "platform-admin": "platform/* create/read/update/delete/list; bound to auth/netkingdom/role/platform-admin (with operator-custody)", + "root": "built-in" + }, + "other_grant_on_path": false, + "secrets_engine_policies_reach_path": false, + "secrets_engine_policies_checked": ["secrets-engine-build", "secrets-engine-prod", "secrets-engine-test"], + "secrets-engine-openbao_jwt_policy_exists": false + }, + "b_jwt_roles": { + "auth_jwt_mount_present": false, + "keycape_services_jwt_mount_present": false, + "auth_mounts": ["approle", "keycape (oidc)", "kubernetes", "netkingdom (oidc)", "token"], + "roles_surveyed": {"kubernetes": 27, "netkingdom": 12, "approle": 14}, + "roles_binding_path_reaching_policies": [ + "kubernetes/external-secrets-keycape-secrets-engine-openbao", + "netkingdom/keycape-secrets-engine-openbao-operator-workload-kv-read", + "netkingdom/platform-admin" + ] + }, + "c_eso_reader_on_kubernetes_auth": true + }, + "5_grant_self_check": { + "result": "pass", + "platform/data/workloads/keycape/secrets-engine-openbao": "deny", + "platform/metadata/workloads/keycape/secrets-engine-openbao": "list, read", + "sys/policies/acl/x": "read", + "sys/policies/acl/ops-mason-survey": "deny", + "auth/approle/role/x/secret-id": "deny", + "auth/token/create": "deny" + } + }, + "not_permitted_by_ops-mason-survey": [ + "auth/keycape/role (list/read) - the KeyCape human OIDC mount's roles were not surveyed; invariant (i)/(ii) cover it only through policy text", + "sys/policies/acl/ops-mason-survey (own policy, explicit deny)" + ], + "observations_for_owners": [ + "ops-mason-build (retired, 'never to be installed') is still installed. It denies platform/data/* but grants create/update on platform/metadata/*, which could change cas_required/max_versions of this entry. No surveyed role binds it. Founder/platform should delete it.", + "secrets-engine-prod may write sys/policies/acl/se-prod-* with unconstrained content and bind it via auth/approle/role/se-prod-*; it could author a policy reaching this path. No such policy exists now; this is an indirect route outside invariant (ii) as worded.", + "agent-high-risk-boundary does not list this path among its explicit denies, unlike the sibling secrets-engine approval-client/requester entries." + ], + "not_covered_here": [ + "Plan section 4 checks 1-3 and 6 need live lane tokens (operator OIDC login, ESO token, non-member identity, secrets-engine service-jwt run). The survey grant cannot mint or use them; policy text and role definitions are verified equivalent instead.", + "The secrets-engine-openbao service-JWT mount (keycape-services) and its policy do not exist yet (RPF-WP-0035-T02); invariant (ii) must be rechecked once they are applied.", + "Pointer-only ops-warden route for the operator lane (MASON-WP-0009-T05)." + ] +} diff --git a/scripts/bao-session.sh b/scripts/bao-session.sh index 8a79a90..402c2cc 100755 --- a/scripts/bao-session.sh +++ b/scripts/bao-session.sh @@ -83,11 +83,21 @@ cmd_grant() { if [[ -z "$task" ]]; then red "usage: $0 grant e.g. 'operators-mount'"; exit 2 fi - if ! bao policy read "$GRANT_POLICY" >/dev/null 2>&1; then - red "policy $GRANT_POLICY does not exist yet." - echo "Create it first — see docs, or ask Claude to draft it. Refusing to" - echo "fall back to a broader policy: an unscoped grant is the thing this" - echo "script exists to avoid." + local probe_err + if ! probe_err="$(bao policy read "$GRANT_POLICY" 2>&1 >/dev/null)"; then + # Only bao's own "No policy named" means absent. A 403 or a missing + # token means your session cannot read it (expired, or not permitted), + # which says nothing about whether the policy exists. + if [[ "$probe_err" == *"No policy named"* ]]; then + red "policy $GRANT_POLICY does not exist yet." + echo "Create it first — see docs, or ask Claude to draft it. Refusing to" + echo "fall back to a broader policy: an unscoped grant is the thing this" + echo "script exists to avoid." + else + red "cannot read policy $GRANT_POLICY (session expired or not permitted)." + echo "Check '$0 status'; refresh with '$0 login' and retry. This is not" + echo "evidence that the policy is missing." + fi exit 1 fi umask 077 diff --git a/tests/test_bao_session.py b/tests/test_bao_session.py new file mode 100644 index 0000000..e96fce0 --- /dev/null +++ b/tests/test_bao_session.py @@ -0,0 +1,41 @@ +"""bao-session.sh grant: distinguish an absent policy from an unreadable one.""" + +import os +import subprocess +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "bao-session.sh" + + +def _run_grant(tmp_path, stub_stderr, stub_rc): + bindir = tmp_path / "bin" + bindir.mkdir() + stub = bindir / "bao" + stub.write_text( + "#!/usr/bin/env bash\n" + f"echo {stub_stderr!r} >&2\n" + f"exit {stub_rc}\n" + ) + stub.chmod(0o755) + env = dict(os.environ) + env["PATH"] = f"{bindir}:{env['PATH']}" + env["HOME"] = str(tmp_path) + env["GRANT_FILE"] = str(tmp_path / "grant") + return subprocess.run( + ["bash", str(SCRIPT), "grant", "t"], env=env, capture_output=True, text=True + ) + + +def test_absent_policy_reported_as_absent(tmp_path): + r = _run_grant(tmp_path, "No policy named: ops-mason-survey", 2) + assert r.returncode == 1 + assert "does not exist yet" in r.stdout + assert not (tmp_path / "grant").exists() + + +def test_permission_denied_not_reported_as_absent(tmp_path): + r = _run_grant(tmp_path, "Code: 403. Errors: * permission denied", 2) + assert r.returncode == 1 + assert "does not exist" not in r.stdout + assert "session expired or not permitted" in r.stdout + assert not (tmp_path / "grant").exists() diff --git a/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md b/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md index 465ee30..3394f2f 100644 --- a/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md +++ b/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md @@ -9,7 +9,7 @@ flavor: planning owner: claude topic_slug: netkingdom created: "2026-10-06" -updated: "2026-10-06" +updated: "2026-10-07" state_hub_workstream_id: "d9be08f5-a0f5-5cd1-8f87-0a6868661d77" --- @@ -73,7 +73,7 @@ state_hub_task_id: "f59fae58-397a-5bf5-9d1d-a33dc4aaa2ac" ```task id: MASON-WP-0009-T04 -status: todo +status: done priority: medium state_hub_task_id: "01f55a4d-06de-5d48-a3db-a0746dcf9892" ``` @@ -87,3 +87,40 @@ Note (2026-10-07, MASON-IN-0004): the capability verification runs under the read-only `ops-mason-survey` grant, which replaces `ops-mason-build`. Installing that policy is a founder act (AGENTS.md, "Installing the survey policy"); ops-mason cannot install or widen it. + +Done 2026-10-07 under grant `claude-MASON-WP-0009-T04` (`ops-mason-survey`, +revoked and shredded after use). Evidence, value-free: +`docs/evidence/2026-10-07-keycape-secrets-engine-openbao-capabilities.json`. +All five capability checks pass: both lane policies match the platform repo +files; both roles match CCR-2026-0033/0034 (retired redirect noted); KV metadata +is v1, `cas_required`, `max_versions=2`, `delete_version_after=0s`, custom +metadata as in plan 3.1; only the two lane policies plus `platform-admin`/root +reach the data path, no `auth/jwt` (or `keycape-services`) mount exists, the +ESO reader is on Kubernetes auth; the grant itself is `deny` on the data path +and read-only on `sys/policies/acl/*`. Not permitted by the survey grant: +`auth/keycape/role`. Owner observations (retired `ops-mason-build` still +installed; `secrets-engine-prod` can author `se-prod-*` policies; this path is +missing from `agent-high-risk-boundary`) are in the evidence file. Plan checks +1-3 and 6 need live lane tokens and the route proposal is still owed: T05. + +Side finding, fixed: `scripts/bao-session.sh grant` reported "policy does not +exist yet" whenever `bao policy read` failed, including when the founder's +`~/.vault-token` session was expired or unauthorised. It now says "cannot read +(session expired or not permitted)" unless bao answers "No policy named" +(`tests/test_bao_session.py`). + +## Live lane proof and pointer route + +```task +id: MASON-WP-0009-T05 +status: wait +blocked_on: message-from:railiance-platform +blocking_reason: "Plan section 4 checks 1-3 and 6 need live lane tokens and the secrets-engine-openbao service-JWT mount and policy (RPF-WP-0035-T02), which are not applied yet; then recheck invariant (ii) and propose the pointer-only ops-warden route." +priority: medium +``` + +Once the service-JWT lane exists: re-run invariant (ii) against its policy, +collect owner-run evidence for checks 1-3 and 6 (operator and ESO token +capabilities, non-member refusal, a secrets-engine `service-jwt` run), then +propose the pointer-only ops-warden entry for the operator lane +(`resolvable: false` until those pass).