Lesson  in  DevSecOps in Practice: Container & Kubernetes Security

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.

ProfileDescription
privilegedUnrestricted
baselinePrevents known privilege escalations
restrictedHardened — 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

SettingInsecure PodHardened Pod
runAsNonRootNo (UID 0)Yes (UID 101)
privilegedtruefalse
allowPrivilegeEscalationtruefalse
readOnlyRootFilesystemNoYes
capabilitiesAlldrop: ALL
seccompProfileNoneRuntimeDefault
Resource limitsNoneCPU + Memory set

Cleanup

kubectl delete pod insecure-pod
kubectl delete pod hardened-pod -n secure-ns
kubectl delete namespace secure-ns