Lab 30 - Pod Security Admission

Lab 30 - Pod Security Admission

Goal: roll out Pod Security Standards on a namespace the safe way: warn first, enforce later. Chapter: Pod Security Standards and Admission

Uses namespace hard and hardened.yaml from Lab 29.

# 1 - which namespaces have a level today?
kubectl get ns -L pod-security.kubernetes.io/enforce,pod-security.kubernetes.io/warn

Nothing - every namespace allows privileged Pods.

# 2 - a privileged Pod, allowed today
kubectl run root-shell -n dev --image busybox:1.36 --restart=Never \
  --overrides='{"spec":{"hostPID":true,"containers":[{"name":"root-shell","image":"busybox:1.36","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'
kubectl get pod root-shell -n dev
kubectl delete pod root-shell -n dev

hostPID + privileged = full control over the node. Nothing stopped it.

Step 1: warn and audit

# 3 -
kubectl label ns dev pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted
kubectl run test -n dev --image nginx:1.27

Created - but with a Warning: would violate PodSecurity "restricted:latest" listing every problem. Developers see it, nothing breaks.

Step 2: what would break?

# 4 - dry run before enforcing
kubectl label --dry-run=server --overwrite ns dev pod-security.kubernetes.io/enforce=baseline
kubectl label --dry-run=server --overwrite ns dev pod-security.kubernetes.io/enforce=restricted

baseline - no complaints: plain nginx is fine. restricted - every existing Pod is listed.

Step 3: enforce

# 5 - baseline in dev
kubectl label --overwrite ns dev pod-security.kubernetes.io/enforce=baseline
kubectl run root-shell -n dev --image busybox:1.36 --restart=Never \
  --overrides='{"spec":{"hostPID":true,"containers":[{"name":"root-shell","image":"busybox:1.36","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'

Error from server (Forbidden): ... violates PodSecurity "baseline:latest": host namespaces (hostPID=true), privileged.

# 6 - restricted in hard: the hardened Pod passes, a plain one does not
kubectl label --overwrite ns hard pod-security.kubernetes.io/enforce=restricted
kubectl delete -f ~/hardened.yaml --ignore-not-found
kubectl apply -f ~/hardened.yaml                   # accepted
kubectl run plain -n hard --image nginx:1.27       # Forbidden

The Deployment trap

# 7 -
kubectl create deploy plain -n hard --image nginx:1.27
kubectl get deploy plain -n hard                   # 0/1 - but "created"
kubectl describe rs -n hard -l app=plain | tail -3

The Deployment was accepted; the ReplicaSet cannot create Pods (FailedCreate ... violates PodSecurity). Always check the ReplicaSet events when a Deployment stays at 0.

Discovery

Infodiscover

Task A. Label every namespace that has no level yet with warn=baseline - except kube-system, calico-system and tigera-operator.

for ns in $(kubectl get ns -o name | cut -d/ -f2 | grep -v -E '^(kube-system|calico-system|tigera-operator)$'); do
  kubectl label ns $ns pod-security.kubernetes.io/warn=baseline --overwrite=false 2>/dev/null
done
kubectl get ns -L pod-security.kubernetes.io/warn

System namespaces run CNI and control plane Pods that legitimately need privileges.

Task B. Make plain in hard run under restricted with the smallest possible change.

Switch to nginxinc/nginx-unprivileged:1.27 and add the restricted securityContext (non-root, no privilege escalation, drop ALL, seccomp RuntimeDefault) - the hardened.yaml template.

Clean up

kubectl delete deploy plain -n hard; kubectl delete pod test -n dev --ignore-not-found