CKS — Pod Security Lab
Pod Security Lab
This lab brings together everything from the Pod Security chapter — Pod Security Admission, security contexts, runtime sandboxing, and workload hardening. Each exercise mirrors a realistic CKS exam task.
Prerequisites
- A Kubernetes cluster (kubeadm, minikube, or kind) running v1.25+
kubectlconfigured and working- For sandboxing exercises: gVisor (
runsc) installed on at least one node (optional — can be simulated) - Cluster admin access
Lab 1: Apply PSA Labels to Enforce Restricted Level
Task
Configure the namespace secure-apps to enforce the Restricted Pod Security Standard. Set audit and warn modes to Restricted as well. Pin to version v1.30.
Steps
# Create the namespace
kubectl create namespace secure-apps
# Apply PSA labels
kubectl label namespace secure-apps \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.30 \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=v1.30 \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=v1.30
# Verify labels
kubectl get namespace secure-apps -o yaml
Expected Output
apiVersion: v1
kind: Namespace
metadata:
labels:
kubernetes.io/metadata.name: secure-apps
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.30
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.30
name: secure-apps
Verification
# Confirm all six PSA labels are present
kubectl get ns secure-apps --show-labels | grep pod-security
Lab 2: Test PSA — Deploy a Privileged Pod (Expect Rejection)
Task
Attempt to deploy a non-compliant Pod in the secure-apps namespace. The Pod should be rejected by PSA.
Steps
# Create a non-compliant Pod manifest
cat <<'EOF' > /tmp/non-compliant-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: non-compliant
namespace: secure-apps
spec:
containers:
- name: test
image: nginx:1.27
# No security context — violates Restricted level
EOF
# Apply and observe rejection
kubectl apply -f /tmp/non-compliant-pod.yaml
Expected Output
Error from server (Forbidden): error when creating "/tmp/non-compliant-pod.yaml":
pods "non-compliant" is forbidden: violates PodSecurity "restricted:v1.30":
allowPrivilegeEscalation != false
(container "test" must set securityContext.allowPrivilegeEscalation=false)
unrestricted capabilities
(container "test" must set securityContext.capabilities.drop=["ALL"])
runAsNonRoot != true
(pod or container "test" must set securityContext.runAsNonRoot=true)
seccompProfile
(pod or container "test" must set securityContext.seccompProfile.type
to "RuntimeDefault" or "Localhost")
Analysis
The Pod was rejected because it violates four Restricted requirements:
allowPrivilegeEscalationnot set tofalse- Capabilities not dropped
runAsNonRootnot set totrue- No Seccomp profile specified
Each violation is listed in the error message — this is how PSA provides actionable feedback.
Lab 3: Deploy a Compliant Pod with Full Security Context
Task
Create and deploy a Pod that passes the Restricted Pod Security Standard in the secure-apps namespace.
Steps
cat <<'EOF' > /tmp/compliant-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: compliant-web
namespace: secure-apps
labels:
app: web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: run
emptyDir: {}
EOF
kubectl apply -f /tmp/compliant-pod.yaml
Expected Output
pod/compliant-web created
Verification
# Confirm Pod is running
kubectl get pod compliant-web -n secure-apps
# NAME READY STATUS RESTARTS AGE
# compliant-web 1/1 Running 0 10s
# Verify security settings inside the container
kubectl exec -n secure-apps compliant-web -- id
# uid=1000 gid=1000 groups=1000
kubectl exec -n secure-apps compliant-web -- cat /proc/1/status | grep -i seccomp
# Seccomp: 2
# Seccomp_filters: 1
# Verify read-only root filesystem
kubectl exec -n secure-apps compliant-web -- touch /test-file 2>&1
# touch: /test-file: Read-only file system
# Verify writable emptyDir mount
kubectl exec -n secure-apps compliant-web -- touch /tmp/test-file
# (no error — writable via emptyDir)
# Verify capabilities
kubectl exec -n secure-apps compliant-web -- cat /proc/1/status | grep Cap
# CapPrm: 0000000000000000
# CapEff: 0000000000000000
# All zeros = no capabilities
Lab 4: Configure Security Contexts — Hardening Exercise
Task
A Deployment is running in the secure-apps namespace but is non-compliant. Fix it to pass the Restricted PSS level.
Setup (Create the Broken Deployment)
First, temporarily set the namespace to warn-only mode:
kubectl label namespace secure-apps \
pod-security.kubernetes.io/enforce=privileged \
--overwrite
cat <<'EOF' > /tmp/broken-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: secure-apps
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: python:3.12-slim
command: ["python", "-m", "http.server", "8080"]
ports:
- containerPort: 8080
EOF
kubectl apply -f /tmp/broken-deploy.yaml
# Wait for Pods to be running
kubectl rollout status deploy/api-server -n secure-apps
# Now re-enable Restricted enforcement
kubectl label namespace secure-apps \
pod-security.kubernetes.io/enforce=restricted \
--overwrite
Task: Fix the Deployment
# The existing Pods keep running (PSA doesn't terminate running Pods)
# But if you try to scale or restart, new Pods will be rejected
# Try restarting
kubectl rollout restart deploy/api-server -n secure-apps
# Check — new ReplicaSet can't create Pods
kubectl get rs -n secure-apps
kubectl describe rs -n secure-apps -l app=api | grep -A 5 Events
Solution
cat <<'EOF' > /tmp/fixed-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: secure-apps
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
containers:
- name: api
image: python:3.12-slim
command: ["python", "-m", "http.server", "8080"]
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
EOF
kubectl apply -f /tmp/fixed-deploy.yaml
# Verify the rollout succeeds
kubectl rollout status deploy/api-server -n secure-apps
# deployment "api-server" successfully rolled out
kubectl get pods -n secure-apps -l app=api
# NAME READY STATUS RESTARTS AGE
# api-server-new-abc123 1/1 Running 0 15s
# api-server-new-def456 1/1 Running 0 15s
What Was Fixed
| Field | Before | After |
|---|---|---|
runAsNonRoot |
Not set | true |
runAsUser |
Not set (root) | 1000 |
seccompProfile |
Not set | RuntimeDefault |
allowPrivilegeEscalation |
Not set (true default) |
false |
capabilities |
Default (14 caps) | drop: ["ALL"] |
readOnlyRootFilesystem |
Not set (false default) |
true |
automountServiceAccountToken |
Not set (true default) |
false |
Lab 5: Create a RuntimeClass for gVisor and Deploy a Sandboxed Pod
Task
Create a RuntimeClass named gvisor with handler runsc, then deploy a Pod using the sandbox runtime.
Steps
# Step 1: Create the RuntimeClass
cat <<'EOF' > /tmp/runtimeclass-gvisor.yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
overhead:
podFixed:
cpu: "250m"
memory: "120Mi"
EOF
kubectl apply -f /tmp/runtimeclass-gvisor.yaml
# Verify
kubectl get runtimeclass
# NAME HANDLER AGE
# gvisor runsc 5s
# Step 2: Deploy a sandboxed Pod
# Note: This requires gVisor (runsc) installed on nodes.
# If runsc is not installed, the Pod will fail to schedule.
cat <<'EOF' > /tmp/sandboxed-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: sandboxed-app
namespace: secure-apps
labels:
app: sandboxed
spec:
runtimeClassName: gvisor
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.27-alpine
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: run
emptyDir: {}
EOF
kubectl apply -f /tmp/sandboxed-pod.yaml
Expected Output (if gVisor is installed)
kubectl get pod sandboxed-app -n secure-apps
# NAME READY STATUS RESTARTS AGE
# sandboxed-app 1/1 Running 0 10s
# Verify RuntimeClass
kubectl get pod sandboxed-app -n secure-apps -o jsonpath='{.spec.runtimeClassName}'
# gvisor
Expected Output (if gVisor is NOT installed)
kubectl describe pod sandboxed-app -n secure-apps | tail -5
# Events:
# Warning FailedCreatePodSandBox ... failed to create containerd task:
# failed to find runtime handler "runsc"
This is expected in environments without gVisor. The key CKS skill is knowing how to create the RuntimeClass and Pod YAML correctly.
Lab 6: Verify Runtime Isolation
Task
Verify that a gVisor-sandboxed Pod provides different runtime characteristics compared to a standard Pod.
Steps (requires gVisor installed)
# Check kernel version — gVisor reports its own
kubectl exec -n secure-apps sandboxed-app -- uname -r
# 4.4.0 ← gVisor's emulated kernel (NOT the host kernel)
# Compare with a standard Pod
kubectl exec -n secure-apps compliant-web -- uname -r
# 5.15.0-86-generic ← Host kernel (shared by standard containers)
# Check dmesg — gVisor has distinctive startup messages
kubectl exec -n secure-apps sandboxed-app -- dmesg 2>/dev/null | head -3
# [ 0.000000] Starting gVisor...
# [ 0.000000] Checking navel for lint...
# [ 0.000000] Creating process schedule...
# Compare process view — gVisor provides process isolation
kubectl exec -n secure-apps sandboxed-app -- ls /proc/ | head -10
# 1
# self
# ...
# Test: attempt a dangerous syscall — blocked by gVisor
kubectl exec -n secure-apps sandboxed-app -- mount -t tmpfs tmpfs /mnt 2>&1
# mount: permission denied (or similar)
Comparison Table
| Check | Standard Pod (runc) | Sandboxed Pod (gVisor) |
|---|---|---|
uname -r |
Host kernel version (e.g., 5.15.0) | gVisor version (e.g., 4.4.0) |
dmesg |
Host kernel messages | gVisor startup messages |
| Kernel CVE impact | Pod vulnerable if host kernel has CVE | Pod isolated — host kernel not exposed |
| Syscall path | Container → Host Kernel | Container → gVisor Sentry → Limited host access |
Cleanup
# Remove all lab resources
kubectl delete namespace secure-apps
kubectl delete runtimeclass gvisor
# Remove temporary files
rm -f /tmp/non-compliant-pod.yaml \
/tmp/compliant-pod.yaml \
/tmp/broken-deploy.yaml \
/tmp/fixed-deploy.yaml \
/tmp/runtimeclass-gvisor.yaml \
/tmp/sandboxed-pod.yaml
# Verify cleanup
kubectl get namespace secure-apps 2>&1
# Error from server (NotFound): namespaces "secure-apps" not found
kubectl get runtimeclass gvisor 2>&1
# Error from server (NotFound): runtimeclasses.node.k8s.io "gvisor" not found
Lab Summary
| Lab | Skill Tested | CKS Relevance |
|---|---|---|
| Lab 1 | Apply PSA labels to namespaces | High — direct exam task |
| Lab 2 | Understand PSA rejection behavior | High — troubleshooting |
| Lab 3 | Write Restricted-compliant Pod specs | High — core exam skill |
| Lab 4 | Fix non-compliant Deployments | High — common exam scenario |
| Lab 5 | Create RuntimeClass and sandboxed Pods | Medium — may appear |
| Lab 6 | Verify runtime isolation | Medium — understanding |
Practice Questions
Q1: A namespace has enforce: restricted set. You deployed a Deployment with 3 replicas, but kubectl get pods shows 0 Pods. The Deployment shows 0/3 ready. Where do you look for the error?
Check the ReplicaSet events, not the Deployment events.
PSA enforces at the Pod level. The Deployment creates a ReplicaSet, and the ReplicaSet attempts to create Pods. When PSA rejects the Pods, the failure is recorded in the ReplicaSet’s events.
# Step 1: Find the ReplicaSet
kubectl get rs -n <namespace> -l <deployment-labels>
# Step 2: Check ReplicaSet events
kubectl describe rs -n <namespace> <replicaset-name>
# Events:
# Warning FailedCreate replicaset-controller
# pods "deploy-abc123-xyz" is forbidden:
# violates PodSecurity "restricted:latest":
# allowPrivilegeEscalation != false ...
# Step 3: Fix the Deployment's Pod template
kubectl edit deploy <name> -n <namespace>
# Add the required security context fields
# Step 4: Verify
kubectl rollout status deploy/<name> -n <namespace>
The Deployment itself doesn’t show the PSA error — you must inspect the ReplicaSet.
Q2: Write the kubectl commands to: (1) create namespace staging, (2) enforce Baseline, (3) set audit and warn to Restricted. Explain this strategy.
# Create namespace
kubectl create namespace staging
# Apply PSA labels
kubectl label namespace staging \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
Strategy explanation:
This is a gradual rollout approach:
- enforce: baseline — Immediately blocks dangerous settings (hostNetwork, privileged, hostPID). This prevents known privilege escalation.
- warn: restricted — Shows developers which Pods would fail if Restricted were enforced. Developers see warnings in
kubectloutput. - audit: restricted — Logs violations to the API server audit log. Security teams can track which workloads need to be fixed.
Migration path:
- Deploy with this label configuration
- Fix all workloads that generate Restricted warnings
- Once all Pods are Restricted-compliant, switch:
kubectl label ns staging pod-security.kubernetes.io/enforce=restricted --overwrite
This avoids breaking existing workloads while providing visibility into what needs to change.
Q3: You need to run a CNI plugin (Calico) as a DaemonSet that requires hostNetwork: true and privileged: true. All namespaces have enforce: restricted. How do you allow this DaemonSet?
Option 1: Label kube-system with Privileged (recommended)
kubectl label namespace kube-system \
pod-security.kubernetes.io/enforce=privileged \
--overwrite
Calico typically runs in kube-system. Setting this namespace to Privileged allows the CNI DaemonSet while all other namespaces remain Restricted.
Option 2: Exempt kube-system via cluster-level AdmissionConfiguration
# /etc/kubernetes/psa/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: "restricted"
enforce-version: "latest"
exemptions:
namespaces:
- kube-system
Why this is necessary: CNI plugins need host-level access to configure networking (iptables, routing, veth pairs). They cannot run under Restricted or even Baseline. Exempting kube-system is a standard practice — Kubernetes itself recommends it.
Q4: Create a Deployment with 2 replicas in namespace secure-apps that: runs as non-root, drops all capabilities, has a read-only root filesystem, uses a Seccomp profile, writes logs to /app/logs, and uses a gVisor sandbox runtime.
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-service
namespace: secure-apps
spec:
replicas: 2
selector:
matchLabels:
app: secure-service
template:
metadata:
labels:
app: secure-service
spec:
runtimeClassName: gvisor
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
containers:
- name: app
image: myapp:1.0
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: logs
mountPath: /app/logs
- name: tmp
mountPath: /tmp
volumes:
- name: logs
emptyDir: {}
- name: tmp
emptyDir: {}
Compliance checklist:
runtimeClassName: gvisor✓ (sandbox isolation)runAsNonRoot: true✓seccompProfile: RuntimeDefault✓allowPrivilegeEscalation: false✓capabilities.drop: ["ALL"]✓readOnlyRootFilesystem: true✓ (withemptyDirfor/app/logsand/tmp)automountServiceAccountToken: false✓- Resource limits set ✓
Q5: After setting enforce: restricted on a namespace, existing Pods are still running with root access. Is this expected? What happens if you restart them?
Yes, this is expected. PSA only checks Pods at admission time (when they are created). It does not retroactively terminate or modify running Pods.
What happens with existing Pods:
- Pods that were running before the label was applied continue to run unchanged
- They maintain their original security context (even if non-compliant)
- PSA does not scan or evict existing Pods
What happens when you restart them:
kubectl rollout restart deploy/my-app -n the-namespace
- The Deployment creates a new ReplicaSet
- The new ReplicaSet tries to create new Pods
- PSA checks the new Pods at admission → rejected if non-compliant
- Old Pods are terminated (by the rollout) but new ones can’t start
- Result: zero running Pods until the manifests are fixed
Safe migration steps:
- Apply
warn: restrictedfirst → identify non-compliant workloads - Fix all Deployment manifests to be Restricted-compliant
- Test with
kubectl apply --dry-run=serverin a Restricted namespace - Apply
enforce: restricted→ now all new Pod creation is checked - Restart Deployments → new compliant Pods replace old non-compliant ones
| Previous | Up | Next |
|---|---|---|
| mTLS & Pod-to-Pod Encryption | Pod Security | OPA Gatekeeper |