← News
/PLAYBOOK · 2026-08-31

Container Escape Defense in Practice: Kubernetes Runtime Isolation Hardening

Container escape is the highest-severity container-security threat. Host control follows a successful breakout. This guide covers permission limits, Capabilities drop, Seccomp/AppArmor, and runtime policy for a complete Kubernetes container-escape defense.

Container-escape defense is a core Kubernetes production-security problem. Escape means an attacker uses a kernel bug, bad privileges, or a dangerous mount to break isolation and control the host. After escape, the attacker can reach every node and sensitive data in the cluster. This guide hardens three layers: permission model, kernel security mechanisms, and runtime policy.

Scope

Prerequisites

How It Works

Three main escape paths: kernel exploits (e.g. CVE-2022-0185, Dirty Pipe); privileged containers and bad mounts (host / or the Docker Socket); over-granted Capabilities (e.g. SYS_ADMIN). Defense is layered: SecurityContext for run privileges and Capabilities drop; Seccomp/AppArmor for syscall restriction; PodSecurity Admission for enforced policy; RuntimeClass + gVisor for VM-grade isolation of untrusted workloads.

Procedure

Step 1: Minimum-privilege SecurityContext on workloads

Deployment example. Disable privileged mode, run as non-root, drop Capabilities:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: app
        image: registry.example.com/web-app:1.4.2
        securityContext:
          allowPrivilegeEscalation: false
          privileged: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]
            add: ["NET_BIND_SERVICE"]

Notes: drop: [ALL] removes all Linux capabilities; add only what is required (NET_BIND_SERVICE to bind port 80). allowPrivilegeEscalation: false blocks privilege escalation. seccompProfile: RuntimeDefault enables the runtime default syscall filter.

Step 2: AppArmor mandatory access control

Create /etc/apparmor.d/container-profile on the node:

#include <tunables/global>
profile container-profile flags=(attach_disconnected) {
  #include <abstractions/base>
  network inet tcp,
  network inet udp,
  deny mount,
  deny ptrace,
  file,
}

Load the profile and apply it to the Pod:

# Load the AppArmor profile on the node
sudo apparmor_parser -r /etc/apparmor.d/container-profile
# Enable via Pod annotation (container runtime is containerd)
metadata:
  annotations:
    container.apparmor.security.beta.kubernetes.io/app: localhost/container-profile

Step 3: Enable PodSecurity admission

# Enable restricted Pod Security Standards on the production namespace
kubectl label --overwrite ns production pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.24
# Verify: creating a privileged Pod should be rejected
kubectl -n production run test-priv --image=nginx --privileged
# Expected: Error from server (Forbidden): ... violates PodSecurity "restricted:latest"

Step 4: gVisor runtime for untrusted workloads

# Install the gVisor RuntimeClass (deploy runsc on each node first)
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
---
# Set runtimeClassName for untrusted workloads
spec:
  runtimeClassName: gvisor
  template:
    spec:
      runtimeClassName: gvisor

gVisor intercepts and virtualizes syscalls in user space. An in-container escape attempt cannot reach the host kernel directly.

Verification

# Verify capabilities have been dropped (no dangerous capabilities such as SYS_ADMIN)
kubectl exec -it deploy/web-app -- capsh --print | grep Current
# Verify Seccomp is in effect (process seccomp mode should be 2)
kubectl exec -it deploy/web-app -- cat /proc/self/status | grep Seccomp
# Verify the read-only root filesystem is in effect
kubectl exec -it deploy/web-app -- touch /tmp/test 2>&1
# Escape attempt: verify the host filesystem cannot be mounted (should be rejected)
kubectl exec -it deploy/web-app -- mount -t proc proc /proc 2>&1

FAQ

FAQ 1: Existing Pods fail to start after enabling restricted. What now?

restricted is the highest level: non-root, no privileged containers. Inventory gaps with kubectl get pods -n production -o yaml | grep -A5 securityContext. Converge by filling in securityContext (runAsNonRoot, drop capabilities). Components that truly need more privilege can use baseline and be recorded on a namespace exception list.

FAQ 2: Seccomp is blocking application syscalls. How to debug?

Start with RuntimeDefault (Docker/containerd default profile; filters only dangerous calls), not a custom profile. If still blocked, inspect denied syscall numbers via journalctl -u kubelet | grep seccomp or auditd. Use strace to see actual syscalls, then allowlist the minimum set in the Seccomp profile.

Summary

No silver bullet. Stack the layers: SecurityContext for least privilege and capability drop; Seccomp/AppArmor to shrink the syscall surface; PodSecurity to enforce the baseline at admission; gVisor for VM-grade isolation of untrusted loads. Bake these settings into the cluster GitOps templates. Recheck periodically with kube-bench and trivy. Keep node kernels patched to shrink the window on known escape CVEs.