Add the scoped ops-mason-builder ClusterRole
Assistant: grok Assistant-Session: 01a1625d-476f-7373-9e10-956ff3ac4333
This commit is contained in:
parent
c27f50b00e
commit
bd6660a916
4 changed files with 144 additions and 0 deletions
29
argocd/platform-addons/ops-mason-builder/clusterrole.yaml
Normal file
29
argocd/platform-addons/ops-mason-builder/clusterrole.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Scoped ops-mason builder (founder decision 2026-10-22, MASON-IN-0006 (b)).
|
||||
# Kinds kubernetes_plane applies, plus get/list/watch on services for dependency
|
||||
# reads, plus create on self-subject access reviews so kubectl auth can-i works.
|
||||
# Excluded: pods, secrets, configmaps, serviceaccounts/token, impersonate,
|
||||
# roles, rolebindings, clusterroles, clusterrolebindings, nodes/proxy, pods/exec,
|
||||
# escalate, bind, wildcards.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: ops-mason-builder
|
||||
labels:
|
||||
app.kubernetes.io/part-of: railiance-gitops
|
||||
railiance-platform/component: ops-mason-builder
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["serviceaccounts"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: ["networkpolicies"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["authorization.k8s.io"]
|
||||
resources: ["selfsubjectaccessreviews", "selfsubjectrulesreviews"]
|
||||
verbs: ["create"]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: ops-mason-builder
|
||||
labels:
|
||||
app.kubernetes.io/part-of: railiance-gitops
|
||||
railiance-platform/component: ops-mason-builder
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: ops-mason-builder
|
||||
subjects:
|
||||
- kind: User
|
||||
name: ops-mason-builder
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
11
argocd/platform-addons/ops-mason-builder/kustomization.yaml
Normal file
11
argocd/platform-addons/ops-mason-builder/kustomization.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# MASON-IN-0006 choice (b), decision 30d29060-8d14-4a8e-9cde-34aa826eb8d1.
|
||||
# Scoped builder for ops-mason. User ops-mason-builder, no ServiceAccount token,
|
||||
# no Secret. Applied by argocd/railiance01/applications/ops-mason-builder.application.yaml.
|
||||
# Manual sync. targetRevision is pinned to the commit that introduced these manifests.
|
||||
|
||||
resources:
|
||||
- clusterrole.yaml
|
||||
- clusterrolebinding.yaml
|
||||
89
tests/test_ops_mason_builder_rbac.py
Normal file
89
tests/test_ops_mason_builder_rbac.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""Offline guard for the ops-mason builder identity (MASON-IN-0006 choice (b)).
|
||||
|
||||
Parses the GitOps manifests and asserts the founder's 2026-10-22 boundary:
|
||||
the five granted rules only, no forbidden resources, no escalate/bind/impersonate,
|
||||
no wildcards, and a User subject with no ServiceAccount.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ADDON = ROOT / "argocd/platform-addons/ops-mason-builder"
|
||||
|
||||
WRITE_VERBS = ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
READ_VERBS = ["get", "list", "watch"]
|
||||
FORBIDDEN_RESOURCES = {
|
||||
"secrets", "pods", "serviceaccounts/token", "configmaps", "roles",
|
||||
"rolebindings", "clusterroles", "clusterrolebindings", "nodes",
|
||||
"nodes/proxy", "pods/exec", "pods/log",
|
||||
}
|
||||
FORBIDDEN_VERBS = {"escalate", "bind", "impersonate"}
|
||||
EXPECTED_RULES = [
|
||||
{"apiGroups": [""], "resources": ["namespaces"], "verbs": WRITE_VERBS},
|
||||
{"apiGroups": [""], "resources": ["serviceaccounts"], "verbs": WRITE_VERBS},
|
||||
{"apiGroups": ["networking.k8s.io"], "resources": ["networkpolicies"],
|
||||
"verbs": WRITE_VERBS},
|
||||
{"apiGroups": [""], "resources": ["services"], "verbs": READ_VERBS},
|
||||
{"apiGroups": ["authorization.k8s.io"],
|
||||
"resources": ["selfsubjectaccessreviews", "selfsubjectrulesreviews"],
|
||||
"verbs": ["create"]},
|
||||
]
|
||||
|
||||
|
||||
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 _one(kind):
|
||||
found = [d for d in _docs() if d["kind"] == kind]
|
||||
assert len(found) == 1, kind
|
||||
return found[0]
|
||||
|
||||
|
||||
def test_only_expected_kinds_no_secret():
|
||||
kinds = sorted(d["kind"] for d in _docs())
|
||||
assert kinds == ["ClusterRole", "ClusterRoleBinding"]
|
||||
for path in ADDON.iterdir():
|
||||
if path.is_file():
|
||||
assert "kind: Secret" not in path.read_text(), path
|
||||
|
||||
|
||||
def test_cluster_role_exact_rules_no_wildcards():
|
||||
role = _one("ClusterRole")
|
||||
assert role["metadata"]["name"] == "ops-mason-builder"
|
||||
assert "aggregationRule" not in role
|
||||
assert role["rules"] == EXPECTED_RULES
|
||||
granted = set()
|
||||
for rule in role["rules"]:
|
||||
values = [*rule["verbs"], *rule.get("apiGroups", []), *rule.get("resources", [])]
|
||||
assert not any("*" in v for v in values), rule
|
||||
assert FORBIDDEN_VERBS.isdisjoint(rule["verbs"]), rule
|
||||
for resource in rule["resources"]:
|
||||
assert resource not in FORBIDDEN_RESOURCES, resource
|
||||
granted.add(resource)
|
||||
assert granted == {
|
||||
"namespaces", "serviceaccounts", "networkpolicies", "services",
|
||||
"selfsubjectaccessreviews", "selfsubjectrulesreviews",
|
||||
}
|
||||
by_resource = {r["resources"][0]: r["verbs"] for r in role["rules"] if len(r["resources"]) == 1}
|
||||
assert by_resource["namespaces"] == WRITE_VERBS
|
||||
assert by_resource["serviceaccounts"] == WRITE_VERBS
|
||||
assert by_resource["networkpolicies"] == WRITE_VERBS
|
||||
assert by_resource["services"] == READ_VERBS
|
||||
review = next(r for r in role["rules"] if "selfsubjectaccessreviews" in r["resources"])
|
||||
assert review["verbs"] == ["create"]
|
||||
assert review["resources"] == ["selfsubjectaccessreviews", "selfsubjectrulesreviews"]
|
||||
|
||||
|
||||
def test_binding_targets_only_the_user():
|
||||
binding = _one("ClusterRoleBinding")
|
||||
assert binding["roleRef"]["name"] == "ops-mason-builder"
|
||||
assert binding["roleRef"]["kind"] == "ClusterRole"
|
||||
assert binding["subjects"] == [{"kind": "User", "name": "ops-mason-builder",
|
||||
"apiGroup": "rbac.authorization.k8s.io"}]
|
||||
assert all(s["kind"] != "ServiceAccount" for s in binding["subjects"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue