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 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 943911@bnt-lap001 Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0
This commit is contained in:
parent
31cf74d36b
commit
4e60f69f93
4 changed files with 193 additions and 0 deletions
12
argocd/platform-addons/nk-agent-status/kustomization.yaml
Normal file
12
argocd/platform-addons/nk-agent-status/kustomization.yaml
Normal file
|
|
@ -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
|
||||||
85
argocd/platform-addons/nk-agent-status/rbac.yaml
Normal file
85
argocd/platform-addons/nk-agent-status/rbac.yaml
Normal file
|
|
@ -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}
|
||||||
10
argocd/platform-addons/nk-agent-status/serviceaccount.yaml
Normal file
10
argocd/platform-addons/nk-agent-status/serviceaccount.yaml
Normal file
|
|
@ -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
|
||||||
86
tests/test_nk_agent_status_rbac.py
Normal file
86
tests/test_nk_agent_status_rbac.py
Normal file
|
|
@ -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"]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue