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

View file

@ -19,3 +19,13 @@ so that ops-warden always has something real to route to.
See `INTENT.md` for the full responsibility boundary against ops-warden,
OpenBao, flex-auth, and key-cape.
## Guarded Kubernetes planes
Small Kubernetes security foundations can use the fail-closed `ops-mason
plane` workflow. It pins source manifests and object identities, validates
cluster context/RBAC/dependencies/drift, requires an approved construction
plan plus exact digest for apply, and produces metadata-only evidence and a
non-executing rollback plan.
See [docs/kubernetes-plane.md](docs/kubernetes-plane.md).

View file

@ -0,0 +1,61 @@
schema_version: ops-mason.kubernetes-plane/v1
id: whitehat-foundational-plane
plan: ../plans/whitehat-foundational-plane.md
expected_context: default
expected_namespace: whitehat
source_repo: whitehat-security
source_revision: 4882c2d47a826a305d5c4e05aa7fcc1252c3887e
implementation_revision: 95129d7a35c8999be359b163b554853c9c6afa3a
evidence_path: ../docs/evidence/whitehat-foundational-plane.json
forbidden_kinds:
- Pod
- Secret
manifests:
- path: ../manifests/whitehat-plane/namespace.yaml
source_path: plane/namespace.yaml
sha256: a0919aa0c3d5f92844eeea3834fc684bc6ad112a3c4419be7f759f6b31c1d7a8
- path: ../manifests/whitehat-plane/network-policy.yaml
source_path: plane/network-policy.yaml
sha256: 05097e33221f93f4d82da7c4d5bb688c5ec5807dc15ab4378263cddad6031610
- path: ../manifests/whitehat-plane/service-account.yaml
source_path: plane/service-account.yaml
sha256: 42cb5276ea2f0a555ea835fed5c14397fe76fbfa18b1acec1f3168bb1db54a5d
allowed_objects:
- api_version: v1
kind: Namespace
resource: namespaces
name: whitehat
- api_version: networking.k8s.io/v1
kind: NetworkPolicy
resource: networkpolicies.networking.k8s.io
namespace: whitehat
name: default-deny
- api_version: networking.k8s.io/v1
kind: NetworkPolicy
resource: networkpolicies.networking.k8s.io
namespace: whitehat
name: allow-audit-core-e2
- api_version: v1
kind: ServiceAccount
resource: serviceaccounts
namespace: whitehat
name: whitehat-runner
dependencies:
- resource: services
namespace: audit-core
name: audit-core
assertions:
- path: /spec/ports/0/port
equals: 8080
- resource: networkpolicies.networking.k8s.io
namespace: audit-core
name: audit-core-whitehat-ingress
assertions:
- path: /spec/ingress/0/ports/0/port
equals: 8080
- path: /spec/ingress/0/from/0/namespaceSelector/matchLabels/kubernetes.io~1metadata.name
equals: whitehat
- path: /spec/ingress/0/from/0/podSelector/matchLabels/whitehat.security~1plane
equals: "true"
- path: /spec/ingress/0/from/0/podSelector/matchLabels/whitehat.security~1target
equals: audit-core

48
docs/kubernetes-plane.md Normal file
View file

@ -0,0 +1,48 @@
# Guarded Kubernetes plane automation
`ops-mason plane` constructs small security foundations from an immutable,
reviewed bundle. It is not a general Kubernetes deployment wrapper.
## Safety model
Every bundle declares its exact object identities, source revisions, expected
cluster context, namespace, dependency assertions, forbidden kinds, manifest
hashes, approved construction plan, and evidence destination.
Read-only commands (`render`, `preflight`, `verify`, `rollback-plan`) do not
require approval. `apply` refuses unless all of these hold:
1. the construction plan is `status: approved` and names its approver/date;
2. `--confirm` exactly matches the plan ID;
3. `--expect-digest` exactly matches the freshly rendered bundle digest;
4. the repository is committed and clean;
5. the Kubernetes context, create RBAC, live-object drift, dependency
assertions, client validation, and available server validation pass;
6. the bundle contains exactly the allowlisted objects, no Pod or Secret, no
`data`/`stringData`, no cross-namespace object, and no token-bearing
ServiceAccount.
The executor applies one pinned manifest at a time with server-side apply and
field manager `ops-mason`. Namespaced manifests receive server dry-run after
the Namespace exists and before they are persisted. A failed partial apply is
reported; the executor does not automatically delete a Namespace.
Successful apply immediately performs metadata-only verification, checks that
no Pod or Secret exists in the plane namespace, and writes JSON evidence with
object UIDs/resource versions and generated rollback commands. It never reads
or records a Secret value.
## Commands
```bash
ops-mason plane render --bundle bundles/<id>.yaml
ops-mason plane preflight --bundle bundles/<id>.yaml
ops-mason plane apply --bundle bundles/<id>.yaml \
--confirm <approved-plan-id> \
--expect-digest <digest-from-preflight>
ops-mason plane verify --bundle bundles/<id>.yaml
ops-mason plane rollback-plan --bundle bundles/<id>.yaml
```
Rollback output is a plan, never an action. Review live inventory immediately
before using it.

View file

@ -0,0 +1,31 @@
# Whitehat foundational plane source pins
These three YAML files are byte-for-byte pins of the selected
`whitehat-security` contract files at coordination revision
`4882c2d47a826a305d5c4e05aa7fcc1252c3887e`; the implementation revision is
`95129d7a35c8999be359b163b554853c9c6afa3a`.
The preserved `CONTRACT ONLY` header means the source repository does not own
or perform the apply. In this repository, only the guarded bundle executor may
apply them, after validating `bundles/whitehat-foundational-plane.yaml` and the
approved construction plan. Do not invoke `kubectl apply` directly on this
directory.
Pinned SHA-256 digests:
- `namespace.yaml`: `a0919aa0c3d5f92844eeea3834fc684bc6ad112a3c4419be7f759f6b31c1d7a8`
- `network-policy.yaml`: `05097e33221f93f4d82da7c4d5bb688c5ec5807dc15ab4378263cddad6031610`
- `service-account.yaml`: `42cb5276ea2f0a555ea835fed5c14397fe76fbfa18b1acec1f3168bb1db54a5d`
Excluded unconditionally: runner Pod, Secret, credential, projected identity,
engagement lease, and traffic.
Rollback is generated by:
```bash
ops-mason plane rollback-plan \
--bundle bundles/whitehat-foundational-plane.yaml
```
It never executes deletion. Namespace deletion remains conditional on a fresh
inventory proving that no later or unrelated object entered the namespace.

View file

@ -0,0 +1,9 @@
# CONTRACT ONLY. Do not apply from this repository.
apiVersion: v1
kind: Namespace
metadata:
name: whitehat
labels:
app.kubernetes.io/name: whitehat-security
whitehat.security/plane: "true"
pod-security.kubernetes.io/enforce: restricted

View file

@ -0,0 +1,40 @@
# CONTRACT ONLY. Do not apply from this repository.
# Default deny in namespace whitehat; live E2 adds a named egress rule per
# registered target. This example names audit-core only as the currently
# applicable live E2 target.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: whitehat
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-audit-core-e2
namespace: whitehat
spec:
podSelector:
matchLabels:
whitehat.security/target: audit-core
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: audit-core
ports:
- protocol: TCP
port: 8080
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53

View file

@ -0,0 +1,9 @@
# CONTRACT ONLY. Do not apply from this repository.
apiVersion: v1
kind: ServiceAccount
metadata:
name: whitehat-runner
namespace: whitehat
labels:
app.kubernetes.io/name: whitehat-security
automountServiceAccountToken: false

View file

@ -0,0 +1,87 @@
---
id: whitehat-foundational-plane
demand_source: "statehub-message:311a274e-9434-4fc3-bc52-8ea15ca68274"
consumer_repo: whitehat-security
credential_type: kubernetes-foundational-plane
status: approved
approved_by: "Bernd Worsch"
approved_at: "2026-08-22"
created: "2026-08-22"
updated: "2026-08-22"
---
# Construction plan: Whitehat foundational plane
## 1. Demand
Provision the non-running Kubernetes foundation requested by
`whitehat-security` and confirmed by `audit-core`: Namespace `whitehat`, a
default deny boundary, exact audit-core/DNS egress for future labelled runner
Pods, and a no-token `whitehat-runner` ServiceAccount. This is foundation for a
later governed engagement, not an engagement or packet itself.
## 2. Existing-structure survey
Read-only inspection on 2026-08-22 found no `whitehat` Namespace. The active
Kubernetes identity can create all four object types. Audit-core Service
`audit-core` listens on TCP 8080 and its `audit-core-whitehat-ingress` policy
already admits only Namespace `whitehat` Pods labelled both
`whitehat.security/plane=true` and `whitehat.security/target=audit-core`.
The selected Whitehat manifests entered at implementation revision `95129d7`
and are unchanged at requested coordination revision `4882c2d`. Reuse those
objects exactly; do not design a parallel policy shape.
## 3. Proposed changes
| # | Action | Object | Reason |
|---|---|---|---|
| 1 | create | Namespace `whitehat` | Dedicated restricted security boundary |
| 2 | create | NetworkPolicy `default-deny` | Deny ingress and egress by default |
| 3 | create | NetworkPolicy `allow-audit-core-e2` | Exact future-runner egress to audit-core:8080 and DNS only |
| 4 | create | ServiceAccount `whitehat-runner` | Stable name with token automount disabled |
No Pod, Secret, credential, projected identity, custody lease, engagement ID,
or target traffic is created. The two 2026-08-21 engagement IDs remain
cancelled and are never reused.
## 4. Review notes
- **Reuse:** source and target policies already agree; no redundant policy is
introduced.
- **Scope:** the egress allow selects only future Pods labelled for audit-core.
- **Identity:** the ServiceAccount cannot receive an automatic API token.
- **Reversal:** delete the two policies and ServiceAccount; delete the
Namespace only after proving it remains foundation-only.
- **Automation:** apply is gated by this approval, an immutable bundle digest,
exact object allowlist, committed inputs, current context, RBAC, dependency
assertions, server dry-run, and metadata-only verification.
## 5. Executive summary
**One-line ask:** create a dormant, restricted Whitehat namespace boundary that
can later host a separately approved audit-core E2 runner.
**Who/what gets access:** no running workload gets access now. A future Pod must
carry the exact audit-core target label before the egress allow selects it.
**To what:** TCP 8080 in Namespace `audit-core`, plus TCP/UDP 53 in
`kube-system`. All other ingress and egress remains denied.
**For how long:** the foundation is standing infrastructure. Any future
credential or runner remains separately time-bounded by its engagement.
**Blast radius:** a wrongly labelled future Pod could reach audit-core:8080 and
DNS, but no other namespace/port through these policies. This plan creates no
credential and sends no request.
**Cost to reverse:** delete three namespaced objects, inventory the Namespace,
then delete it only if it contains no later or unrelated resources.
**Decision:** approved by Bernd Worsch on 2026-08-22 via the explicit operator
instruction to implement MASON-WP-0002. Approval is limited to the four objects
above and does not extend to a runner, credential, engagement, or traffic.
## 6. Build result
Pending guarded apply and metadata-only verification.

View file

@ -11,6 +11,9 @@ dependencies = [
"PyYAML>=6.0",
]
[project.scripts]
ops-mason = "ops_mason.cli:main"
[project.optional-dependencies]
dev = ["pytest>=8"]

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

View file

@ -0,0 +1,258 @@
import hashlib
import json
from pathlib import Path
import pytest
import yaml
from ops_mason.kubernetes_plane import (
CommandResult,
PlaneBundle,
PlaneRefused,
_json_path,
apply,
preflight,
rollback_plan,
)
ROOT = Path(__file__).resolve().parents[1]
def test_whitehat_bundle_is_exactly_four_allowlisted_objects() -> None:
bundle = PlaneBundle.load(ROOT / "bundles/whitehat-foundational-plane.yaml")
assert bundle.digest
assert [ref.display for ref in bundle.allowed_objects] == [
"Namespace/whitehat",
"NetworkPolicy/whitehat/default-deny",
"NetworkPolicy/whitehat/allow-audit-core-e2",
"ServiceAccount/whitehat/whitehat-runner",
]
assert bundle.plan().is_approved()
assert {doc["kind"] for doc in bundle.documents} == {
"Namespace",
"NetworkPolicy",
"ServiceAccount",
}
def _fixture(tmp_path: Path, *, approved: bool = True, kind: str = "Namespace") -> Path:
(tmp_path / "bundles").mkdir()
(tmp_path / "manifests").mkdir()
(tmp_path / "plans").mkdir()
manifest = tmp_path / "manifests/plane.yaml"
if kind == "Namespace":
document = {
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "whitehat",
"labels": {"pod-security.kubernetes.io/enforce": "restricted"},
},
}
resource = "namespaces"
namespace = None
else:
document = {
"apiVersion": "v1",
"kind": kind,
"metadata": {"name": "forbidden", "namespace": "whitehat"},
}
resource = kind.lower() + "s"
namespace = "whitehat"
manifest.write_text(yaml.safe_dump(document, sort_keys=False))
digest = hashlib.sha256(manifest.read_bytes()).hexdigest()
plan = tmp_path / "plans/plane.md"
status = "approved" if approved else "reviewed"
approval = 'approved_by: "Bernd"\napproved_at: "2026-08-22"\n' if approved else ""
plan.write_text(f"---\nid: plane\nstatus: {status}\n{approval}---\n# Plan\n")
descriptor = {
"schema_version": "ops-mason.kubernetes-plane/v1",
"id": "plane",
"plan": "../plans/plane.md",
"expected_context": "default",
"expected_namespace": "whitehat",
"source_repo": "whitehat-security",
"source_revision": "abc",
"implementation_revision": "def",
"evidence_path": "../evidence/plane.json",
"forbidden_kinds": ["Pod", "Secret"],
"manifests": [
{
"path": "../manifests/plane.yaml",
"source_path": "plane.yaml",
"sha256": digest,
}
],
"allowed_objects": [
{
"api_version": "v1",
"kind": kind,
"resource": resource,
"namespace": namespace,
"name": document["metadata"]["name"],
}
],
}
bundle_path = tmp_path / "bundles/plane.yaml"
bundle_path.write_text(yaml.safe_dump(descriptor, sort_keys=False))
return bundle_path
def test_bundle_refuses_forbidden_pod_even_when_allowlisted(tmp_path: Path) -> None:
with pytest.raises(PlaneRefused, match="forbidden Kubernetes kind"):
PlaneBundle.load(_fixture(tmp_path, kind="Pod"))
def test_bundle_refuses_manifest_hash_drift(tmp_path: Path) -> None:
bundle_path = _fixture(tmp_path)
manifest = tmp_path / "manifests/plane.yaml"
manifest.write_text(manifest.read_text() + "# drift\n")
with pytest.raises(PlaneRefused, match="digest mismatch"):
PlaneBundle.load(bundle_path)
def test_bundle_refuses_secret_bearing_key_even_on_other_kind(tmp_path: Path) -> None:
bundle_path = _fixture(tmp_path)
manifest = tmp_path / "manifests/plane.yaml"
document = yaml.safe_load(manifest.read_text())
document["stringData"] = {"token": "must-never-enter-mason"}
manifest.write_text(yaml.safe_dump(document, sort_keys=False))
descriptor = yaml.safe_load(bundle_path.read_text())
descriptor["manifests"][0]["sha256"] = hashlib.sha256(manifest.read_bytes()).hexdigest()
bundle_path.write_text(yaml.safe_dump(descriptor, sort_keys=False))
with pytest.raises(PlaneRefused, match="secret-bearing"):
PlaneBundle.load(bundle_path)
def test_json_pointer_supports_label_keys_with_slashes_and_dots() -> None:
value = {"labels": {"kubernetes.io/metadata.name": "whitehat"}}
assert _json_path(value, "/labels/kubernetes.io~1metadata.name") == "whitehat"
class FakeCluster:
def __init__(
self, *, context: str = "default", drift: bool = False, dirty: bool = False
) -> None:
self.context = context
self.drift = drift
self.dirty = dirty
self.applied = False
self.calls: list[list[str]] = []
def __call__(self, args) -> CommandResult:
command = list(args)
self.calls.append(command)
if command[:4] == ["git", "-C", command[2], "status"]:
return CommandResult(0, " M src/ops_mason/kubernetes_plane.py\n" if self.dirty else "")
if command == ["kubectl", "config", "current-context"]:
return CommandResult(0, self.context + "\n")
if command[:4] == ["kubectl", "auth", "can-i", "create"]:
return CommandResult(0, "yes\n")
if "apply" in command:
if "--dry-run=client" not in command and "--dry-run=server" not in command:
self.applied = True
return CommandResult(0, "configured\n")
if command[:3] == ["kubectl", "get", "namespaces"]:
if not self.applied:
return CommandResult(1, "", 'Error from server (NotFound): namespaces "whitehat" not found')
labels = {"pod-security.kubernetes.io/enforce": "baseline" if self.drift else "restricted"}
return CommandResult(
0,
json.dumps(
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "whitehat",
"labels": labels,
"uid": "uid-1",
"resourceVersion": "10",
},
}
),
)
if command[:4] == ["kubectl", "-n", "whitehat", "get"]:
return CommandResult(0, json.dumps({"items": []}))
raise AssertionError(f"unexpected command: {command}")
def test_preflight_refuses_wrong_context_before_kubectl_apply(tmp_path: Path) -> None:
bundle = PlaneBundle.load(_fixture(tmp_path))
cluster = FakeCluster(context="wrong")
with pytest.raises(PlaneRefused, match="context mismatch"):
preflight(bundle, cluster)
assert not any("apply" in call for call in cluster.calls)
def test_apply_refuses_unapproved_plan_without_calling_runner(tmp_path: Path) -> None:
bundle = PlaneBundle.load(_fixture(tmp_path, approved=False))
calls = []
def runner(args):
calls.append(args)
raise AssertionError("runner must not be called")
with pytest.raises(PlaneRefused, match="not approved"):
apply(bundle, confirm_plan_id="plane", expected_digest=bundle.digest, runner=runner)
assert calls == []
def test_apply_refuses_digest_mismatch_without_calling_runner(tmp_path: Path) -> None:
bundle = PlaneBundle.load(_fixture(tmp_path))
calls = []
def runner(args):
calls.append(args)
raise AssertionError("runner must not be called")
with pytest.raises(PlaneRefused, match="digest confirmation mismatch"):
apply(bundle, confirm_plan_id="plane", expected_digest="wrong", runner=runner)
assert calls == []
def test_apply_refuses_dirty_repository_before_kubectl(tmp_path: Path) -> None:
bundle = PlaneBundle.load(_fixture(tmp_path))
cluster = FakeCluster(dirty=True)
with pytest.raises(PlaneRefused, match="committed and clean"):
apply(
bundle,
confirm_plan_id="plane",
expected_digest=bundle.digest,
runner=cluster,
)
assert not any(call and call[0] == "kubectl" for call in cluster.calls)
def test_apply_runs_guarded_path_and_writes_metadata_only_evidence(tmp_path: Path) -> None:
bundle = PlaneBundle.load(_fixture(tmp_path))
cluster = FakeCluster()
evidence = apply(
bundle,
confirm_plan_id="plane",
expected_digest=bundle.digest,
runner=cluster,
)
assert evidence["verification"]["negative_scope"] == {"pods": 0, "secrets": 0}
assert bundle.evidence_path.exists()
text = bundle.evidence_path.read_text()
assert "uid-1" in text
assert "data" not in evidence["verification"]
mutating = [call for call in cluster.calls if "apply" in call and "--dry-run=server" not in call and "--dry-run=client" not in call]
assert len(mutating) == 1
assert "--field-manager=ops-mason" in mutating[0]
def test_preflight_refuses_unmanaged_live_drift(tmp_path: Path) -> None:
bundle = PlaneBundle.load(_fixture(tmp_path))
cluster = FakeCluster(drift=True)
cluster.applied = True
with pytest.raises(PlaneRefused, match="unmanaged live drift"):
preflight(bundle, cluster)
def test_rollback_is_generated_but_never_executed(tmp_path: Path) -> None:
bundle = PlaneBundle.load(_fixture(tmp_path))
result = rollback_plan(bundle)
assert result["object_scoped_commands"] == []
assert result["conditional_namespace_commands"] == ["kubectl delete namespaces whitehat"]

View file

@ -4,7 +4,7 @@ type: workplan
title: "Provision the Whitehat foundational plane and reconcile intake drift"
domain: infotech
repo: ops-mason
status: ready
status: active
owner: codex
topic_slug: whitehat-foundational-plane
created: "2026-08-22"
@ -55,7 +55,7 @@ set. No unresolved dependency makes the first implementation step guesswork.
```task
id: MASON-WP-0002-T01
status: todo
status: done
priority: high
state_hub_task_id: "067451e4-50f7-59d9-b804-50f348e22508"
```
@ -82,11 +82,19 @@ Whitehat to align its README. Do not mint an engagement ID here.
four allowed objects, passes dry-run validation, and has an explicit exclusion
list and rollback procedure.
**Done (2026-08-22):** added the byte-identical source pins, approved
construction plan, declarative bundle, guarded CLI, safety documentation, and
rollback generator. Thirty tests pass. Live read-only preflight reports all
four objects absent and validates the matching audit-core Service/policy.
Bundle digest: `9636f48f0b994118ff60a8c014e0099486945d66a2b3d3582dc57a09862b2035`.
Whitehat README alignment request: State Hub message
`47032e07-5086-449f-8251-0f69df6cd934`.
## Provision and verify the foundational plane
```task
id: MASON-WP-0002-T02
status: todo
status: progress
priority: high
state_hub_task_id: "23f78bc5-4185-5181-9f3c-fd718641d2a5"
```