Enforce admission and network boundaries in a disposable Kubernetes cluster
Offline review: Node.js 22+, standard library.
Live exercise: Docker with Linux containers/cgroup v2, kind v0.31.0, kubectl v1.35.x,
Kubernetes v1.35.0 (node digest pinned in kind-config.json), Calico v3.33.0.
Allow 75–120 minutes and several GB of disk/memory. Downloads require internet.

Reference solution: not included here, so the exercise does not hand you the answer.
Attempt the repair first. Then download kubernetes-enforcement-solution.zip from the project card
on https://softwaresecurity.com/labs/#project-kubernetes-enforcement and extract it into this folder;
it adds reference.mjs without changing your files, and the reference commands below
need it.

Execution status
The offline tests run without a cluster. They inspect configuration and evaluate
the fixture's simple selector relationships; they DO NOT demonstrate packet
enforcement or validate CEL in an API server. The live steps below establish
those properties. They were last run end to end on 10 October 2026 with kind
v0.31.0, kubectl v1.35.9, the pinned Kubernetes v1.35.0 node image, Calico v3.33.0
and Docker 29.4.3 (Docker Desktop on Apple silicon, cgroup v2). The starter
admitted the bad Pod with only a warning and let outside-lab/foreign-client reach
the server; after the reference repair, verify-cluster.mjs passed all eight
checks. In that run the downloaded calico.yaml had SHA-256
2de8f47595fb9c41b3f47d7b767a1f8e72ecf84057af834738ff12689a234da5 and busybox:1.37.0
resolved to sha256:bdf57e528e45e4433820e045b29b4597825a1c9e38353532d90a01445013f82e
(arm64). Compare your own digests, versions, CNI health and output before relying
on a result in your environment.

Dev container
.devcontainer/devcontainer.json opens this folder in a pinned Node.js 24 image
(VS Code Dev Containers or GitHub Codespaces) for the offline steps. The live
steps need Docker on the host, so run them outside the container.

Goal
Separate admission (whether a workload may be created) from network policy
(whether an admitted workload may connect). Repair a warning-only admission
binding and an overbroad ingress rule. Retain an allowed path while denying a
wrong-role Pod and an identically labelled Pod in a foreign namespace.

Architecture
security-lab/allowed-client -- TCP 8080 --> security-lab/server
security-lab/wrong-role     -- denied --> security-lab/server
outside-lab/foreign-client  -- denied --> security-lab/server
Both namespaces use restricted Pod Security Admission. The server uses an
unprivileged BusyBox HTTP process and a read-only ConfigMap content mount.
Calico implements NetworkPolicy; the Kubernetes API accepting a policy object
alone does not mean the network enforces it. No host ports or external Services.

1. See the configuration failure offline [OFFLINE: Node.js only]
   node --test exercise.test.mjs
   Expected starter: 9 passing / 3 failing assertions. Failures identify warning
   mode, wrong-role ingress and cross-namespace ingress.
   Read exercise.mjs, reference.mjs and resources.mjs. Multiple from entries are
   OR alternatives. Namespace and Pod selectors in the SAME entry are ANDed.
   Read-only root filesystems are this lab's additional admission condition;
   restricted Pod Security Admission alone does not impose that condition.
   node render.mjs
   Inspect resources.json, policies.json and bad-readonly.json. The last file is
   otherwise a restricted-compliant Pod so custom admission is what must reject it.

2. Create only the disposable cluster [LIVE: needs Docker, kind, kubectl and internet access]
   Install the pinned tools using their official release instructions. The node
   digest is from the kind v0.31.0 release notes; use the supplied config unchanged.
   kind version
   kubectl version --client
   docker info
   kind get clusters
   If security-fieldguide-lab already exists, stop and inspect it; choose to
   remove your previous lab explicitly rather than replacing an unknown cluster.
   kind create cluster --name security-fieldguide-lab --config kind-config.json
   No --wait is supplied: Nodes cannot become Ready until the CNI is installed.
   The API address is localhost and all remaining commands pin the lab context.
   Never substitute a shared or production context for this exercise.

3. Install an actual network-policy implementation [LIVE: needs Docker, kind, kubectl and internet access]
   curl --fail --location --output calico-v3.33.0.yaml https://raw.githubusercontent.com/projectcalico/calico/v3.33.0/manifests/calico.yaml
   Inspect the downloaded release manifest before applying it. This is the
   manifest-based installation with crd.projectcalico.org CRDs; it does not use
   the separate native-v3/operator quickstart or its feature-gate instructions.
   kubectl --context kind-security-fieldguide-lab apply --server-side -f calico-v3.33.0.yaml
   kubectl --context kind-security-fieldguide-lab -n kube-system rollout status daemonset/calico-node --timeout=180s
   kubectl --context kind-security-fieldguide-lab -n kube-system rollout status deployment/calico-kube-controllers --timeout=180s
   kubectl --context kind-security-fieldguide-lab wait --for=condition=Ready nodes --all --timeout=180s
   Save the downloaded manifest and its SHA-256 in your run record. A version
   URL narrows the release; it is not an immutable content digest. BusyBox 1.37.0
   is also a version tag: record the resolved imageID after the Pods start. The
   cluster node image is the only image pinned by immutable digest here.

4. Observe the real starter behavior [LIVE: needs Docker, kind, kubectl and internet access]
   kubectl --context kind-security-fieldguide-lab apply -f resources.json
   kubectl --context kind-security-fieldguide-lab apply -f policies.json
   kubectl --context kind-security-fieldguide-lab get validatingadmissionpolicy fieldguide-readonly-root -o yaml
   Wait for its status/typeChecking to populate; fix any expressionWarnings.
   kubectl --context kind-security-fieldguide-lab create --dry-run=server -f bad-readonly.json
   Expected starter: a warning, but creation is allowed in server dry-run. No bad
   Pod is actually stored. Warn provides visibility, not enforcement.
   kubectl --context kind-security-fieldguide-lab -n security-lab wait --for=condition=Ready pods --all --timeout=180s
   kubectl --context kind-security-fieldguide-lab -n outside-lab wait --for=condition=Ready pods --all --timeout=180s
   kubectl --context kind-security-fieldguide-lab -n security-lab get pod server -o wide
   Copy the server Pod IP from this output as SERVER_IP below (do not use the node
   IP). Replace the placeholder, including the capitalized name:
   kubectl --context kind-security-fieldguide-lab -n outside-lab exec foreign-client -- wget -T 3 -qO- http://SERVER_IP:8080/
   Expected starter: security-lab-ok. The empty namespaceSelector permits all
   namespaces. The second peer does not restrict the first peer's allowance.

5. Repair and reapply [LIVE: needs Docker, kind, kubectl and internet access]
   In exercise.mjs change validationActions to Deny. Use one ingress peer with
   BOTH the security-lab namespace selector and role=client Pod selector.
   node --test exercise.test.mjs
   Expected: all 12 offline tests pass.
   node render.mjs
   kubectl --context kind-security-fieldguide-lab apply -f policies.json
   Wait for the updated policy generation/status and allow CNI reconciliation.
   node verify-cluster.mjs
   Expected: the bad read-only Pod is denied by this specific admission message;
   the permitted client reaches the healthy origin by Pod IP and Service DNS;
   wrong-role and foreign clients fail to connect. The script stops on a failed
   check and never silently turns a cluster/network failure into a passing test.
   After a recent change, retry following reconciliation; investigate persistent
   failure rather than increasing timeouts indefinitely.

Reference comparison
   EXERCISE_MODULE=reference node --test exercise.test.mjs
   PowerShell: $env:EXERCISE_MODULE='reference'; node --test exercise.test.mjs
   Clear that variable before checking your edits. To render the reference:
   node render.mjs --reference
   Reapply policies.json and run node verify-cluster.mjs. The live script reads
   cluster state, not exercise.mjs; passing source tests is not deployment proof.

Worked explanation
The default-deny policy selects all Pods in security-lab for ingress and egress.
Additional policies ADD allowances; they do not override or subtract earlier
allowances. Every Pod in security-lab may attempt TCP 8080 to server Pods, so the
wrong-role negative check is decided at server ingress instead of accidentally
passing because its egress was blocked. The outside namespace has no egress
policy, so the foreign-client probe also reaches the ingress decision. A single
combined peer narrows ingress to the intended namespace AND role. DNS egress is
a separate UDP/TCP 53 allowance to kube-system/kube-dns, verified by Service DNS.
The live admission test uses server dry-run, so rejection tests never create the
invalid workload. NetworkPolicy does not authenticate application-level callers;
production authorization must still protect the HTTP endpoint.

Evidence and completion rubric
Save: tool versions, node image, CNI rollout output, manifests, resolved workload
imageIDs, offline test output and live probe output. Add a fourth test Pod with
role=client in a third namespace; explain its expected denial before running it.
Explain who can change namespace labels, Pod labels, policies and admission
bindings: a cluster administrator can weaken these lab controls. Record both
positive and negative outcomes; timeouts without a healthy positive control are
not persuasive evidence of isolation.

Troubleshooting
Nodes stuck NotReady: confirm disableDefaultCNI is true and Calico Pods are ready.
ImagePullBackOff: check registry access and image architecture. Cgroup errors:
use a current Docker Linux VM with cgroup v2. Subnet overlap: the documented
192.168.0.0/16 lab CIDR must not overlap host/VPN routes; changing it also requires
reviewing the Calico pool, rather than changing one file in isolation.
Admission unexpectedly allows: confirm binding action, namespace selector, CEL
status and context. Existing Pods are not retroactively killed by an admission
change. Network unexpectedly allows: inspect ALL policies selecting the Pod;
an additional allowance wins over default-deny. DNS failure: confirm CoreDNS
labels and both TCP/UDP policy, then compare Pod-IP and DNS probes.
The live script checks the intended context and v1.35 server family. It cannot
certify a compromised cluster or distinguish every possible network failure.

Cleanup
   kind delete cluster --name security-fieldguide-lab
This removes ONLY the named disposable cluster, its namespaced resources and its
cluster-scoped admission objects. Delete the local generated resources.json,
policies.json, bad-readonly.json and downloaded Calico manifest if no longer needed.
Do not run broad kubectl delete commands against your normal current context.

Limits and sources
Single-node IPv4, standard NetworkPolicy, one CNI and one Kubernetes family. This
is not a production hardening baseline, multi-node failure test, RBAC lesson,
egress-domain filter or proof against privileged/node attackers. The pinned
versions are a reproducible educational fixture, not a patch recommendation.
Refresh pins and rerun live probes before adopting another version.
https://github.com/kubernetes-sigs/kind/releases/tag/v0.31.0
https://kind.sigs.k8s.io/docs/user/quick-start/
https://docs.tigera.io/calico/latest/getting-started/kubernetes/requirements
https://docs.tigera.io/calico/latest/getting-started/kubernetes/installation-options
https://kubernetes.io/docs/concepts/services-networking/network-policies/
https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
https://kubernetes.io/docs/concepts/security/pod-security-admission/
