Skip to content

The driver sidecar on GKE

How can an ask or deny-risky turn on GKE keep its driver out of the agent's reach? This page is the design, for whoever reviews or finishes it. Part of the security model.

Status: built, off by default (KAN-186). With the setting on, ask, deny-risky and governed turns run their driver in the sidecar. HANGAR_UID_SPLIT=required then lets them run where the pod passes hangar's probe, and refuses them elsewhere. The setting stays off until the checks in Before enabling pass on the dev cluster. Until then H-1 stays mitigated, not closed.

Why Docker's split doesn't carry over

On Docker, the uid split gives the driver a user of its own inside the sandbox. Docker's exec picks the uid, and root sets the user up. A GKE Sandbox pod has neither: its container runs as one non-root user with no capabilities. So the second user has to come from a second container.

The pod

HANGAR_K8S_DRIVER_SIDECAR=true adds a hangar-driver container to each sandbox pod (add_driver_sidecar in providers/kubernetes.py). It runs the environment's own image, so nothing new is pulled.

sandbox (the agent) hangar-driver
uid 1000 1001, no capabilities, allowPrivilegeEscalation: false, read-only root
hangar-home (/var/lib/hangar): the driver, run scripts, hangar's harness settings read-only read-write
hangar-ctl (/var/lib/hangar-ctl, in memory): run keys, commands, approvals, tmux not mounted read-write
PID namespace its own (shareProcessNamespace: false) its own

Writes are stopped by the mounts, not by file modes. The pod's fsGroup makes both volumes group-writable for the agent's group. A pod whose agent container is privileged (a heavy pod with Docker) gets no sidecar. An agent that can be root gains nothing from it.

Starting the harness from another container

The driver can't start a process in the agent's container, so hangar starts relay.py there instead, as the agent, through pods/exec. The run's relay token goes on its stdin, never on a command line. The relay makes itself non-dumpable before it reads the token.

The driver (driver.py --relay) listens on the pod's loopback and writes its port to hangar-home. The relay connects twice: io carries the harness's stdin and stdout, and ctl its stderr, exit status and signals. The driver takes the first two connections that say the token, then stops listening. Anything else on the loopback is dropped.

The relay and the harness are the agent's processes. The agent can stop them or lie through them, as it can through the harness under the Docker split. It can't reach the driver's key, control files, memory or approvals.

How a turn runs there

What Where
The driver, its tmux session, the run key, relay token, commands and approvals hangar-driver, in hangar-ctl
The run's script, log and exit status, and hangar's harness settings hangar-home: the driver writes them, the agent reads them
The relay and the harness sandbox, as the agent, started through pods/exec
hangar's reads and writes: events, approvals, status, terminals, steer commands pods/exec into hangar-driver, never envd

An auto turn runs in the agent's container as before. Before a pod is used, hangar reads its spec through the API and checks each container from inside. A pod out of shape keeps the sandbox single-user, and the turn says why.

Per-edit trees and the verify gate

Both run in the agent's container, as the agent, through the relay. Trees from there are trusted: false. Trees and the verify gate under the sidecar says how, and why not a shared checkout.

What gVisor gives

gVisor runs the whole pod in one sandbox kernel, so isolation between its containers is gVisor's to enforce. These claims were checked in the gVisor source (google/gvisor at 4777ee7, 27 October 2026), not on a cluster.

Claim Where
A container without a shared PID namespace gets a new one: it can't see, signal or trace the driver runsc/boot/loader.go, startSubcontainer
ptrace and /proc/<pid>/mem need the same uid, or CAP_SYS_PTRACE; non-dumpable is honoured; Yama defaults to scope 1 pkg/sentry/kernel/ptrace.go, kernel.go
Each container has its own mount namespace; a shared volume is mounted with each container's own options, so readOnly holds per container runsc/boot/vfs.go, mountSharedSubmount
GKE's admission controller and the shim make emptyDirs sandbox-internal tmpfs, shared at pod level pkg/shim/v1/utils/volumes.go

Not checked: that GKE's admission policy accepts the second container, and the loopback between containers. hangar's service sidecars already rely on that loopback.

What it doesn't cover

  • Shared by the pod: the network namespace, SysV IPC and /dev/shm. The driver uses none of them except its token-checked listener.
  • The sandbox kernel itself. A gVisor escape reaches every container in the pod.
  • Trusted trees in turns that ask. The agent's container hashes them (above), so they are trusted: false. hangar's verify still runs after the turn, whatever the gate said.
  • Graders. A grade on GKE stays trust: best_effort until graders are built (KAN-188).
  • E2B. One microVM, one container. Those turns are refused there by default (the driver's own user on E2B).
  • Replays don't depend on the uid split. Their sandbox has no credentials or egress, and the gateway compares outside it.

Graders

The same shape fits sealed graders (KAN-188). The hidden tests go to hangar-home, read-only for the trial's code. The grade wrapper runs in hangar-driver. The grader's exit status and output come from the exec API, and its report reaches the wrapper on a loopback socket. That would make GKE grades trust: isolated. It isn't built yet.

Cost

Each sandbox reserves 50m CPU and 64 MiB more, up to 64 MiB of it for the in-memory control volume. That is about 5% of a 2-CPU sandbox's CPU request. Each turn takes one more exec to start the relay. These are estimates, not measurements.

Moved from this page

Section Now on
Before enabling Turning the driver sidecar on
Checking it on a cluster Turning the driver sidecar on