From 4e60f69f93bf21ec245f1991d86dc6fe1fa2a357 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 6 Nov 2026 01:12:32 +0100 Subject: [PATCH] nk-agent-status: read-only status identity for the net-kingdom agent (NK-WP-0058-T05, decision 4af1ec01) Founder decision 2026-11-06, option A, 1 h tokens. ServiceAccount sso/nk-agent-status without automount; read-only Jobs, pods, Deployments and ExternalSecrets in sso; one named ConfigMap in informed-decision; the informed-decision Application in argocd. No Secrets, pod logs, exec, writes or cluster scope. Guard: tests/test_nk_agent_status_rbac.py. The Application follows in the next commit, pinned here. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 943911@bnt-lap001 Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0 --- .../nk-agent-status/kustomization.yaml | 12 +++ .../platform-addons/nk-agent-status/rbac.yaml | 85 ++++++++++++++++++ .../nk-agent-status/serviceaccount.yaml | 10 +++ tests/test_nk_agent_status_rbac.py | 86 +++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 argocd/platform-addons/nk-agent-status/kustomization.yaml create mode 100644 argocd/platform-addons/nk-agent-status/rbac.yaml create mode 100644 argocd/platform-addons/nk-agent-status/serviceaccount.yaml create mode 100644 tests/test_nk_agent_status_rbac.py diff --git a/argocd/platform-addons/nk-agent-status/kustomization.yaml b/argocd/platform-addons/nk-agent-status/kustomization.yaml new file mode 100644 index 0000000..05ff5e3 --- /dev/null +++ b/argocd/platform-addons/nk-agent-status/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# net-kingdom NK-WP-0058-T05, founder decision 4af1ec01 (option A, 2026-11-06). Read only by +# argocd/railiance01/applications/nk-agent-status.application.yaml. +# Read-only status identity for the net-kingdom agent on the bounded-execution path (SI-14: the agent presents as +# itself). No Secret, no static token, no write verb, no pod logs. Credentials are 1 h TokenRequest tokens that the +# founder mints per working session (net-kingdom `make agent-status-token`). + +resources: + - serviceaccount.yaml + - rbac.yaml diff --git a/argocd/platform-addons/nk-agent-status/rbac.yaml b/argocd/platform-addons/nk-agent-status/rbac.yaml new file mode 100644 index 0000000..01a3b87 --- /dev/null +++ b/argocd/platform-addons/nk-agent-status/rbac.yaml @@ -0,0 +1,85 @@ +# Read-only, namespaced, named where it leaves sso. Never: Secrets, pod logs, exec, attach, port-forward, +# serviceaccounts/token, a write verb or a cluster-scoped grant. Guard: tests/test_nk_agent_status_rbac.py. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nk-agent-status + namespace: sso + labels: + railiance-platform/component: nk-agent-status +rules: + # bounded-executor Jobs and their pods (phase, conditions, exit codes); KeyCape and Authelia rollouts + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch"] + # Ready of the custody deliveries; an ExternalSecret holds references, never values + - apiGroups: ["external-secrets.io"] + resources: ["externalsecrets"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: nk-agent-status + namespace: sso + labels: + railiance-platform/component: nk-agent-status +roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: nk-agent-status} +subjects: + - {kind: ServiceAccount, name: nk-agent-status, namespace: sso} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nk-agent-status + namespace: informed-decision + labels: + railiance-platform/component: nk-agent-status +rules: + # the live routing and submitter registry (review-groups.json, intake-submitters.json): non-secret configuration + - apiGroups: [""] + resources: ["configmaps"] + resourceNames: ["informed-decision-workload-registry"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: nk-agent-status + namespace: informed-decision + labels: + railiance-platform/component: nk-agent-status +roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: nk-agent-status} +subjects: + - {kind: ServiceAccount, name: nk-agent-status, namespace: sso} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nk-agent-status + namespace: argocd + labels: + railiance-platform/component: nk-agent-status +rules: + # the informed-decision Application's sync revision and status (is live the pinned revision?) + - apiGroups: ["argoproj.io"] + resources: ["applications"] + resourceNames: ["informed-decision"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: nk-agent-status + namespace: argocd + labels: + railiance-platform/component: nk-agent-status +roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: nk-agent-status} +subjects: + - {kind: ServiceAccount, name: nk-agent-status, namespace: sso} diff --git a/argocd/platform-addons/nk-agent-status/serviceaccount.yaml b/argocd/platform-addons/nk-agent-status/serviceaccount.yaml new file mode 100644 index 0000000..8e1513c --- /dev/null +++ b/argocd/platform-addons/nk-agent-status/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nk-agent-status + namespace: sso + labels: + app.kubernetes.io/name: nk-agent-status + app.kubernetes.io/part-of: railiance-gitops + railiance-platform/component: nk-agent-status +automountServiceAccountToken: false # no pod runs as it; the founder mints a 1 h token per session diff --git a/tests/test_nk_agent_status_rbac.py b/tests/test_nk_agent_status_rbac.py new file mode 100644 index 0000000..1e1a3ff --- /dev/null +++ b/tests/test_nk_agent_status_rbac.py @@ -0,0 +1,86 @@ +"""Offline guard for the net-kingdom agent status identity (net-kingdom NK-WP-0058-T05, decision 4af1ec01). + +Parses the GitOps manifests and asserts the founder's 2026-11-06 boundary (option A): read-only verbs on the curated +namespaced resources, named objects outside sso, no forbidden resources, no wildcards, no cluster scope, a +ServiceAccount without token automount, and a manual, pinned Application. +""" +from pathlib import Path +import re + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +ADDON = ROOT / "argocd/platform-addons/nk-agent-status" +APP = ROOT / "argocd/railiance01/applications/nk-agent-status.application.yaml" + +READ_VERBS = {"get", "list", "watch"} +FORBIDDEN_RESOURCES = { + "secrets", "pods/log", "pods/exec", "pods/attach", "pods/portforward", "serviceaccounts/token", + "nodes/proxy", "services/proxy", "pods/proxy", "roles", "rolebindings", "clusterroles", "clusterrolebindings", +} +EXPECTED = { + ("sso", "batch", "jobs"): None, + ("sso", "", "pods"): None, + ("sso", "apps", "deployments"): None, + ("sso", "external-secrets.io", "externalsecrets"): None, + ("informed-decision", "", "configmaps"): ["informed-decision-workload-registry"], + ("argocd", "argoproj.io", "applications"): ["informed-decision"], +} +SUBJECT = [{"kind": "ServiceAccount", "name": "nk-agent-status", "namespace": "sso"}] + + +def _docs(): + kust = yaml.safe_load((ADDON / "kustomization.yaml").read_text()) + docs = [] + for res in kust["resources"]: + docs.extend(d for d in yaml.safe_load_all((ADDON / res).read_text()) if d) + return docs + + +def test_only_namespaced_kinds_no_secret(): + kinds = sorted(d["kind"] for d in _docs()) + assert kinds == ["Role"] * 3 + ["RoleBinding"] * 3 + ["ServiceAccount"] + for path in ADDON.iterdir(): + if path.is_file(): + assert "kind: Secret" not in path.read_text(), path + + +def test_service_account_without_automount(): + (sa,) = [d for d in _docs() if d["kind"] == "ServiceAccount"] + assert (sa["metadata"]["name"], sa["metadata"]["namespace"]) == ("nk-agent-status", "sso") + assert sa["automountServiceAccountToken"] is False + + +def test_roles_read_only_curated_named_outside_sso(): + granted = {} + for role in (d for d in _docs() if d["kind"] == "Role"): + ns = role["metadata"]["namespace"] + for rule in role["rules"]: + values = [*rule["verbs"], *rule["apiGroups"], *rule["resources"]] + assert not any("*" in v for v in values), rule + assert set(rule["verbs"]) <= READ_VERBS, rule + for group in rule["apiGroups"]: + for resource in rule["resources"]: + assert resource not in FORBIDDEN_RESOURCES, resource + granted[(ns, group, resource)] = rule.get("resourceNames") + assert granted == EXPECTED + + +def test_bindings_target_only_the_agent(): + for binding in (d for d in _docs() if d["kind"] == "RoleBinding"): + assert binding["subjects"] == SUBJECT + assert binding["roleRef"] == {"apiGroup": "rbac.authorization.k8s.io", "kind": "Role", + "name": "nk-agent-status"} + + +def test_application_manual_and_pinned(): + if not APP.exists(): + pytest.skip("Application not yet placed") + app = yaml.safe_load(APP.read_text()) + spec = app["spec"] + assert spec["project"] == "railiance-platform-addons" + assert spec["source"]["path"] == "argocd/platform-addons/nk-agent-status" + assert re.fullmatch(r"[0-9a-f]{40}", spec["source"]["targetRevision"]) + assert "automated" not in spec.get("syncPolicy", {}) + assert "finalizers" not in app["metadata"]