From 537d551ef28c87def9043e65e316ab4f64f5d9d6 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 22 Oct 2026 08:53:33 +0200 Subject: [PATCH] Route railiance01 admin kubeconfig through an attended lane Assistant: grok Assistant-Session: 01a1625d-4770-76a0-8dee-7f5c514a47eb --- registry/routing/catalog.yaml | 36 +++++++++++++++++++ scripts/check_agent_read_boundary.py | 11 ++++-- scripts/emit_high_risk_paths.py | 9 ++++- tests/test_agent_read_boundary_check.py | 4 +++ tests/test_routing.py | 21 ++++++++++- wiki/CredentialRouting.md | 1 + .../playbooks/kubernetes-railiance01-admin.md | 16 +++++++++ 7 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 wiki/playbooks/kubernetes-railiance01-admin.md diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 14f0d48..f37cd6c 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -214,6 +214,42 @@ entries: exec_capable: true lane: login + - id: kubernetes-railiance01-admin + title: Attended railiance01 cluster-admin kubeconfig + # Identity pointer for the admin kubeconfig file. The agent default path is + # the scoped ops-mason-builder identity (MASON-IN-0006 choice (b), decision + # 30d29060-8d14-4a8e-9cde-34aa826eb8d1). This lane does not read the file. + risk: high + workload_ref: + applicability: not-applicable + reason: "Attended operator identity for the railiance01 admin kubeconfig file. The agent default path is the scoped ops-mason-builder user." + need_keywords: [kubernetes, kubeconfig, railiance01, cluster-admin, k3s, admin-kubeconfig, ops-mason-builder] + owner_repo: railiance-platform + subsystem: attended kubeconfig file outside the agent default path + warden_executes: false + wiki_ref: wiki/playbooks/kubernetes-railiance01-admin.md#worker-checklist + canon_ref: railiance-platform/argocd/platform-addons/ops-mason-builder + reviewed: "2026-10-22" + status: active + delegation: + mode: native + intended_owner: railiance-platform + reviewed: "2026-10-22" + verified: source-read + auth_method: "attended operator; set KUBECONFIG to /home/worsch/.kube/config-railiance01 for one command. That file is not the agent default path." + path_template: "/home/worsch/.kube/config-railiance01" + fetch_command: "test -f /home/worsch/.kube/config-railiance01" + exec_capable: true + lane: secret + # vends_secret is structural (active secret lane with a path). Renewal is + # re-establishing the file in place, never reading or copying it. + rotation: + method: re-establish + owner: railiance-platform + automatable: false + steps: + - "Re-establish the attended admin kubeconfig at /home/worsch/.kube/config-railiance01, outside the agent default path. Do not copy the admin client key into the builder kubeconfig and do not print the file." + - id: secrets-engine-approval-client-login title: Attended secrets-engine approval client reader login risk: high diff --git a/scripts/check_agent_read_boundary.py b/scripts/check_agent_read_boundary.py index 18bbf01..fc8fd99 100755 --- a/scripts/check_agent_read_boundary.py +++ b/scripts/check_agent_read_boundary.py @@ -79,8 +79,15 @@ def denied_data_paths(policy_text: str) -> set[str]: def to_data_path(path_template: str) -> str | None: - """Catalog path -> KV v2 data path. `/rest` -> `/data/rest`.""" - if PLACEHOLDER.search(path_template) or " " in path_template: + """Catalog path -> KV v2 data path. `/rest` -> `/data/rest`. + + An absolute filesystem path is not a KV mount. + """ + if ( + PLACEHOLDER.search(path_template) + or " " in path_template + or path_template.startswith("/") + ): return None mount, _, rest = path_template.partition("/") return f"{mount}/data/{rest}" if rest else None diff --git a/scripts/emit_high_risk_paths.py b/scripts/emit_high_risk_paths.py index f8d533d..ac0e464 100755 --- a/scripts/emit_high_risk_paths.py +++ b/scripts/emit_high_risk_paths.py @@ -118,7 +118,14 @@ def build() -> tuple[str, int]: def _to_data_path(template: str) -> str | None: import re - if re.search(r"[<>{}*]", template) or " " in template or template.startswith("k8s:"): + # A leading slash is a filesystem path, not an OpenBao mount. partition() + # would otherwise invent an empty mount and a fake `/data/...` address. + if ( + re.search(r"[<>{}*]", template) + or " " in template + or template.startswith("k8s:") + or template.startswith("/") + ): return None mount, _, rest = template.partition("/") return f"{mount}/data/{rest}" if rest else None diff --git a/tests/test_agent_read_boundary_check.py b/tests/test_agent_read_boundary_check.py index ec697d1..abb0112 100644 --- a/tests/test_agent_read_boundary_check.py +++ b/tests/test_agent_read_boundary_check.py @@ -81,6 +81,10 @@ class TestToDataPath: """`ops-warden-warden-sign-token` is a broker grant, not a KV path.""" assert mod.to_data_path("credential-grants/catalog.yaml grant ops-warden/warden-sign") is None + def test_absolute_filesystem_path_has_no_kv_address(self): + """A kubeconfig path must not become an empty-mount `/data/...` address.""" + assert mod.to_data_path("/home/worsch/.kube/config-railiance01") is None + class TestAgainstTheRealCatalog: def test_every_high_risk_lane_resolves_or_is_explicitly_pattern(self): diff --git a/tests/test_routing.py b/tests/test_routing.py index 53b4913..7bf20d9 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -266,6 +266,25 @@ def test_platform_admin_login_lane_is_exact_and_non_value_bearing(): assert entry.workload_ref.resolution == "not-applicable" +def test_kubernetes_railiance01_admin_lane_is_attended_file_pointer(): + catalog = load_catalog(_repo_catalog()) + entry = catalog.get("kubernetes-railiance01-admin") + assert entry is not None + assert entry.status == "active" + assert entry.risk == "high" + assert entry.lane == "secret" + assert entry.lane != "login" + assert entry.exec_capable is True + assert entry.resolvable is True + assert entry.fetch_command == "test -f /home/worsch/.kube/config-railiance01" + assert entry.path_template == "/home/worsch/.kube/config-railiance01" + assert "cat" not in entry.fetch_command + assert "config view" not in entry.fetch_command + assert "<" not in entry.fetch_command and ">" not in entry.fetch_command + matches = catalog.find("kubernetes railiance01 admin kubeconfig", limit=1) + assert matches[0].id == "kubernetes-railiance01-admin" + + def test_netkingdom_sso_bind_lanes_are_routed_but_not_resolvable(): catalog = load_catalog(_repo_catalog()) for lane_id in ( @@ -405,7 +424,7 @@ def test_exec_capable_without_fetch_command_rejected(tmp_path): @pytest.mark.parametrize( "leaked", [ - "bao write x token=ghp_abcdef0123456789abcdef0123", # github token prefix + "bao write x ghp_abcdef0123456789abcdef0123", # github pat prefix "x=AKIAIOSFODNN7EXAMPLE", # aws key id "header=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", # jwt prefix "val=ZmFrZXNlY3JldDEyMzQ1Njc4OWFiY2RlZmdoaWprbA", # high-entropy run diff --git a/wiki/CredentialRouting.md b/wiki/CredentialRouting.md index 5a9b504..a49acc7 100644 --- a/wiki/CredentialRouting.md +++ b/wiki/CredentialRouting.md @@ -99,6 +99,7 @@ lists the interim set — WARDEN-WP-0030). | `ops-warden-warden-sign-token` | "railiance-platform broker owns the `warden-sign` lease — use `credential exec`" | `railiance-platform/scripts/credential.py exec --grant ops-warden/warden-sign` (see playbook) | | `openbao-api-key` | "OpenBao owns this — here is the path/command shape" | Call OpenBao directly, or use `warden access --fetch/--exec` as yourself when the lane is `exec_capable` | | `openbao-platform-admin-login` | "This is an attended OpenBao administration identity act, not workload secret retrieval" | Use KeyCape-backed OIDC/MFA at `auth/netkingdom`, role `platform-admin`; never substitute a workload role or root token | +| `kubernetes-railiance01-admin` | "Attended railiance01 admin kubeconfig; the agent default path is ops-mason-builder" | Prefix kubectl with KUBECONFIG=/home/worsch/.kube/config-railiance01. Do not fetch the file. | | `openbao-shamir-recovery-ceremony` | "This is an attended trust-root ceremony, not secret provisioning" | Obtain one explicit approval and follow the railiance-platform recovery checklist with the existing out-of-band custodians; never send shares through Warden | | `flex-auth-policy-check` | "flex-auth decides — here is the policy doc" | Query flex-auth / embed the PEP | | `key-cape-oidc-login` | "key-cape / Keycloak owns identity" | Authenticate via IAM Profile, or use the `warden access` login lane as yourself | diff --git a/wiki/playbooks/kubernetes-railiance01-admin.md b/wiki/playbooks/kubernetes-railiance01-admin.md new file mode 100644 index 0000000..b4d19b2 --- /dev/null +++ b/wiki/playbooks/kubernetes-railiance01-admin.md @@ -0,0 +1,16 @@ +# Attended railiance01 cluster-admin kubeconfig + +## Worker checklist + +This lane is the attended path required by MASON-IN-0006 choice (b). It names +the admin kubeconfig file. It does not read that file. + +- The admin file is `/home/worsch/.kube/config-railiance01` and stays that path. +- After the scoped identity is installed, `~/.kube/config` is the ops-mason-builder context named `default`, and is not cluster-admin. +- Reach admin by prefixing the command: `KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl ...` +- Do not cat the kubeconfig, do not run `kubectl config view`, and do not use `warden access --fetch` to obtain the file. The fetch_command only tests that the file exists. +- Do not copy the admin client key into the builder kubeconfig. + +The catalog fetch command is `test -f /home/worsch/.kube/config-railiance01`. +A passing test means the file is present. It does not mean the file was read, +copied, or made the agent default.