Sandboxed runtimes: gVisor & Kata

A kernel boundary for untrusted workloads.

Advanced10 min · lesson 17 of 24

Ordinary containers on a node all share one host kernel, so a kernel-level container escape does not land in another container — it lands on the node, with access to every other pod there. seccomp and AppArmor shrink the odds, but they are still filtering calls into the same shared kernel. When a workload is untrusted enough that you cannot accept that risk — multi-tenant platforms, customer-supplied code, anything you did not write — a sandboxed runtime gives that pod its own kernel boundary.

Two mainstream options. gVisor runs a user-space kernel (runsc) that intercepts a container’s syscalls and services most of them itself, so very few ever reach the host kernel. Kata Containers goes further and wraps each pod in a lightweight virtual machine with a real, separate kernel. You expose either through a RuntimeClass and opt a pod in with runtimeClassName — everything else about the pod spec is unchanged.

How much kernel is shared
standard runtime — shared host kernel
runc
syscalls hit host kernel directly
sandboxed — isolated
gVisor (runsc)
user-space kernel intercepts syscalls
Kata
lightweight VM, separate real kernel
An escape from a sandboxed pod lands in the sandbox (the user-space kernel or the VM), not on the node — the whole point.
runtimeclass.yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata: { name: gvisor }
handler: runsc # the gVisor handler, installed + configured on nodes
---
apiVersion: v1
kind: Pod
metadata: { name: untrusted, namespace: sandbox }
spec:
runtimeClassName: gvisor
containers:
- { name: app, image: registry.internal/untrusted:1.0 }

The tradeoff

Isolation is not free: both runtimes add latency and some syscall or feature incompatibility (gVisor implements most but not all of the Linux surface; Kata carries VM overhead). So you apply them selectively — the risky tenant, the untrusted job — often on a dedicated node pool the RuntimeClass schedules onto, rather than cluster-wide. You can confirm a pod really landed in the sandbox from inside it.

terminal
# inside a gVisor pod, the kernel identifies itself as gVisor
$ kubectl exec -n sandbox untrusted -- dmesg 2>/dev/null | grep -i gvisor
... Starting gVisor...
$ kubectl exec -n sandbox untrusted -- uname -a
Linux untrusted 4.4.0 ... gVisor
The handler must exist on the node
A RuntimeClass only names a handler (runsc, kata) that has to be installed and configured on the nodes first — reference one that is not there and the pod stays Pending or fails to start. Install the runtime (and, usually, label the nodes that have it) before you point workloads at the class.