CKS — Container Runtime Sandboxing
Container Runtime Sandboxing
Standard containers share the host kernel. If an attacker exploits a kernel vulnerability from inside a container, they can compromise the entire node. Runtime sandboxing adds an isolation layer between the container and the host kernel, dramatically reducing the blast radius of container escapes.
For the CKS exam, you need to understand why sandboxing matters, how gVisor and Kata Containers work, and how to assign sandbox runtimes to Pods using RuntimeClass.
1. Why Sandboxing?
The Container Escape Problem
Standard Container (runc):
Container Process
│
├── Linux Namespace (PID, NET, MNT, ...) ← Isolation boundary
│
└── System Call ──────────────────────────→ HOST KERNEL
│
├── If kernel has CVE → Node compromised
├── Shared with ALL containers on node
└── Shared with host processes
Containers are processes with namespace isolation, not virtual machines. Every container on a node makes system calls directly to the same host kernel. A single kernel vulnerability (CVE) can allow:
- Container escape — break out of the namespace boundary
- Cross-container attacks — access other containers on the same node
- Node takeover — gain root on the underlying host
Historical Container Escapes
| CVE | Year | Impact |
|---|---|---|
| CVE-2019-5736 | 2019 | runc vulnerability — overwrite host runc binary from inside container |
| CVE-2020-15257 | 2020 | containerd vulnerability — access host network namespace |
| CVE-2022-0185 | 2022 | Kernel vulnerability — heap overflow in file system context, exploitable from unprivileged container |
| CVE-2024-21626 | 2024 | runc vulnerability — file descriptor leak allowing container escape |
Sandboxing prevents these attacks by ensuring the container never talks directly to the host kernel.
2. Sandboxing Approaches
gVisor (runsc)
gVisor is an application kernel written in Go that intercepts system calls in user space. The container talks to gVisor instead of the host kernel.
gVisor Architecture:
Container Process
│
└── System Call → gVisor (Sentry) ← User-space kernel
│
├── Handles most syscalls internally
│ (no host kernel involvement)
│
└── Proxies limited syscalls → Host Kernel
(file I/O via Gofer process)
Key components:
- Sentry — the user-space kernel that intercepts and handles syscalls
- Gofer — a file proxy that mediates file system access to the host
- runsc — the OCI-compatible runtime binary (replaces runc)
Characteristics:
- Implements ~70% of Linux syscalls in user space
- No hardware virtualization required
- Lower memory overhead than VMs
- Some performance overhead for syscall-heavy workloads
- May break applications that use unsupported syscalls
Kata Containers
Kata Containers runs each Pod inside a lightweight virtual machine (VM). The container gets its own dedicated kernel running inside a VM.
Kata Containers Architecture:
Container Process
│
└── System Call → Guest Kernel ← Dedicated kernel per Pod
│
└── Hypervisor (QEMU/Cloud Hypervisor/Firecracker)
│
└── Host Kernel
Key components:
- Guest kernel — lightweight Linux kernel running inside the VM
- Hypervisor — QEMU, Cloud Hypervisor, or Firecracker
- kata-runtime — the OCI-compatible runtime that manages VM lifecycle
Characteristics:
- Hardware-level isolation (VM boundary)
- Full Linux syscall compatibility
- Higher memory overhead (each Pod gets a VM)
- Slightly longer startup time
- Requires hardware virtualization support (Intel VT-x / AMD-V)
Standard runc
The default container runtime. Containers run as processes with namespace isolation, sharing the host kernel.
runc Architecture:
Container Process
│
└── System Call → Host Kernel (shared with all containers)
3. Comparison Table
| Feature | runc (Standard) | gVisor (runsc) | Kata Containers |
|---|---|---|---|
| Isolation level | Namespaces only | User-space kernel | VM (hardware isolation) |
| Kernel sharing | Shared host kernel | gVisor kernel (user-space) | Dedicated guest kernel |
| Syscall handling | Direct to host kernel | Intercepted by Sentry | Via guest kernel |
| Container escape risk | Kernel CVE = node compromise | Kernel CVE = gVisor restart (host safe) | Kernel CVE = VM compromise (host safe) |
| Performance | Best (native) | Good (overhead on syscall-heavy workloads) | Good (VM overhead on startup) |
| Memory overhead | Lowest | Low–Medium | Higher (VM per Pod) |
| Startup time | Fastest | Fast | Slightly slower (VM boot) |
| Syscall compatibility | Full | ~70% (some apps may break) | Full |
| Hardware requirement | None | None | VT-x / AMD-V |
| OCI runtime | runc |
runsc |
kata-runtime |
| Use case | Trusted workloads | Multi-tenant, untrusted code | Strong isolation for sensitive workloads |
4. RuntimeClass Resource
RuntimeClass is the Kubernetes resource that maps a human-readable name to a container runtime handler. Pods reference the RuntimeClass to request a specific runtime.
Creating a RuntimeClass for gVisor
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc # Must match the handler name configured in containerd
Creating a RuntimeClass for Kata Containers
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata # Must match the handler name configured in containerd
RuntimeClass with Scheduling and Overhead
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
overhead:
podFixed:
cpu: "250m"
memory: "120Mi" # Extra resources consumed by the sandbox runtime
scheduling:
nodeSelector:
sandbox-runtime: "gvisor" # Only schedule on nodes with gVisor installed
tolerations:
- key: "sandbox"
operator: "Equal"
value: "gvisor"
effect: "NoSchedule"
Fields explained:
handler— the name registered in the container runtime (containerd/CRI-O) configurationoverhead.podFixed— additional resources consumed by the sandbox runtime itself (not the application)scheduling.nodeSelector— ensures Pods using this RuntimeClass only run on nodes with the runtime installedscheduling.tolerations— allows Pods to run on tainted sandbox nodes
5. Containerd Configuration for Sandbox Runtimes
The RuntimeClass handler must match a runtime entry in containerd’s configuration.
gVisor Configuration
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc.options]
TypeUrl = "io.containerd.runsc.v1.options"
Kata Containers Configuration
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
runtime_type = "io.containerd.kata.v2"
privileged_without_host_devices = true
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata.options]
ConfigPath = "/opt/kata/share/defaults/kata-containers/configuration.toml"
After modifying containerd config:
sudo systemctl restart containerd
sudo systemctl status containerd
6. Assigning Sandbox Runtimes to Pods
Using runtimeClassName
apiVersion: v1
kind: Pod
metadata:
name: sandboxed-pod
spec:
runtimeClassName: gvisor # ← References the RuntimeClass name
containers:
- name: app
image: nginx:1.27-alpine
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
securityContext:
seccompProfile:
type: RuntimeDefault
Deployment with Sandbox Runtime
apiVersion: apps/v1
kind: Deployment
metadata:
name: untrusted-app
spec:
replicas: 3
selector:
matchLabels:
app: untrusted
template:
metadata:
labels:
app: untrusted
spec:
runtimeClassName: gvisor
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: untrusted-app:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Verifying the Runtime
# Check which runtime a Pod is using
kubectl get pod sandboxed-pod -o jsonpath='{.spec.runtimeClassName}'
# gvisor
# From inside the Pod — gVisor identifies itself in /proc
kubectl exec sandboxed-pod -- dmesg | head -5
# [ 0.000000] Starting gVisor...
# [ 0.000000] Checking navel for lint...
# [ 0.000000] Creating process schedule...
# Check uname — gVisor reports a different kernel version
kubectl exec sandboxed-pod -- uname -r
# 4.4.0 ← gVisor's emulated kernel version (not the host kernel)
7. Performance Considerations
When to Use Sandboxing
| Workload Type | Recommended Runtime | Reason |
|---|---|---|
| Trusted internal apps | runc | Performance matters, trusted code |
| Multi-tenant (untrusted code) | gVisor | User-submitted code, CI/CD runners |
| Serverless / FaaS | gVisor or Kata | Isolation between functions |
| Sensitive data processing | Kata | Strongest isolation for regulated workloads |
| High-performance databases | runc | Syscall overhead impacts I/O-heavy workloads |
| Network-intensive services | runc or gVisor | gVisor handles networking well |
| System-level workloads (CNI, CSI) | runc | Need direct host access |
Performance Impact
| Operation | runc | gVisor | Kata |
|---|---|---|---|
| Container startup | ~100ms | ~150ms | ~500ms–1s |
| Syscall overhead | None | 2–10× per syscall | Similar to runc (real kernel) |
| Memory overhead | ~5MB | ~50–120MB per sandbox | ~128MB+ per VM |
| File I/O | Native | Slower (Gofer proxy) | Near-native |
| Network I/O | Native | Slight overhead | Near-native |
Exam Tip: The CKS exam won’t ask about gVisor internals or Kata VM configuration. Focus on: creating RuntimeClass resources, setting
runtimeClassNameon Pods, and understanding why sandboxing provides stronger isolation than namespaces alone.
8. PSA Exemptions for Sandbox Runtimes
Since sandbox runtimes provide their own isolation, you can exempt them from strict PSA enforcement:
# In AdmissionConfiguration
exemptions:
runtimeClasses:
- gvisor
- kata
This allows Pods using sandbox runtimes to run with elevated privileges (if needed) because the sandbox provides equivalent or stronger isolation than PSA restrictions.
9. CKS Exam Patterns
Pattern 1: Create RuntimeClass and Assign to Pod
Task: Create a RuntimeClass named "sandbox" with handler "runsc".
Update Pod "web" to use this runtime.
Step 1:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: sandbox
handler: runsc
Step 2: Add runtimeClassName to the Pod
spec:
runtimeClassName: sandbox
Pattern 2: Debug Why a Sandboxed Pod Fails
Task: A Pod with runtimeClassName "gvisor" is in Pending state.
Diagnosis:
1. kubectl describe pod — check events
2. Likely: "RuntimeClass 'gvisor' not found" → create the RuntimeClass
3. Or: no nodes match scheduling constraints → check nodeSelector
4. Or: handler "runsc" not configured in containerd → check node config
Summary
| Component | Detail |
|---|---|
| Why sandbox | Standard containers share the host kernel — kernel CVE = node compromise |
| gVisor | User-space kernel (Sentry) intercepts syscalls — no host kernel exposure |
| Kata Containers | Lightweight VM per Pod — hardware-level isolation |
| RuntimeClass | Kubernetes resource mapping a name to a runtime handler |
| Pod config | spec.runtimeClassName: <name> |
| Handler | Must match containerd/CRI-O runtime configuration |
| Overhead | RuntimeClass can declare CPU/memory overhead for scheduling |
| Use case | Multi-tenant, untrusted code, serverless, regulated workloads |
Practice Questions
Q1: What is the fundamental security limitation of standard containers (runc) that sandboxing addresses?
Standard containers share the host kernel with all other containers and host processes. Every system call from every container goes directly to the same kernel.
This means:
- A kernel vulnerability (CVE) exploitable from any container can compromise the entire node
- All containers on the node are at risk, not just the one running the exploit
- The container namespace boundary is a software isolation that depends on kernel correctness
Sandboxing addresses this by adding a layer between the container and the host kernel:
- gVisor intercepts syscalls in user space — the container never talks to the host kernel for most operations
- Kata Containers gives each Pod its own kernel inside a VM — even if the guest kernel is compromised, the host kernel is protected by the hypervisor
The isolation model:
runc: Container → Host Kernel (shared, single point of failure)
gVisor: Container → gVisor Sentry → Limited host kernel access
Kata: Container → Guest Kernel → Hypervisor → Host Kernel
Q2: Create a RuntimeClass for gVisor with resource overhead and node scheduling constraints.
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
overhead:
podFixed:
cpu: "250m"
memory: "120Mi"
scheduling:
nodeSelector:
runtime.gvisor: "true"
tolerations:
- key: "runtime"
operator: "Equal"
value: "gvisor"
effect: "NoSchedule"
Explanation:
handler: runsc— matches the gVisor runtime configured in containerdoverhead.podFixed— accounts for gVisor’s own resource consumption (Sentry process + Gofer)scheduling.nodeSelector— ensures Pods are only scheduled on nodes labeledruntime.gvisor=truescheduling.tolerations— allows scheduling on nodes tainted for gVisor workloads
Apply:
kubectl apply -f runtimeclass-gvisor.yaml
kubectl get runtimeclass
Q3: A Pod with runtimeClassName: gvisor is stuck in Pending state. How do you troubleshoot?
Step 1: Check Pod events
kubectl describe pod sandboxed-pod
Common causes and fixes:
| Error/Event | Cause | Fix |
|---|---|---|
RuntimeClass "gvisor" not found |
RuntimeClass doesn’t exist | Create the RuntimeClass resource |
0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector |
No nodes match RuntimeClass scheduling | Label nodes: kubectl label node <name> runtime.gvisor=true |
failed to find runtime handler runsc |
gVisor not installed or containerd not configured | Install gVisor on the node and configure containerd |
Insufficient cpu/memory |
Overhead makes Pod too large | Check if overhead + requests exceed node capacity |
Step 2: Verify RuntimeClass exists
kubectl get runtimeclass gvisor -o yaml
Step 3: Check node labels (if scheduling is configured)
kubectl get nodes --show-labels | grep gvisor
Step 4: Verify containerd config on the node (if handler not found)
ssh node01
cat /etc/containerd/config.toml | grep runsc
Q4: Compare gVisor and Kata Containers. When would you choose one over the other?
| Criteria | Choose gVisor | Choose Kata |
|---|---|---|
| Syscall compatibility | App uses common syscalls (~70% coverage) | App needs full Linux syscall support |
| Performance | Acceptable syscall overhead for your workload | Need near-native syscall performance |
| Hardware | No special hardware required | Hardware virtualization (VT-x/AMD-V) available |
| Memory | Constrained environment (~50–120MB per sandbox) | Can afford higher memory (~128MB+ per VM) |
| Use case | Multi-tenant SaaS, CI/CD runners, untrusted code | Regulated workloads, financial services, healthcare |
| Isolation strength | Strong (user-space kernel) | Strongest (VM boundary — hardware isolation) |
| Container startup | Fast (~150ms) | Slower (~500ms–1s for VM boot) |
General guidance:
- gVisor — default choice for multi-tenant isolation. Lower overhead, no hardware requirements, good enough for most security needs.
- Kata — when you need guaranteed hardware-level isolation or full syscall compatibility. Common in regulated industries where “VM isolation” is a compliance requirement.
- runc — remains the right choice for trusted, internal workloads where performance is critical and the container images are fully controlled.
Q5: Write a complete Pod spec that uses a gVisor sandbox runtime AND passes the Restricted PSS level.
apiVersion: v1
kind: Pod
metadata:
name: sandboxed-secure-app
namespace: production
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-distroless
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
This Pod has:
- gVisor sandboxing via
runtimeClassName: gvisor— syscall isolation - Restricted PSS compliance —
runAsNonRoot,drop: ["ALL"],allowPrivilegeEscalation: false, Seccomp - Read-only root filesystem with
emptyDirfor temporary files - No service account token — reduces credential theft risk
- Resource limits — prevents resource exhaustion
- Distroless image — minimal attack surface (no shell, no package manager)
| Previous | Up | Next |
|---|---|---|
| Security Contexts Deep Dive | Pod Security | mTLS & Pod-to-Pod Encryption |