CKS — Container Runtime Sandboxing

10 minute read

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) configuration
  • overhead.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 installed
  • scheduling.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 runtimeClassName on 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 containerd
  • overhead.podFixed — accounts for gVisor’s own resource consumption (Sentry process + Gofer)
  • scheduling.nodeSelector — ensures Pods are only scheduled on nodes labeled runtime.gvisor=true
  • scheduling.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 emptyDir for 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

Updated: