#!/usr/bin/env python3 """Acceptance checks for what Hermes may and may not do to the cluster. Denials use Kubernetes authorization reviews only. Live denied attempts were removed because a policy regression could turn a supposed probe into a real credential read or mutation; the default harness never constructs such argv. """ from __future__ import annotations from hermes_handoff_catalog import ( FLUX_PROJECTION, OPERATOR, SELF, Targets, check, step, ) from hermes_handoff_model import REVIEW, CheckSpec def _access(targets: Targets) -> list[CheckSpec]: namespace = targets.namespace return [ check( "access.no-cluster-admin-binding-for-agent", "No ClusterRoleBinding grants the agent service account cluster-admin", "access-denied", "names_absent", [ step( "bindings", OPERATOR, "kubectl", "get", "clusterrolebindings", "-o", "name", ) ], {"step": "bindings", "contains": ("hermes-agent-cluster-admin",)}, rationale="The binding inventory and in-pod authorization reviews are independent desired/enforced views.", ), check( "access.secrets-are-denied", "Hermes cannot read Secrets cluster-wide", "access-denied", "review_denied", [ step( "review", SELF, "kubectl", "auth", "can-i", "get", "secrets", "--all-namespaces", kind=REVIEW, ) ], rationale="A live Secret request is never constructed, even when an RBAC regression would allow it.", ), check( "access.service-account-tokens-are-denied", "Hermes cannot mint a service-account token", "access-denied", "review_denied", [ step( "review-token", SELF, "kubectl", "auth", "can-i", "create", "serviceaccounts/token", "--namespace", namespace, kind=REVIEW, ), step( "review-serviceaccount", SELF, "kubectl", "auth", "can-i", "create", "serviceaccounts", "--namespace", namespace, kind=REVIEW, ), ], rationale="Token creation has no credential-safe live probe, so both relevant grants must be denied by review.", ), check( "access.impersonation-is-denied", "Hermes cannot impersonate another identity", "access-denied", "review_denied", [ step( "review", SELF, "kubectl", "auth", "can-i", "impersonate", "users", "--all-namespaces", kind=REVIEW, ) ], rationale="The default harness never constructs an impersonated request.", ), check( "access.workload-mutation-is-denied", "Hermes cannot mutate its own workload", "access-denied", "review_denied", [ step( "review", SELF, "kubectl", "auth", "can-i", "patch", "deployments", "--namespace", namespace, kind=REVIEW, ) ], rationale="Client-side and server-side dry-run mutation tricks are excluded structurally.", ), check( "access.pod-exec-is-denied", "Hermes cannot exec into an arbitrary pod", "access-denied", "review_denied", [ step( "review", SELF, "kubectl", "auth", "can-i", "create", "pods/exec", "--all-namespaces", kind=REVIEW, ) ], rationale="No cross-pod exec request is created by a read-only run.", ), check( "access.attach-portforward-and-kube-system-writes-are-denied", "Hermes holds no attach, port-forward, or control-plane write authority", "access-denied", "review_denied", [ step( "review-attach", SELF, "kubectl", "auth", "can-i", "create", "pods/attach", "--all-namespaces", kind=REVIEW, ), step( "review-portforward", SELF, "kubectl", "auth", "can-i", "create", "pods/portforward", "--all-namespaces", kind=REVIEW, ), step( "review-create", SELF, "kubectl", "auth", "can-i", "create", "configmaps", "--namespace", "kube-system", kind=REVIEW, ), ], rationale="Every authority is reviewed without issuing the request it would authorize.", ), check( "access.required-reads-are-allowed", "Hermes retains the reads its operator role depends on", "access-allowed", "allowed", [ step( "watch-review", SELF, "kubectl", "auth", "can-i", "watch", "pods", "--namespace", namespace, kind=REVIEW, ), step( "pods", SELF, "kubectl", "--namespace", namespace, "get", "pods", "-o", "name", ), step("namespaces", SELF, "kubectl", "get", "namespaces", "-o", "name"), step( "deployments", SELF, "kubectl", "--namespace", namespace, "get", "deployments", "-o", "name", ), step("nodes", SELF, "kubectl", "get", "nodes", "-o", "name"), ], ), check( "access.flux-and-helm-status-are-allowed", "Hermes can read Flux and Helm reconciliation status", "access-allowed", "allowed", [ step( "kustomizations", SELF, "kubectl", "get", "kustomizations.kustomize.toolkit.fluxcd.io", "--all-namespaces", "-o", "name", record=False, ), step( "helmreleases", SELF, "kubectl", "get", "helmreleases.helm.toolkit.fluxcd.io", "--all-namespaces", "-o", "name", record=False, ), ], ), check( "access.namespace-view-agrees-across-vantages", "The operator and in-pod vantages see the same namespace inventory", "access-allowed", "vantages_agree", [ step( "operator", OPERATOR, "kubectl", "get", "namespaces", "-o", "name", record=False, ), step( "self", SELF, "kubectl", "get", "namespaces", "-o", "name", record=False, ), ], {"steps": ("operator", "self")}, rationale="A disagreement here means one vantage is not seeing the cluster the other is certifying.", ), ] def _gitops(targets: Targets) -> list[CheckSpec]: return [ check( f"gitops.{kind.split('.')[0]}-reconcile-cleanly", f"No unexpected {label} suspension or unhealthy reconciliation", "gitops", "flux_health", [ step( "objects", OPERATOR, "kubectl", "get", kind, "--all-namespaces", "-o", FLUX_PROJECTION, record=False, ) ], {"step": "objects", "expected_suspensions": targets.expected_suspensions}, ) for kind, label in ( ("kustomizations.kustomize.toolkit.fluxcd.io", "Kustomization"), ("helmreleases.helm.toolkit.fluxcd.io", "HelmRelease"), ) ] def access_checks(targets: Targets) -> list[CheckSpec]: """Return the access-denied, access-allowed, and GitOps health checks.""" return [*_access(targets), *_gitops(targets)]