Pod Hardening & Pod Security Admission
Deploy an insecure privileged pod, then apply Kubernetes Pod Security Admission (PSA) to a namespace. Watch PSA block the insecure pod at admission time — before any code runs. Finally, deploy a fully hardened pod that passes the restricted standard.
The Insecure Pod & PSA Setup
Pod Security Admission is Built In
Kubernetes's Pod Security Admission (PSA) enforces security standards at the namespace level — no extra tools needed. You label a namespace and Kubernetes enforces the standard at admission time.
| Profile | Description |
|---|---|
privileged | Unrestricted |
baseline | Prevents known privilege escalations |
restricted | Hardened — current best practices |
Deploy an Insecure Pod
cat > pod-insecure.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: insecure-pod
spec:
containers:
- name: app
image: public.ecr.aws/nginx/nginx:1.25-alpine
securityContext:
privileged: true
runAsUser: 0
allowPrivilegeEscalation: true
resources: {}
EOF
kubectl apply -f pod-insecure.yaml
kubectl wait --for=condition=Ready pod/insecure-pod --timeout=60s
kubectl exec insecure-pod -- id
kubectl exec insecure-pod -- cat /proc/1/status | grep -E "CapEff|CapPrm"
Expected: uid=0(root) and a full capabilities bitmask.
Apply Pod Security Admission
kubectl create namespace secure-ns
kubectl label namespace secure-ns \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest
Try to Deploy the Insecure Pod in the Restricted Namespace
kubectl apply -f pod-insecure.yaml -n secure-ns
Expected:
Error from server (Forbidden): pods "insecure-pod" is forbidden:
violates PodSecurity "restricted:latest":
privileged, runAsNonRoot, allowPrivilegeEscalation ...
The pod never starts — PSA blocked it at admission time, before any code ran.
Deploying the Hardened Pod
A Pod That Passes the Restricted Standard
Every field matters. The hardened pod drops all capabilities, runs as a specific non-root UID, uses a read-only root filesystem, and applies a seccomp profile — these are the four settings with the highest security impact.
cat > pod-hardened.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: hardened-pod
namespace: secure-ns
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginxinc/nginx-unprivileged:1.25-alpine
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources:
requests: {cpu: "100m", 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 pod-hardened.yaml
kubectl wait --for=condition=Ready pod/hardened-pod -n secure-ns --timeout=60s
Verify the Hardening
kubectl exec -n secure-ns hardened-pod -- id
kubectl exec -n secure-ns hardened-pod -- sh -c "echo test > /etc/test.txt" 2>&1
kubectl exec -n secure-ns hardened-pod -- sh -c "echo 'tmp works' > /tmp/test && cat /tmp/test"
Expected:
uid=101 gid=101 groups=101
sh: can't create /etc/test.txt: Read-only file system
tmp works
Security Context Comparison
| Setting | Insecure Pod | Hardened Pod |
|---|---|---|
runAsNonRoot | No (UID 0) | Yes (UID 101) |
privileged | true | false |
allowPrivilegeEscalation | true | false |
readOnlyRootFilesystem | No | Yes |
capabilities | All | drop: ALL |
seccompProfile | None | RuntimeDefault |
| Resource limits | None | CPU + Memory set |
Cleanup
kubectl delete pod insecure-pod
kubectl delete pod hardened-pod -n secure-ns
kubectl delete namespace secure-ns
- Previous lesson
- HashiCorp Vault + External Secrets Operator
- Next lesson
- Namespace & RBAC Isolation