Lab 02 — Kubernetes Admission Policy Evaluator
Difficulty: 3/5 | Runs locally: yes
Pairs with the Phase 07 WARMUP Chapters 8–9 (the Kubernetes security model, admission control) and the HITCHHIKERS-GUIDE ("Kubernetes Platform Validation").
Why This Lab Exists (Purpose & Goal)
Kubernetes will run whatever manifest it is given — including a pod that mounts the host filesystem, runs as root with all capabilities, or shares the host network. Admission control is the enforcement point that inspects every manifest before it is persisted and rejects the dangerous ones. The goal of this lab is to build the admission policy evaluator that encodes the Phase 06 container- hardening rules as cluster policy, so unsafe workloads cannot be deployed in the first place.
The Concept, In the Weeds
Admission control is deny-by-default applied to cluster configuration. The evaluator rejects pod specs that defeat isolation — each a Phase 06 lesson surfacing at the platform layer:
- privileged mode, host namespaces, hostPath / runtime sockets — direct routes to the node and its credentials (a container escape's destination);
- root execution, allowPrivilegeEscalation, added capabilities, writable root — re-grant the power the sandbox should remove;
- missing seccomp — kernel syscall surface left open;
- unbounded resources — denial of service / noisy neighbor;
- mutable image tags (
latest) — you can't prove what's running, and it breaks provenance; - automounted service tokens — a compromised pod inherits cluster API access.
Two subtleties this lab must teach. First, namespaces are not a tenant boundary — pods share the node, kernel, and (by default) network, so admission policy plus runtime isolation plus NetworkPolicy are all required for multi-tenancy. Second — the validation gotcha — you must check the admitted object and the running pod, because mutation/defaulting or another controller can change the effective state after your check; the manifest you submitted is not proof of what runs.
Why This Matters for Protecting the Company
Kubernetes is where most modern infrastructure runs, and a single privileged pod or hostPath mount is how a container escape becomes a node compromise becomes a cluster compromise. Admission policy is the control that prevents the dangerous configuration from ever being deployed — across every team, automatically. Without it, security depends on every engineer writing every manifest correctly forever, which never happens. A tested admission policy (block unsafe, permit safe) is how a platform team makes the cluster safe by default and keeps it that way under constant change.
Build It
Implement the evaluator over pod-like manifests: reject privileged mode, host namespaces, hostPath/ runtime sockets, root execution, privilege escalation, added capabilities, writable root, missing seccomp, unbounded resources, mutable image tags, and automounted service tokens. Test with known-bad and known-good fixtures.
LAB_MODULE=solution pytest -q
# Translate the rules into Gatekeeper (Rego) or Kyverno and test the same fixtures on a local kind cluster.
Secure Implementation Patterns
The dangerous pod (what admission must reject):
spec:
hostPID: true # sees/leaks host processes
containers:
- image: app:latest # mutable tag -> can't prove what runs
securityContext:
privileged: true # ~= root on the node
capabilities: {add: ["SYS_ADMIN"]}
volumes: [{hostPath: {path: /}}] # mounts the host filesystem
The evaluator — default-deny across every isolation dimension (mirrors solution.py):
def evaluate(pod: dict) -> tuple[str, ...]:
issues, spec = [], pod.get("spec", {})
if any(spec.get(n) for n in ("hostPID", "hostIPC", "hostNetwork")): issues.append("HOST_NAMESPACE")
if spec.get("automountServiceAccountToken", True): issues.append("AUTOMOUNTED_SERVICE_TOKEN")
if any(v.get("hostPath") for v in spec.get("volumes", [])): issues.append("HOST_PATH")
for c in spec.get("containers", []):
sc = c.get("securityContext", {})
if sc.get("privileged"): issues.append("PRIVILEGED")
if sc.get("runAsNonRoot") is not True: issues.append("MAY_RUN_AS_ROOT")
if sc.get("allowPrivilegeEscalation") is not False: issues.append("PRIVILEGE_ESCALATION")
if sc.get("readOnlyRootFilesystem") is not True: issues.append("WRITABLE_ROOT")
if sc.get("capabilities", {}).get("add"): issues.append("ADDED_CAPABILITY")
if sc.get("seccompProfile", {}).get("type") not in {"RuntimeDefault", "Localhost"}:
issues.append("MISSING_SECCOMP")
if not c.get("resources", {}).get("limits"): issues.append("MISSING_RESOURCES")
if "@sha256:" not in c.get("image", ""): issues.append("MUTABLE_IMAGE") # pin by digest
return tuple(sorted(set(issues)))
Note the default assumptions are the secure ones: runAsNonRoot is not True and
allowPrivilegeEscalation is not False both flag the absence of the safe setting — you must opt
into safety, not out of danger.
The same policy in Gatekeeper (Rego) for the real cluster:
package k8s.admission
deny[msg] {
c := input.request.object.spec.containers[_]
c.securityContext.privileged == true
msg := sprintf("privileged container %q denied", [c.name])
}
Production practices to carry forward:
- Verify the admitted object and the running pod — mutation/defaulting can change effective state after your check; the submitted manifest is not proof.
- Namespaces are not a tenant boundary — pair admission with default-deny NetworkPolicy (real CNI enforcer), runtime isolation (gVisor/Kata/node pools), and per-tenant RBAC.
- Test known-bad and known-good fixtures so the policy blocks unsafe and passes safe — a policy that blocks everything gets exempted into uselessness.
- Add image-signature verification at admission (Phase 03 provenance) so only signed, digest-pinned images run.
Validation — What You Should Be Able to Do Now
- Write a tested admission policy that blocks unsafe manifests and passes safe ones.
- Explain why a Kubernetes namespace is not a tenant boundary and what hard multi-tenancy actually requires (runtime isolation + NetworkPolicy + RBAC + admission).
- Explain why you must validate the admitted object and running pod, not the submitted manifest.
- Connect each rejected setting to the Phase 06 isolation dimension it would defeat.
The Broader Perspective
This lab is the cloud-native instance of the enforcement-point pattern you've now built five times (engagement guard, object-level authZ, release gate, sandbox compiler, admission control): a deny-by-default policy decision point applied uniformly before an action takes effect. Recognizing that admission control, a release gate, IAM, and an agent tool-gateway are the same architectural idea — policy evaluated outside the actor, failing closed — is what lets you design consistent security across wildly different layers. The Kubernetes-specific lesson (namespaces aren't isolation; verify effective state) also reinforces the recurring "verify what actually runs, not what was declared" discipline.
Interview Angle
- "How do you prevent cross-tenant access in a multi-tenant K8s platform?" — Don't rely on namespaces: per-tenant RBAC, no cluster-admin SAs in pods, default-deny NetworkPolicy with a real CNI enforcer, Pod Security, strong runtime isolation (gVisor/Kata or node pools) for untrusted tenants, per-tenant workload identity, quotas — or separate clusters. Add admission policy and audit.
Extension (Stretch)
Translate the rules into Gatekeeper/Kyverno, run them on a kind cluster, and add image-signature verification at admission (Phase 03 provenance).
References
- Phase 07 WARMUP Chapters 8–9; Kubernetes Pod Security Standards; NSA/CISA Kubernetes Hardening Guidance; OPA/Gatekeeper and Kyverno documentation.