Sandboxed Runtimes
When shared kernel isn't secure enough — gVisor, Kata, Firecracker, and Wasm
The Security Gap: Shared Kernel
Warning
The fundamental problem: Traditional containers (runc) share the host kernel. A kernel exploit in any container = full host compromise.
Trusted workload
Untrusted code / attacker
Another tenant
Why Namespaces + cgroups Aren't Enough
- Attack surface: the Linux kernel exposes ~300+ system calls. Many are complex, historically buggy subsystems (BPF, io_uring, futex, etc.)
- Kernel exploits: a single vulnerability (e.g., Dirty COW, OverlayFS CVEs) can break out of namespaces entirely
- Multi-tenant danger: if you run untrusted code from different customers on the same kernel, one exploit compromises everyone
- seccomp helps but is not a security boundary — it filters syscalls but the kernel still processes them
Two Approaches to Solving This
Reduce attack surface
Intercept syscalls in user space so the host kernel sees only a tiny, well-audited set
Approach: gVisor (user-space kernel)
Eliminate kernel sharing
Give each container its own kernel inside a lightweight VM
Approach: Kata Containers, Firecracker
gVisor (runsc) — User-Space Kernel
What gVisor Is
- Google's sandboxed container runtime (open-source)
- Implements a subset of the Linux kernel in Go — runs entirely in user space
- OCI-compatible: just swap
runcforrunsc(the gVisor binary) - Used by: Google Cloud Run, GKE Sandbox
Architecture
Intercepts and handles all container syscalls in user space. Written in Go. Runs as a regular unprivileged process.
Application thinks it's talking to a real Linux kernel
Handles filesystem access via 9P protocol. Isolated from Sentry — separate process, minimal privileges.
How Syscalls Flow
Container App
Calls read(), write(), open(), etc.
Sentry
Intercepts syscall, handles in user-space Go code
Host Kernel
Only ~20 syscalls (read, write, futex, etc.)
Container sees: ~300 Linux syscalls → Host kernel sees: ~20 syscalls from Sentry
Tradeoffs
- Massively reduced kernel attack surface (~20 vs ~300 syscalls)
- Sentry is written in Go — memory-safe, no buffer overflows
- No hardware virtualization needed (no KVM)
- Drop-in replacement for runc (OCI-compatible)
- Fine-grained syscall compatibility tracking
- 2-5x overhead for syscall-heavy workloads (each syscall goes through user-space)
- Not all syscalls supported (e.g., no
io_uring, limitedeBPF) - Filesystem performance limited by 9P protocol to Gofer
- Some applications may behave differently (kernel compatibility gaps)
- Networking has higher latency (netstack in user-space)
Tip
Best for: untrusted code execution, serverless functions, multi-tenant platforms, CI/CD build runners — anywhere you run code you don't fully trust.
Kata Containers — Per-Container Lightweight VMs
What Kata Containers Does
- Each container runs in its own lightweight virtual machine with its own kernel
- Combines VM-level isolation with container-level ergonomics (OCI-compatible)
- Uses QEMU or Firecracker as the VMM (Virtual Machine Monitor)
- Guest kernel is minimal (~30MB) — just enough to run containers
Architecture
Runs inside VM with dedicated kernel
Completely isolated kernel
How It Works
containerd / CRI-O
CRI request to run container
kata-runtime
OCI runtime (replaces runc)
QEMU / Firecracker
Creates lightweight VM with KVM
Guest Kernel
Minimal Linux kernel boots
kata-agent
Manages container inside VM via vsock
Performance Characteristics
- CPU: near-native (hardware virtualization via VT-x/AMD-V)
- Memory: ~30MB overhead per VM for guest kernel + agent
- I/O: small overhead for virtio passthrough
- Startup: ~500ms (VM boot + kernel init + agent)
- Networking: tap device + virtio-net into VM
Key Components
- kata-runtime: OCI runtime binary (called by containerd-shim-kata-v2)
- kata-agent: runs inside the VM, manages the container
- vsock: host-to-VM communication (no network stack needed)
- virtiofsd: filesystem sharing between host and guest
- Guest kernel: stripped-down Linux (~30MB image)
Tip
Best for: workloads needing strong kernel-level isolation with container ergonomics — regulated industries, multi-tenant clouds, running untrusted kernel modules.
Firecracker — Minimal MicroVMs for Serverless
What Firecracker Is
- Amazon's microVM manager — written in Rust
- Powers AWS Lambda and AWS Fargate
- NOT a full QEMU — stripped-down VMM with minimal device model
- Designed for serverless: fast startup, high density
Key Specifications
Note
Note
Note
Firecracker vs QEMU
- ~70 emulated devices
- Legacy hardware support (IDE, PS/2, VGA, PCI bridges...)
- Huge codebase (~1.4M lines of C)
- Boot time: seconds
- Memory overhead: ~100MB+ per VM
- Attack surface: large
- ~4 emulated devices only:
virtio-net(networking)virtio-blk(block storage)- serial console
- minimal legacy i8042 + RTC
- ~50K lines of Rust (memory-safe)
- Boot time: <125ms
- Memory overhead: ~5MB
How It Integrates
- Uses KVM under the hood (requires hardware virtualization)
- Managed via REST API (not OCI directly)
- Can be used as a Kata Containers backend (instead of QEMU)
- firecracker-containerd: integration with containerd for running containers in Firecracker microVMs
containerd
CRI / gRPC API
firecracker-containerd
Shim that manages Firecracker VMs
Firecracker
Creates microVM via KVM
Container
Runs inside microVM
Tip
Best for: serverless platforms (Lambda-style), high-density multi-tenant workloads, any scenario needing thousands of isolated environments with fast cold starts.
WebAssembly (Wasm) Runtimes
Wasm as a Container Runtime
- Wasm containers via
runwasi— a containerd shim for Wasm runtimes - Not a VM, not a Linux process — a Wasm module runs in a sandboxed Wasm VM
- Runtimes: wasmtime (Bytecode Alliance), WasmEdge (CNCF), Wasmer
- Applications must be compiled to Wasm (from Rust, Go, C/C++, Python, etc.)
Key Characteristics
Strengths
- Sub-millisecond cold start (<1ms)
- Tiny footprint (~KB per module, not MB)
- Sandboxed by design — no access to host unless explicitly granted
- Portable — same binary runs on any OS/architecture
- Near-native performance for compute (AOT compiled)
Limitations
- No Linux syscalls — only WASI (WebAssembly System Interface) APIs
- Limited filesystem access — must explicitly grant directory access
- No raw sockets — networking is evolving (wasi-sockets)
- Thread support — only recently standardized (wasm-threads proposal)
- Ecosystem maturity — not all libraries/frameworks support Wasm yet
WASI — WebAssembly System Interface
WASI is the standardized system API for Wasm modules — it's what allows Wasm to interact with the outside world:
wasi-filesystem— pre-opened directory handles (capability-based)wasi-sockets— TCP/UDP networking (in development)wasi-http— HTTP request/response handlingwasi-cli— stdin/stdout/stderr, environment variables, args
Think of WASI as a capability-based POSIX — you only get access to what the host explicitly grants.
Integration with Kubernetes
kubelet
CRI request
containerd
Selects Wasm shim
containerd-shim-runwasi
Wasm-specific shim
wasmtime / WasmEdge
Executes Wasm module
Tip
Best for: edge computing, plugin systems, serverless functions, CDN workers (Cloudflare Workers, Fastly Compute), anywhere you need sub-millisecond startup with strong sandboxing.
Comparison Matrix — All Runtimes
| Runtime | Isolation | Startup | Overhead | Syscall Support | Best For |
|---|---|---|---|---|---|
| runc | Namespaces + cgroups | ~100ms | Minimal | Full Linux | General purpose |
| gVisor (runsc) | User-space kernel | ~200ms | 2-5x syscalls | Partial (~70%) | Untrusted code |
| Kata Containers | MicroVM + own kernel | ~500ms | ~30MB + VM | Full Linux | Strong isolation |
| Firecracker | MicroVM (minimal) | <125ms | ~5MB | Full Linux | Serverless, high density |
| Wasm (runwasi) | Wasm sandbox | <1ms | ~KB | WASI only | Edge, plugins |
Note
Tradeoff spectrum: As you move from runc → gVisor → Kata/Firecracker, you get stronger isolation but higher overhead. Wasm breaks the pattern entirely — incredible startup and density, but a fundamentally different programming model (no Linux ABI).
Isolation Models — Side by Side
NS + cgroups only
NS + cgroups only
User-space syscall handling
Wasm sandbox, no kernel access, capability-based WASI
Each module fully isolated, ~KB footprint
Kubernetes RuntimeClass — Per-Pod Runtime Selection
How Kubernetes Selects a Runtime
The RuntimeClass resource lets you run different pods with different runtimes on the same cluster:
# Define a RuntimeClass for gVisor
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc # matches containerd's runtime config name
scheduling:
nodeSelector:
runtime/gvisor: "true" # only schedule on nodes that have gVisor installed
overhead:
podFixed:
memory: "64Mi" # account for gVisor's memory overhead in scheduling
# Use it in a Pod spec
apiVersion: v1
kind: Pod
metadata:
name: untrusted-workload
spec:
runtimeClassName: gvisor # use gVisor instead of default runc
containers:
- name: sandbox
image: nginx:latest
Common RuntimeClass Configurations
| RuntimeClass Name | Handler | Runtime | Use Case |
|---|---|---|---|
gvisor | runsc | gVisor | Untrusted code, multi-tenant |
kata | kata | Kata Containers | VM-level isolation |
wasm | wasmtime | runwasi + wasmtime | Wasm workloads |
| (default / none) | runc | runc | Standard containers |
Note
scheduling.nodeSelector ensures pods land on nodes that actually have the requested runtime installed. Without this, a pod requesting gvisor could be scheduled on a node without gVisor, and it would fail to start.
containerd Runtime Configuration
To register alternative runtimes with containerd, add them to /etc/containerd/config.toml:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
runtime_type = "io.containerd.kata.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmtime]
runtime_type = "io.containerd.wasmtime.v1"
Tip
Mix and match: On a single Kubernetes cluster, you can run trusted workloads with runc (lowest overhead), untrusted user code in gVisor (reduced kernel exposure), and regulated workloads in Kata (full kernel isolation) — all selected per-pod via RuntimeClass.