340 lines
10 KiB
Python
340 lines
10 KiB
Python
#!/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)]
|