Ausführen von Docker-in-Docker innerhalb von gVisor und verschachtelten Sandboxes mit gvx

Schnelllösung: Sicheres Ausführen von docker:dind in einer unprivilegierten gVisor (runsc)-Sandbox
Cloud-KI-Agenten-Rechenumgebungen (etwa Google Agent Compute Engine-Sandboxes), Kubernetes-agent-sandbox-Deployments (RuntimeClass: gvisor) und Evaluierungs-Harnesses für Reinforcement Learning isolieren nicht vertrauenswürdige Workloads gezielt in gVisor (runsc)-Containern, um den Host-Kernel zu schützen. Versucht ein Workload innerhalb eines standardmäßigen, unprivilegierten runsc-Containers, docker.io oder docker-ce über apt-get zu installieren, service docker start oder dockerd zu starten und docker build auszuführen, schlägt dies fehl, selbst wenn er innerhalb des Containers als root (UID 0) läuft.
Mit gvx (rungv) lassen sich docker:dind, docker build und docker compose sicher direkt in einem unprivilegierten gVisor (runsc)-Container ausführen (oder gvx innerhalb von gvx verschachteln), ohne die äußere RuntimeClass zu schwächen, Host-Sockets freizugeben oder --privileged zu gewähren.
Aufwandsarme, token-effiziente Lösung für KI-Agenten: In einer cloudbasierten Agentenumgebung mit gVisor-Unterbau kann der Versuch,
docker.iozu installieren und manuell Fehler bei/etc/init.d/dockerulimit, Fehlschläge beiiptables-nft, Zugriffsverweigerungen bei rohen Sockets voniptables-legacy, Mount-Fehler beioverlay2sowie Abstürze vondocker buildbeirunc/pivot_rootzu umgehen, typischerweise über 20 Tool-Call-Runden und zehntausende Token verbrauchen, bevor er vollständig fehlschlägt.gvx(rungv) ist eine aufwandsarme, token-effiziente Drop-in-Lösung: Der Agent führt in einer einzigen Runde 3 Befehle aus, um einen funktionsfähigen Docker-Daemon im User-Space zu starten, und führt anschließend unmittelbardocker buildunddocker composeaus, ohne Projektdateien zu verändern.
Erhalt und Stärkung der Isolation der äußeren Sandbox: Die Ausführung von
rungvinnerhalb vonrunscumgeht keine einzige Einschränkung, die von der äußeren gVisor-Sandbox durchgesetzt wird. Stattdessen erzeugtgvxeinen zweiten, verschachtelten gVisor-Sentry-Kernel im User-Space mit eigenemhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), eigenem In-Memory-netstackund einem Einzeldatei-Volumeext4lite. Inneredockerd- undrunc-Container sind doppelt gesandboxt: Ihre Systemaufrufe, Mounts und Rohpakete verlassen niemals den innerengvx-Kernel im User-Space.
1# Innerhalb eines bestehenden gVisor (runsc)-Containers oder Kubernetes-Pods:
2# rungv aus einem SLSA-attestierten GitHub-Release-Archiv oder über go install installieren:
3CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
4
5mkdir -p ~/.gvx
6rungv run \
7 -p unix:~/.gvx/docker.sock:2375 \
8 -v gvx-docker:/var/lib/docker \
9 -e DOCKER_TLS_CERTDIR= \
10 docker:dind --tls=false &
11
12# Standardmäßige Docker-Builds und Container gegen den inneren gvx-Socket ausführen
13export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
14until docker info >/dev/null 2>&1; do sleep 0.5; done
15docker info --format '{{.Driver}} {{.OperatingSystem}}'
16# Ausgabe: overlayfs Alpine Linux ... (containerisiert)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
Fünf Gründe, warum die direkte Docker-Installation innerhalb von gVisor (runsc) fehlschlägt
Wenn ein Benutzer oder ein KI-Coding-Agent apt-get install -y docker.io (oder docker-ce) innerhalb eines standardmäßigen runsc-Containers ausführt und anschließend dockerd zu starten versucht, stößt er auf fünf aufeinanderfolgende technische Hürden.
1. Fehler des Init-Skripts (systemctl / service docker start ulimit) und des Kernelmoduls (modprobe, 4.4.0)
Nach apt-get install -y docker.io (oder docker-ce) schlägt der Versuch, den Daemon über systemctl start docker oder service docker start zu starten, sofort fehl, weil PID 1 nicht systemd ist und /etc/init.d/docker versucht, ulimit -n über die harte RLIMIT_NOFILE-Obergrenze der Sandbox anzuheben. Wird dockerd anschließend direkt aufgerufen, rufen die Initialisierungsskripte modprobe auf, um overlay, bridge, br_netfilter, ip_tables und nf_nat gegen die virtuelle Kernelversion von gVisor (4.4.0) zu laden. Dieses Verzeichnis /lib/modules/4.4.0 existiert jedoch nicht:
1System has not been booted with systemd as init system (PID 1). Can't operate.
2Failed to connect to bus: Host is down
3/etc/init.d/docker: 62: ulimit: error setting limit (Operation not permitted)
4level=warning msg="Running modprobe bridge br_netfilter failed with message: modprobe: WARNING: Module bridge not found in directory /lib/modules/4.4.0\nmodprobe: WARNING: Module br_netfilter not found in directory /lib/modules/4.4.0\n, error: exit status 1"
5modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
6modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
7modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
8modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
2. iptables-nft (nf_tables-Netlink), Zugriffsverweigerungen bei rohen Sockets von iptables-legacy und Fehler bei der docker0-Bridge
Unter Ubuntu 22.04, Ubuntu 24.04 und Debian 11/12 wählt apt-get install docker.io iptables-nft (gestützt auf das Netlink-Subsystem nf_tables) als Standardalternative für /usr/sbin/iptables aus. Selbst wenn der Benutzer oder Agent auf iptables-legacy umschaltet (update-alternatives --set iptables /usr/sbin/iptables-legacy), lehnen standardmäßige unprivilegierte gVisor-Sandboxes (--net-raw=false) Sockets vom Typ AF_INET, SOCK_RAW mit EPERM ab. Dadurch schlägt iptables-legacy mit Permission denied (you must be root) fehl, selbst wenn der Prozess als UID 0 läuft:
1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to register "bridge" driver: failed to create NAT chain DOCKER: iptables failed: iptables --wait -t nat -N DOCKER: iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
2iptables v1.8.10 (legacy): can't initialize iptables table `nat': Permission denied (you must be root)
3Perhaps iptables or your kernel needs to be upgraded.
4iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
5iptables: No chain/target/match by that name.
6Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i docker0 -j RETURN: iptables: No chain/target/match by that name.)
7Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
3. Fehler von overlay2 auf einem gofer-/9p-Rootfs und Erschöpfung des Speichers beim Treiber vfs
Sofern der Administrator des äußeren runsc nicht ausdrücklich ein dediziertes tmpfs oder Filestore-Mount unter /var/lib/docker vorkonfiguriert hat, wird das Root-Dateisystem des Containers durch den gofer (9p/lisafs) von gVisor oder durch ein verschachteltes Overlay-Mount bereitgestellt. Das Mounten von overlay2 über /var/lib/docker schlägt fehl:
1level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
2failed to mount overlay: invalid argument
3Error starting daemon: error initializing graphdriver: driver not supported: overlay2
Versuchen Benutzer oder Agenten, diesen Fehler zu umgehen, indem sie dockerd --storage-driver=vfs starten, deaktiviert Docker die Copy-on-Write-Freigabe von Layern vollständig. Jede RUN-Anweisung in einem Dockerfile führt dann eine vollständige rekursive Kopie (dirCopy) des gesamten Root-Dateisystems durch. Ein 500-MB-Image über 10 Build-Schritte vervielfacht sich so auf 5 GB Schreibvorgänge und beendet die Sandbox mit no space left on device oder einem I/O-Timeout.
4. Fehler bei docker build (BuildKit) und verschachtelten runc-Aufrufen (pivot_root, /proc, Cgroups und DNS)
Selbst wenn dockerd mit dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs erzwungen gestartet wird, schlagen docker build -t app . oder docker run sofort fehl, sobald BuildKit oder runc den Build-Container startet:
1ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully:
2docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process:
3error during container init: error jailing process inside rootfs: pivot_root .: invalid argument
4error during container init: error mounting "cgroup" to rootfs at "/sys/fs/cgroup": mount cgroup:/sys/fs/cgroup/cpuset (via /proc/self/fd/6), flags: 0xf, data: cpuset: operation not permitted
5cgroups: cgroup mountpoint does not exist: unknown
6runc create failed: systemd not running on this host: cannot use systemd cgroup manager
7error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
8error during container init: error loading seccomp filter into kernel: operation not permitted
9Temporary failure resolving 'deb.debian.org'
10Temporary failure resolving 'archive.ubuntu.com'
5. Verschachteltes runsc schlägt mit SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY) fehl
Versucht ein Benutzer, eine weitere runsc-Instanz innerhalb von runsc mit der Plattform systrap auszuführen, bricht die Initialisierung sofort ab:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Sowohl der Linux-Kernel als auch der gVisor-Sentry erlauben höchstens einen Listener vom Typ SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) pro Task-Hierarchie. Da die äußere Sandbox diesen Listener-Slot bereits belegt, gibt der innere Aufruf von seccomp(2) EBUSY (device or resource busy) zurück.
Wie gvx die verschachtelte Ausführung innerhalb von gVisor löst
gvx begegnet allen fünf Hürden in seinem eigenen Kernel im User-Space und seiner Plattformschicht. Dadurch kann rungv innerhalb eines äußeren gVisor (runsc)-Containers oder sogar rekursiv innerhalb eines weiteren gvx-Containers (gvx in gvx) laufen.

1. Automatischer Fallback von systrap von EBUSY auf die Futex-Schleife _RUN_SYSCALL_LOOP
Unter Linux systrap führt jeder Stub-Subprozess zwei Arten von Threads aus:
- Gast-Ausführungsthreads (
sysmsg): Fangen Gast-Systemaufrufe überSECCOMP_RET_TRAP(SIGSYS) und binär gepatchtejmp rel32-Trampoline (syshandler) ab und tauschen den Registerzustand über gemeinsamen Speicher undSYS_FUTEXmit dem Sentry aus. - Ein Stub-
syscallThread(syscall_thread.go): Führt Systemaufrufe zur Adressraumverwaltung (mmap,munmap,clone) im Stub-Subprozess im Auftrag des Sentry aus.
Im upstream-gVisor unterstützt syscallThread zwei Weckmodi in stub_amd64.s und stub_arm64.s:
- Seccomp-User-Notify-Schleife (
_RUN_SECCOMP_LOOP = 6, wennt.seccompNotify != nil): WährendsyscallThread.init()installiertt.installSeccompNotify()einen Listener vom TypSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), damit der Sentry Anfragen überioctl(SECCOMP_IOCTL_NOTIF_SEND)undioctl(SECCOMP_IOCTL_NOTIF_RECV)verteilen kann. - Futex-Systemaufrufschleife (
_RUN_SYSCALL_LOOP = 5, wennt.seccompNotify == nil): DersyscallThreadführtsyscall_loop(stub_amd64.s/stub_arm64.s) aus und schläft aufSYS_FUTEX(FUTEX_WAIT) über dem gemeinsamen 2-Seiten-BereichsyscallSentryMessage(PROT_READim Stub) undsyscallStubMessage(PROT_READ | PROT_WRITEim Stub).
Upstream-gVisor ruft t.installSeccompNotify() bedingungslos auf, sobald seccompNotifyIsSupported wahr ist, und bricht die Erstellung des Stubs ab, wenn seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) EBUSY zurückgibt.
In gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go und syscall_thread.go) ruft syscallThread.init() stattdessen t.installSeccompNotifyIfFree() auf:
1// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
2var seccompNotifyBusy atomic.Bool
3
4func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
5 if seccompNotifyBusy.Load() {
6 return nil, nil
7 }
8 f, err := t.installSeccompNotify()
9 if errors.Is(err, unix.EBUSY) {
10 if seccompNotifyBusy.CompareAndSwap(false, true) {
11 log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
12 "The stub threads use the slower futex loop instead of seccomp-unotify.")
13 }
14 return nil, nil
15 }
16 return f, err
17}
Gibt installSeccompNotify() unix.EBUSY zurück, weil ein äußeres gVisor (runsc) oder eine äußere gvx-Sandbox den einzigen Listener-Slot der Prozess-Filterkette bereits belegt:
- Setzt
installSeccompNotifyIfFree()seccompNotifyBusy(atomic.Bool), protokolliert genau eine Warnung für den Prozess und belässtt.seccompNotify == nilfür diesen Stub und alle nachfolgenden Stubs. - In
syscallThread.detach()(syscall_thread_amd64.goundsyscall_thread_arm64.go) setztt.seccompNotify == nilaufamd64regs.Rbx = _RUN_SYSCALL_LOOP(5) bzw. aufarm64regs.Regs[9] = _RUN_SYSCALL_LOOP, anstelle von_RUN_SECCOMP_LOOP(6). - In
syscallThread.syscall()(syscall_thread.go) verteilt der innere Sentry Stub-Systemaufrufe ausschließlich über gemeinsamen Speicher undSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Benachrichtigt den Syscall-Thread über eine neue Syscall-Anfrage.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Auf Antwort warten.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Da SECCOMP_RET_TRAP-Filter auf Gast-Threads beliebig tief geschichtet werden können und syscallThread auf _RUN_SYSCALL_LOOP zurückfällt, ohne einen zweiten User-Notification-Listener anzufordern, läuft gvx sauber innerhalb von runsc und unterstützt rekursive Verschachtelung (gvx in gvx).
2. Natives overlayfs auf Einzeldatei-Volumes ext4lite
Um zu vermeiden, dass overlayfs über dem gofer-Root-Dateisystem (9p/lisafs) des äußeren gVisor-Containers gemountet wird, legt -v gvx-docker:/var/lib/docker /var/lib/docker auf dem internen Dateisystem ext4lite von gvx ab, das in einer einzigen regulären Datei (gvx-docker.ext4) gespeichert ist.
Für das äußere runsc ist gvx-docker.ext4 lediglich eine reguläre Datei, auf die über pread64(2) und pwrite64(2) zugegriffen wird. Innerhalb von gvx stellt ext4lite ein JBD2-journalisiertes ext4-Dateisystem bereit, mit erweiterten Attributen trusted.overlay.* und security.capability, Whiteout-Knoten als Zeichengeräte (0:0), d_type-Verzeichniseinträgen und RENAME_EXCHANGE. Dadurch kann der innere dockerd seinen nativen Speichertreiber overlayfs verwenden.
3. Eigenständiger netstack mit docker0 und iptables-legacy
Innerhalb von gvx interagiert dockerd weder mit den Netzwerkschnittstellen des äußeren runsc-Containers noch mit dessen netfilter-Tabellen. Stattdessen gilt:
dockerderzeugt seine Bridgedocker0undveth-Paare im eigenennetstackvongvxim User-Space.- Alle Regeln von
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) werden gegen die interne netfilter-Implementierung vongvxausgeführt. - Verschachtelte
runc-Container mounten ihre Hierarchien/proc,/sysund/sys/fs/cgroupinnerhalb der In-Memory-Mount-TabelleVFS2vongvxund rufen niemals den Systemaufrufmount(2)des äußerenrunscauf.
Verifizierung von verschachteltem docker:dind innerhalb von gVisor (oder gvx in gvx)
Beide Szenarien (rungv innerhalb von runsc oder rungv innerhalb von rungv) lassen sich mit den folgenden Befehlen überprüfen:
1# Innerhalb eines äußeren gVisor (runsc)-Containers oder eines äußeren rungv-Containers:
2mkdir -p ~/.gvx
3rungv run \
4 -p unix:~/.gvx/docker.sock:2375 \
5 -v gvx-docker:/var/lib/docker \
6 -e DOCKER_TLS_CERTDIR= \
7 docker:dind --tls=false &
8DIND_PID=$!
9
10export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
11until docker info >/dev/null 2>&1; do sleep 0.5; done
12
13# Prüfen, ob der innere Docker-Daemon natives overlayfs (nicht vfs) verwendet
14docker info --format 'Storage Driver: {{.Driver}}'
15# Ausgabe: Storage Driver: overlayfs
16
17# Einen mehrschichtigen Build ausführen und einen verschachtelten Container starten
18mkdir -p /tmp/nested-test && cd /tmp/nested-test
19cat <<'EOF' > Dockerfile
20FROM alpine:3.20
21RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
22USER appuser
23WORKDIR /app
24RUN echo "nested-build-ok" > status.txt
25CMD ["cat", "/app/status.txt"]
26EOF
27
28docker build -t nested-test:latest .
29docker run --rm nested-test:latest
30# Ausgabe: nested-build-ok
31
32kill -INT $DIND_PID
Weiterführende Literatur in dieser Reihe
- gvx: Linux-Container, Docker und Kubernetes in einem unprivilegierten Prozess ausführen: Überblick über
gvxunter Windows (whpx,systrap), macOS (hvf) und Linux (systrap). - Docker, docker build und docker compose ohne Root-Rechte oder User Namespaces ausführen: Wie
gvxdocker:dindausführt, wennroot,sudo,unshare(CLONE_NEWUSER),mount(2)und/dev/fuseblockiert sind. - Docker-in-Docker innerhalb von gVisor und verschachtelten Sandboxes mit gvx ausführen (dieser Beitrag)
- Schnelle isolierte Docker-Umgebungen für RL-Rollouts von KI-Agenten mit gvx: Wie sich
gvxund Copy-on-Write-Snapshots mit einzelnenext4lite-Dateien nutzen lassen, um Hunderte paralleleSWE-bench- und RL-Rollout-Umgebungen mit Zurücksetzungen in 100 Millisekunden auszuführen.