GoSuda

Spúšťanie Docker-in-Docker vnútri gVisor a Nested Sandboxes s gvx

By Lemon Mint views ...

Rýchle riešenie: Bezpečné spúšťanie docker:dind vnútri neprivilegovaného sandboxu gVisor (runsc)

Cloudové platformy pre AI agentov, nasadenia agent-sandbox v Kubernetes (RuntimeClass: gvisor) a evaluačné rámce pre reinforcement learning izolujú nedôveryhodné úlohy v kontajneroch gVisor (runsc), konkrétne na ochranu jadra hostiteľa. Keď sa úloha v predvolenom neprivilegovanom kontajneri runsc pokúsi nainštalovať docker.io alebo docker-ce pomocou apt-get a spustiť dockerd, zlyhá aj vtedy, ak má v kontajneri oprávnenia root (UID 0).

Pomocou gvx (rungv) môžete bezpečne spúšťať docker:dind, docker build a docker compose priamo vnútri neprivilegovaného kontajnera gVisor (runsc) (alebo vnoriť gvx do gvx) bez oslabenia vonkajšej RuntimeClass, bez sprístupnenia hostiteľských socketov a bez udelenia --privileged.

Zachovanie a posilnenie izolácie vonkajšieho sandboxu: Spustenie rungv vnútri runsc neobchádza ani jedno obmedzenie vynucované vonkajším sandboxom gVisor. Namiesto toho gvx vytvorí druhé, vnorené jadro gVisor Sentry v používateľskom priestore s vlastným hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), vlastným netstack v pamäti a jedným súborovým zväzkom ext4lite. Vnútorné kontajnery dockerd a runc sú dvojito sandboxované: ich systémové volania, pripojenia a surové pakety nikdy neopustia vnútorné jadro gvx v používateľskom priestore.

 1# Vnútri existujúceho kontajnera gVisor (runsc) alebo Kubernetes Pod:
 2# Nainštalujte rungv z archívu vydania na GitHube s SLSA atestáciou alebo pomocou go install:
 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# Spúšťajte štandardné Docker buildy a kontajnery voči vnútornému socketu gvx
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# Výstup: overlayfs Alpine Linux ... (v kontajneri)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Päť dôvodov, prečo priama inštalácia Dockeru zlyhá vnútri gVisor (runsc)

Keď používateľ alebo AI agent pre programovanie spustí apt-get install -y docker.io (alebo docker-ce) vnútri štandardného kontajnera runsc a pokúsi sa spustiť dockerd, narazí na päť za sebou nasledujúcich technických prekážok.

1. Načítanie kernel modulov (modprobe) voči virtuálnemu jadru 4.4.0

Počas inštalácie balíka a spúšťania dockerd inicializačné skripty spúšťajú modprobe na načítanie modulov overlay, br_netfilter, ip_tables, nf_nat a xt_conntrack. Vonkajší Sentry gVisor blokuje init_module(2) a finit_module(2) a hlási virtuálnu verziu jadra (4.4.0), pre ktorú neexistuje adresár /lib/modules/4.4.0:

1modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
2modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
3modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
4modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0

2. Zlyhania iptables-nft (netlink nf_tables) a mostu docker0

Na Ubuntu 22.04, Ubuntu 24.04 a Debian 11/12 príkaz apt-get install docker.io vyberie iptables-nft (založený na netlink podsystéme nf_tables) ako predvolenú alternatívu /usr/sbin/iptables. V štandardnom kontajneri gVisor bez sieťových administrátorských oprávnení alebo podpory netlink nf_tables dockerd zlyhá pri inicializácii reťazca NAT DOCKER a mostu docker0:

1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER
2iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
3iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
4iptables: No chain/target/match by that name.
5Unable 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.)
6Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted

3. Zlyhanie overlay2 na rootfs gofer / 9p a vyčerpanie disku pri vfs

Pokiaľ vonkajší administrátor runsc výslovne nepredkonfiguroval vyhradené pripojenie tmpfs alebo filestore na /var/lib/docker, koreňový súborový systém kontajnera je podložený gofer (9p/lisafs) od gVisor alebo vnoreným overlay pripojením. Pripojenie overlay2 na /var/lib/docker zlyhá:

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

Keď sa používatelia alebo agenti pokúsia túto chybu obísť spustením dockerd --storage-driver=vfs, Docker úplne vypne zdieľanie vrstiev Copy-on-Write. Každá inštrukcia RUN v Dockerfile vykoná úplnú rekurzívnu kópiu (dirCopy) celého koreňového súborového systému, takže obraz s veľkosťou 500 MB v 10 krokoch zostavenia vytvorí 5 GB zápisov a ukončí sandbox chybou no space left on device alebo časovým limitom I/O.

4. Odmietnutie vnorených cgroup a pripojení /proc v runc

Aj keď je dockerd vynútene spustený s dockerd --iptables=false --bridge=none --storage-driver=vfs, spustenie docker run alebo docker build vyvolá runc, ktorý vytvorí vnútorný kontajner. V predvolenom neprivilegovanom kontajneri gVisor runc zlyhá pri pripájaní cgroup, opätovnom pripájaní /proc alebo načítaní vnútorných filtrov seccomp:

1docker: 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:
2error 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
3cgroups: cgroup mountpoint does not exist: unknown
4runc create failed: systemd not running on this host: cannot use systemd cgroup manager
5error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
6error during container init: error loading seccomp filter into kernel: operation not permitted

5. Vnorený runsc zlyhá s SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)

Ak sa používateľ pokúsi spustiť ďalšiu inštanciu runsc vnútri runsc s platformou systrap, inicializácia sa okamžite preruší:

1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy

Linuxové jadro aj Sentry gVisor povoľujú najviac jeden listener SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) na hierarchiu úloh. Keďže vonkajší sandbox už drží tento slot listenera, vnútorné volanie seccomp(2) vráti EBUSY (device or resource busy).


Ako gvx rieši vnorené spúšťanie vnútri gVisor

gvx rieši všetkých päť prekážok vo vlastnom jadre v používateľskom priestore a vrstve platformy, čo umožňuje rungv bežať vnútri vonkajšieho kontajnera gVisor (runsc) alebo dokonca rekurzívne vnútri ďalšieho kontajnera gvx (gvx v gvx).

Vnorené vykonávanie gvx vnútri vonkajšieho gVisor

1. Automatický záložný režim systrap z EBUSY na futex slučku _RUN_SYSCALL_LOOP

V Linux systrap každý podproces stub spúšťa dva druhy vlákien:

  • Vlákna vykonávania hosťa (sysmsg): zachytávajú systémové volania hosťa cez SECCOMP_RET_TRAP (SIGSYS) a binárne upravené skoky jmp rel32 (syshandler), pričom si so Sentry vymieňajú stav registrov cez zdieľanú pamäť a SYS_FUTEX.
  • Stub syscallThread (syscall_thread.go): vykonáva systémové volania adresného priestoru (mmap, munmap, clone) vnútri podprocesu stub v mene Sentry.

V upstream gVisor syscallThread podporuje dva režimy prebudenia v stub_amd64.s a stub_arm64.s:

  1. Slučka Seccomp User-Notify (_RUN_SECCOMP_LOOP = 6, keď t.seccompNotify != nil): Počas syscallThread.init() volanie t.installSeccompNotify() nainštaluje listener SECCOMP_RET_USER_NOTIF (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), aby Sentry mohla odosielať požiadavky cez ioctl(SECCOMP_IOCTL_NOTIF_SEND) a ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Slučka systémových volaní Futex (_RUN_SYSCALL_LOOP = 5, keď t.seccompNotify == nil): syscallThread spúšťa syscall_loop (stub_amd64.s / stub_arm64.s) a spí na SYS_FUTEX (FUTEX_WAIT) nad zdieľanou oblasťou dvoch stránok syscallSentryMessage (PROT_READ v stub) a syscallStubMessage (PROT_READ | PROT_WRITE v stub).

Upstream gVisor bezpodmienečne volá t.installSeccompNotify() vždy, keď je seccompNotifyIsSupported pravdivé, a zruší vytvorenie stubu, ak seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) vráti EBUSY.

V gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go a syscall_thread.go) volá syscallThread.init() funkciu t.installSeccompNotifyIfFree():

 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}

Keď installSeccompNotify() vráti unix.EBUSY, pretože vonkajší gVisor (runsc) alebo vonkajší sandbox gvx už drží jediný slot listenera v reťazci filtrov procesu:

  • installSeccompNotifyIfFree() nastaví seccompNotifyBusy (atomic.Bool), zaznamená jedno varovanie pre proces a ponechá t.seccompNotify == nil pre tento stub a všetky nasledujúce stuby.
  • V syscallThread.detach() (syscall_thread_amd64.go a syscall_thread_arm64.go) podmienka t.seccompNotify == nil nastaví regs.Rbx = _RUN_SYSCALL_LOOP (5) na amd64 (alebo regs.Regs[9] = _RUN_SYSCALL_LOOP na arm64) namiesto _RUN_SECCOMP_LOOP (6).
  • V syscallThread.syscall() (syscall_thread.go) vnútorný Sentry odosiela systémové volania stubu výhradne cez zdieľanú pamäť a SYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Oznámte vláknu syscall stubu novú požiadavku na systémové volanie.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Počkajte na odpoveď.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))

Keďže filtre SECCOMP_RET_TRAP na vláknach hosťa sa vrstvia ľubovoľne hlboko a syscallThread sa vracia k _RUN_SYSCALL_LOOP bez žiadania druhého používateľského listenera notifikácií, gvx beží čisto vnútri runsc a podporuje rekurzívne vnorenie (gvx vnútri gvx).

2. Natívny overlayfs na jednosúborových zväzkoch ext4lite

Aby sa zabránilo pripojeniu overlayfs na koreňový súborový systém gofer (9p/lisafs) vonkajšieho kontajnera gVisor, -v gvx-docker:/var/lib/docker podkladá /var/lib/docker interným súborovým systémom ext4lite od gvx, uloženým v jedinom bežnom súbore (gvx-docker.ext4).

Pre vonkajší runsc je gvx-docker.ext4 len jeden bežný súbor, ku ktorému sa pristupuje cez pread64(2) a pwrite64(2). Vnútri gvx poskytuje ext4lite súborový systém ext4 s žurnálom JBD2, s rozšírenými atribútmi trusted.overlay.* a security.capability, uzlami whiteout znakových zariadení (0:0), položkami adresárov d_type a RENAME_EXCHANGE, čo umožňuje vnútornému dockerd použiť natívny ovládač úložiska overlayfs.

3. Samostatný netstack s docker0 a iptables-legacy

Vnútri gvx dockerd neinteraguje so sieťovými rozhraniami vonkajšieho kontajnera runsc ani s tabuľkami netfilter. Namiesto toho:

  • dockerd vytvára svoj most docker0 a páry veth vnútri vlastného netstack od gvx v používateľskom priestore.
  • Všetky pravidlá iptables-legacy (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) sa vykonávajú voči internej implementácii netfilter od gvx.
  • Vnorené kontajnery runc pripájajú svoje hierarchie /proc, /sys a /sys/fs/cgroup vnútri pamäťovej tabuľky pripojení VFS2 od gvx a nikdy nevolajú systémové volanie mount(2) vonkajšieho runsc.

Overenie vnoreného docker:dind vnútri gVisor (alebo gvx v gvx)

Obidva scenáre (rungv vnútri runsc alebo rungv vnútri rungv) môžete overiť pomocou nasledujúcich príkazov:

 1# Vnútri vonkajšieho kontajnera gVisor (runsc) alebo vonkajšieho kontajnera rungv:
 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# Overte, že vnútorný Docker daemon používa natívny overlayfs (nie vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Výstup: Storage Driver: overlayfs
16
17# Spustite viacvrstvový build a vykonajte vnorený kontajner
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# Výstup: nested-build-ok
31
32kill -INT $DIND_PID

Ďalšie čítanie v tejto sérii

  1. gvx: Spúšťanie Linux kontajnerov, Dockeru a Kubernetes v jednom neprivilegovanom procese: Prehľad gvx naprieč systémami Windows (whpx, systrap), macOS (hvf) a Linux (systrap).
  2. Spúšťanie Dockeru, docker build a docker compose bez root oprávnení alebo používateľských namespace: Ako gvx spúšťa docker:dind, keď sú zablokované root, sudo, unshare(CLONE_NEWUSER), mount(2) a /dev/fuse.
  3. Docker-in-Docker vnútri gVisor a vnorených sandboxov s gvx (tento príspevok)
  4. Rýchle izolované Docker prostredia pre RL rollouty AI agentov s gvx: Ako použiť gvx a jednosúborové copy-on-write snapshoty ext4lite na spustenie stoviek paralelných prostredí SWE-bench a RL rolloutov s resetmi za 100 milisekúnd.