build: add guarded Kubernetes plane executor

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02878-7c21-7692-bcd6-ce2838c4b448
This commit is contained in:
tegwick 2026-08-22 11:23:38 +02:00
parent 1dd98b4f27
commit 2b318634b6
13 changed files with 1240 additions and 3 deletions

67
src/ops_mason/cli.py Normal file
View file

@ -0,0 +1,67 @@
"""Command-line surface for guarded ops-mason builders."""
from __future__ import annotations
import argparse
import json
import sys
from collections.abc import Sequence
from ops_mason.kubernetes_plane import (
PlaneBundle,
PlaneError,
apply,
preflight,
rollback_plan,
verify,
)
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="ops-mason")
families = parser.add_subparsers(dest="family", required=True)
plane = families.add_parser("plane", help="guarded Kubernetes security plane")
commands = plane.add_subparsers(dest="command", required=True)
for name in ("render", "preflight", "verify", "rollback-plan"):
command = commands.add_parser(name)
command.add_argument("--bundle", required=True)
apply_parser = commands.add_parser("apply")
apply_parser.add_argument("--bundle", required=True)
apply_parser.add_argument("--confirm", required=True, help="exact approved plan id")
apply_parser.add_argument(
"--expect-digest", required=True, help="exact digest returned by preflight"
)
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = _parser().parse_args(argv)
try:
bundle = PlaneBundle.load(args.bundle)
if args.command == "render":
result = bundle.render()
elif args.command == "preflight":
result = preflight(bundle)
elif args.command == "verify":
result = verify(bundle)
elif args.command == "rollback-plan":
result = rollback_plan(bundle)
elif args.command == "apply":
result = apply(
bundle,
confirm_plan_id=args.confirm,
expected_digest=args.expect_digest,
)
else: # pragma: no cover - argparse enforces the command set
raise AssertionError(args.command)
except (OSError, PlaneError) as exc:
print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
return 2
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())

View file

@ -0,0 +1,606 @@
"""Guarded construction of small, source-pinned Kubernetes security planes.
The executor is deliberately narrower than a generic ``kubectl apply`` wrapper.
It accepts an allowlisted bundle, refuses unexpected or secret-bearing objects,
requires an approved construction plan and an exact bundle digest for writes,
and records metadata-only evidence.
"""
from __future__ import annotations
import hashlib
import json
import subprocess
from collections.abc import Callable, Mapping, Sequence
from dataclasses import asdict, dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
from ops_mason.plan import ConstructionPlan
class PlaneError(RuntimeError):
"""The bundle or live plane is invalid."""
class PlaneRefused(PlaneError):
"""A safety or approval gate refused a mutating action."""
@dataclass(frozen=True)
class CommandResult:
returncode: int
stdout: str = ""
stderr: str = ""
Runner = Callable[[Sequence[str]], CommandResult]
def subprocess_runner(args: Sequence[str]) -> CommandResult:
proc = subprocess.run(
list(args), capture_output=True, text=True, timeout=30, check=False
)
return CommandResult(proc.returncode, proc.stdout, proc.stderr)
@dataclass(frozen=True, order=True)
class ObjectRef:
api_version: str
kind: str
name: str
namespace: str | None
resource: str
@property
def display(self) -> str:
scope = f"{self.namespace}/" if self.namespace else ""
return f"{self.kind}/{scope}{self.name}"
@dataclass(frozen=True)
class ManifestEntry:
path: Path
source_path: str
sha256: str
@dataclass(frozen=True)
class Dependency:
resource: str
name: str
namespace: str | None
assertions: tuple[tuple[str, Any], ...]
@dataclass
class PlaneBundle:
id: str
path: Path
repo_root: Path
plan_path: Path
expected_context: str
expected_namespace: str
source_repo: str
source_revision: str
implementation_revision: str
manifests: tuple[ManifestEntry, ...]
allowed_objects: tuple[ObjectRef, ...]
forbidden_kinds: frozenset[str]
dependencies: tuple[Dependency, ...]
evidence_path: Path
documents: tuple[dict[str, Any], ...]
@classmethod
def load(cls, path: str | Path) -> "PlaneBundle":
bundle_path = Path(path).resolve()
try:
raw = yaml.safe_load(bundle_path.read_text()) or {}
except (OSError, yaml.YAMLError) as exc:
raise PlaneError(f"cannot load bundle {bundle_path}: {exc}") from exc
if not isinstance(raw, Mapping):
raise PlaneError("bundle root must be a mapping")
required = {
"id",
"plan",
"expected_context",
"expected_namespace",
"source_repo",
"source_revision",
"implementation_revision",
"manifests",
"allowed_objects",
"evidence_path",
}
missing = required - set(raw)
if missing:
raise PlaneError(f"bundle missing field(s): {', '.join(sorted(missing))}")
repo_root = bundle_path.parent.parent.resolve()
def local_path(value: str) -> Path:
candidate = (bundle_path.parent / value).resolve()
if not candidate.is_relative_to(repo_root):
raise PlaneRefused(f"bundle path escapes repository: {value}")
return candidate
manifests: list[ManifestEntry] = []
documents: list[dict[str, Any]] = []
for item in raw["manifests"]:
if not isinstance(item, Mapping) or not {"path", "source_path", "sha256"} <= set(item):
raise PlaneError("each manifest needs path, source_path, and sha256")
manifest_path = local_path(str(item["path"]))
content = manifest_path.read_bytes()
actual_hash = hashlib.sha256(content).hexdigest()
expected_hash = str(item["sha256"])
if actual_hash != expected_hash:
raise PlaneRefused(
f"manifest digest mismatch for {manifest_path}: "
f"expected {expected_hash}, got {actual_hash}"
)
manifests.append(
ManifestEntry(manifest_path, str(item["source_path"]), expected_hash)
)
try:
loaded = list(yaml.safe_load_all(content.decode()))
except (UnicodeDecodeError, yaml.YAMLError) as exc:
raise PlaneError(f"invalid YAML in {manifest_path}: {exc}") from exc
for doc in loaded:
if not isinstance(doc, dict):
raise PlaneError(f"{manifest_path}: every YAML document must be an object")
documents.append(doc)
allowed = tuple(_parse_ref(item) for item in raw["allowed_objects"])
dependencies = tuple(_parse_dependency(item) for item in raw.get("dependencies", []))
bundle = cls(
id=str(raw["id"]),
path=bundle_path,
repo_root=repo_root,
plan_path=local_path(str(raw["plan"])),
expected_context=str(raw["expected_context"]),
expected_namespace=str(raw["expected_namespace"]),
source_repo=str(raw["source_repo"]),
source_revision=str(raw["source_revision"]),
implementation_revision=str(raw["implementation_revision"]),
manifests=tuple(manifests),
allowed_objects=allowed,
forbidden_kinds=frozenset(str(x) for x in raw.get("forbidden_kinds", ["Pod", "Secret"])),
dependencies=dependencies,
evidence_path=local_path(str(raw["evidence_path"])),
documents=tuple(documents),
)
bundle.validate()
return bundle
@property
def digest(self) -> str:
digest = hashlib.sha256()
digest.update(self.path.read_bytes())
for manifest in self.manifests:
digest.update(manifest.path.read_bytes())
return digest.hexdigest()
def validate(self) -> None:
actual_refs = tuple(_document_ref(doc, self.allowed_objects) for doc in self.documents)
if len(set(actual_refs)) != len(actual_refs):
raise PlaneRefused("bundle contains duplicate Kubernetes object identities")
if set(actual_refs) != set(self.allowed_objects):
unexpected = sorted(set(actual_refs) - set(self.allowed_objects))
missing = sorted(set(self.allowed_objects) - set(actual_refs))
raise PlaneRefused(
"object allowlist mismatch: "
f"unexpected={[x.display for x in unexpected]}, "
f"missing={[x.display for x in missing]}"
)
for doc, ref in zip(self.documents, actual_refs, strict=True):
if ref.kind in self.forbidden_kinds:
raise PlaneRefused(f"forbidden Kubernetes kind: {ref.kind}")
forbidden_keys = sorted(_find_keys(doc, {"data", "stringData"}))
if forbidden_keys:
raise PlaneRefused(
f"secret-bearing key(s) forbidden in {ref.display}: {forbidden_keys}"
)
if ref.namespace and ref.namespace != self.expected_namespace:
raise PlaneRefused(
f"{ref.display} escapes expected namespace {self.expected_namespace}"
)
if ref.kind == "ServiceAccount":
if doc.get("automountServiceAccountToken") is not False:
raise PlaneRefused(
f"{ref.display} must set automountServiceAccountToken: false"
)
if doc.get("secrets") or doc.get("imagePullSecrets"):
raise PlaneRefused(f"{ref.display} must not reference credentials")
if ref.kind == "Namespace" and ref.name == self.expected_namespace:
labels = doc.get("metadata", {}).get("labels", {})
if labels.get("pod-security.kubernetes.io/enforce") != "restricted":
raise PlaneRefused(
f"Namespace/{ref.name} must enforce restricted Pod Security"
)
def plan(self) -> ConstructionPlan:
return ConstructionPlan.load(self.plan_path)
def render(self) -> dict[str, Any]:
return {
"bundle_id": self.id,
"bundle_digest": self.digest,
"source_repo": self.source_repo,
"source_revision": self.source_revision,
"implementation_revision": self.implementation_revision,
"expected_context": self.expected_context,
"objects": [asdict(ref) | {"display": ref.display} for ref in self.allowed_objects],
"forbidden_kinds": sorted(self.forbidden_kinds),
"plan_id": self.plan().id,
"plan_approved": self.plan().is_approved(),
}
def _parse_ref(item: Mapping[str, Any]) -> ObjectRef:
required = {"api_version", "kind", "name", "resource"}
if not isinstance(item, Mapping) or not required <= set(item):
raise PlaneError("allowed object needs api_version, kind, name, and resource")
return ObjectRef(
str(item["api_version"]),
str(item["kind"]),
str(item["name"]),
str(item["namespace"]) if item.get("namespace") else None,
str(item["resource"]),
)
def _parse_dependency(item: Mapping[str, Any]) -> Dependency:
if not isinstance(item, Mapping) or not {"resource", "name"} <= set(item):
raise PlaneError("dependency needs resource and name")
assertions: list[tuple[str, Any]] = []
for assertion in item.get("assertions", []):
if not isinstance(assertion, Mapping) or not {"path", "equals"} <= set(assertion):
raise PlaneError("dependency assertion needs path and equals")
assertions.append((str(assertion["path"]), assertion["equals"]))
return Dependency(
resource=str(item["resource"]),
name=str(item["name"]),
namespace=str(item["namespace"]) if item.get("namespace") else None,
assertions=tuple(assertions),
)
def _document_ref(doc: Mapping[str, Any], allowed: Sequence[ObjectRef]) -> ObjectRef:
metadata = doc.get("metadata")
if not isinstance(metadata, Mapping) or not metadata.get("name"):
raise PlaneError("Kubernetes object missing metadata.name")
identity = (
str(doc.get("apiVersion", "")),
str(doc.get("kind", "")),
str(metadata["name"]),
str(metadata["namespace"]) if metadata.get("namespace") else None,
)
matches = [ref for ref in allowed if identity == (ref.api_version, ref.kind, ref.name, ref.namespace)]
if len(matches) == 1:
return matches[0]
resource = f"unmapped:{identity[1].lower()}"
return ObjectRef(*identity, resource)
def _find_keys(value: Any, forbidden: set[str], prefix: str = "") -> list[str]:
found: list[str] = []
if isinstance(value, Mapping):
for key, child in value.items():
path = f"{prefix}.{key}" if prefix else str(key)
if key in forbidden:
found.append(path)
found.extend(_find_keys(child, forbidden, path))
elif isinstance(value, list):
for index, child in enumerate(value):
found.extend(_find_keys(child, forbidden, f"{prefix}.{index}"))
return found
def _run(runner: Runner, args: Sequence[str], *, allow_not_found: bool = False) -> CommandResult:
result = runner(args)
if result.returncode == 0:
return result
if allow_not_found and "notfound" in result.stderr.lower().replace(" ", ""):
return result
command = " ".join(args)
raise PlaneError(f"`{command}` failed: {result.stderr.strip()[:400]}")
def _kubectl_get_args(resource: str, name: str, namespace: str | None) -> list[str]:
args = ["kubectl"]
if namespace:
args += ["-n", namespace]
return args + ["get", resource, name, "-o", "json"]
def _json_path(value: Any, path: str) -> Any:
current = value
if path.startswith("/"):
tokens = [token.replace("~1", "/").replace("~0", "~") for token in path[1:].split("/")]
else:
tokens = path.split(".")
for token in tokens:
if isinstance(current, list):
try:
current = current[int(token)]
except (ValueError, IndexError) as exc:
raise PlaneError(f"invalid dependency assertion path: {path}") from exc
elif isinstance(current, Mapping) and token in current:
current = current[token]
else:
raise PlaneError(f"dependency assertion path not found: {path}")
return current
def _is_subset(desired: Any, live: Any) -> bool:
if isinstance(desired, Mapping):
return isinstance(live, Mapping) and all(
key in live and _is_subset(value, live[key]) for key, value in desired.items()
)
if isinstance(desired, list):
return isinstance(live, list) and len(desired) == len(live) and all(
_is_subset(left, right) for left, right in zip(desired, live, strict=True)
)
return desired == live
def _desired_by_ref(bundle: PlaneBundle) -> dict[ObjectRef, dict[str, Any]]:
return {
_document_ref(doc, bundle.allowed_objects): doc for doc in bundle.documents
}
def _check_inputs_clean(bundle: PlaneBundle, runner: Runner) -> None:
result = _run(
runner,
["git", "-C", str(bundle.repo_root), "status", "--porcelain"],
)
if result.stdout.strip():
raise PlaneRefused("repository must be committed and clean before Kubernetes mutation")
def preflight(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[str, Any]:
context = _run(runner, ["kubectl", "config", "current-context"]).stdout.strip()
if context != bundle.expected_context:
raise PlaneRefused(
f"Kubernetes context mismatch: expected {bundle.expected_context!r}, got {context!r}"
)
for ref in bundle.allowed_objects:
args = ["kubectl", "auth", "can-i", "create", ref.resource]
if ref.namespace:
args += ["-n", ref.namespace]
allowed = _run(runner, args).stdout.strip().lower()
if allowed != "yes":
raise PlaneRefused(f"create permission denied for {ref.display}")
for manifest in bundle.manifests:
_run(runner, ["kubectl", "apply", "--dry-run=client", "-f", str(manifest.path)])
desired = _desired_by_ref(bundle)
live_state: list[dict[str, str]] = []
namespace_exists = False
for ref in bundle.allowed_objects:
result = _run(
runner,
_kubectl_get_args(ref.resource, ref.name, ref.namespace),
allow_not_found=True,
)
if result.returncode != 0:
state = "absent"
else:
live = json.loads(result.stdout)
state = "exact" if _is_subset(desired[ref], live) else "drift"
if state == "drift":
raise PlaneRefused(f"unmanaged live drift for {ref.display}")
if ref.kind == "Namespace" and ref.name == bundle.expected_namespace:
namespace_exists = True
live_state.append({"object": ref.display, "state": state})
server_validated: list[str] = []
for manifest in bundle.manifests:
docs = list(yaml.safe_load_all(manifest.path.read_text()))
namespaced = any(doc.get("metadata", {}).get("namespace") for doc in docs)
if namespaced and not namespace_exists:
continue
_run(
runner,
[
"kubectl",
"apply",
"--server-side",
"--dry-run=server",
"--field-manager=ops-mason",
"-f",
str(manifest.path),
],
)
server_validated.append(str(manifest.path.relative_to(bundle.repo_root)))
dependency_evidence: list[dict[str, Any]] = []
for dependency in bundle.dependencies:
result = _run(
runner,
_kubectl_get_args(dependency.resource, dependency.name, dependency.namespace),
)
live = json.loads(result.stdout)
checked: dict[str, Any] = {}
for path, expected in dependency.assertions:
actual = _json_path(live, path)
if actual != expected:
raise PlaneRefused(
f"dependency {dependency.resource}/{dependency.name} {path}: "
f"expected {expected!r}, got {actual!r}"
)
checked[path] = actual
dependency_evidence.append(
{
"resource": dependency.resource,
"namespace": dependency.namespace,
"name": dependency.name,
"assertions": checked,
}
)
return {
"ok": True,
"bundle_id": bundle.id,
"bundle_digest": bundle.digest,
"context": context,
"plan_id": bundle.plan().id,
"plan_approved": bundle.plan().is_approved(),
"objects": live_state,
"client_validated_manifests": [
str(item.path.relative_to(bundle.repo_root)) for item in bundle.manifests
],
"server_validated_manifests": server_validated,
"deferred_server_validation": [
str(item.path.relative_to(bundle.repo_root))
for item in bundle.manifests
if str(item.path.relative_to(bundle.repo_root)) not in server_validated
],
"dependencies": dependency_evidence,
}
def verify(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[str, Any]:
desired = _desired_by_ref(bundle)
objects: list[dict[str, Any]] = []
for ref in bundle.allowed_objects:
result = _run(runner, _kubectl_get_args(ref.resource, ref.name, ref.namespace))
live = json.loads(result.stdout)
if not _is_subset(desired[ref], live):
raise PlaneError(f"live object does not match bundle: {ref.display}")
metadata = live.get("metadata", {})
objects.append(
{
"object": ref.display,
"uid": metadata.get("uid"),
"resource_version": metadata.get("resourceVersion"),
"generation": metadata.get("generation"),
}
)
negative_scope: dict[str, int] = {}
for resource in ("pods", "secrets"):
result = _run(
runner,
["kubectl", "-n", bundle.expected_namespace, "get", resource, "-o", "json"],
)
count = len(json.loads(result.stdout).get("items", []))
if count:
raise PlaneError(
f"negative-scope check failed: {count} {resource} exist in "
f"{bundle.expected_namespace}"
)
negative_scope[resource] = count
return {
"ok": True,
"bundle_id": bundle.id,
"bundle_digest": bundle.digest,
"context": bundle.expected_context,
"objects": objects,
"negative_scope": negative_scope,
}
def rollback_plan(bundle: PlaneBundle) -> dict[str, Any]:
namespaced = [ref for ref in bundle.allowed_objects if ref.namespace]
namespaces = [ref for ref in bundle.allowed_objects if ref.kind == "Namespace"]
commands = [
" ".join(
["kubectl", "-n", ref.namespace or "", "delete", ref.resource, ref.name]
)
for ref in namespaced
]
namespace_commands = [
f"kubectl delete {ref.resource} {ref.name}" for ref in namespaces
]
return {
"bundle_id": bundle.id,
"object_scoped_commands": commands,
"inventory_before_namespace_delete": [
f"kubectl -n {bundle.expected_namespace} get all,configmaps,secrets,serviceaccounts,networkpolicies"
],
"conditional_namespace_commands": namespace_commands,
"warning": "Delete the namespace only after proving it contains no later or unrelated objects.",
}
def apply(
bundle: PlaneBundle,
*,
confirm_plan_id: str,
expected_digest: str,
runner: Runner = subprocess_runner,
) -> dict[str, Any]:
plan = bundle.plan()
if not plan.is_approved():
raise PlaneRefused(
f"plan {plan.id!r} is not approved; refusing Kubernetes mutation"
)
if confirm_plan_id != plan.id:
raise PlaneRefused(
f"confirmation must exactly match approved plan id {plan.id!r}"
)
if expected_digest != bundle.digest:
raise PlaneRefused(
f"bundle digest confirmation mismatch: expected {bundle.digest}"
)
_check_inputs_clean(bundle, runner)
before = preflight(bundle, runner)
for manifest in bundle.manifests:
docs = list(yaml.safe_load_all(manifest.path.read_text()))
namespaced = any(doc.get("metadata", {}).get("namespace") for doc in docs)
if namespaced:
_run(
runner,
[
"kubectl",
"apply",
"--server-side",
"--dry-run=server",
"--field-manager=ops-mason",
"-f",
str(manifest.path),
],
)
_run(
runner,
[
"kubectl",
"apply",
"--server-side",
"--field-manager=ops-mason",
"-f",
str(manifest.path),
],
)
verified = verify(bundle, runner)
evidence = {
"schema_version": "ops-mason.kubernetes-plane-evidence/v1",
"recorded_at": datetime.now(UTC).isoformat(),
"plan": {
"id": plan.id,
"approved_by": plan.approved_by,
"approved_at": plan.approved_at,
},
"source": {
"repo": bundle.source_repo,
"revision": bundle.source_revision,
"implementation_revision": bundle.implementation_revision,
},
"preflight": before,
"verification": verified,
"rollback": rollback_plan(bundle),
}
bundle.evidence_path.parent.mkdir(parents=True, exist_ok=True)
bundle.evidence_path.write_text(json.dumps(evidence, indent=2, sort_keys=True) + "\n")
return evidence