GoSuda

Docker-in-Docker futtatása gVisor és Nested Sandboxes környezetben a gvx segítségével

By Lemon Mint views ...

Gyors megoldás: docker:dind biztonságos futtatása nem privilegizált gVisor (runsc) sandboxban

Cloud AI agent platformok, Kubernetes agent-sandbox telepítések (RuntimeClass: gvisor) és megerősítéses tanulási (reinforcement learning) kiértékelő keretrendszerek a megbízhatatlan munkaterheléseket gVisor (runsc) konténerekbe izolálják, kifejezetten a gazdagép kernelének védelme érdekében. Amikor egy munkaterhelés egy alapértelmezett, nem privilegizált runsc konténerben megpróbálja telepíteni a docker.io vagy docker-ce csomagot apt-get segítségével, és elindítani a dockerd folyamatot, ez meghiúsul, még akkor is, ha a konténeren belül root (UID 0) jogosultsággal rendelkezik.

Az gvx (rungv) segítségével biztonságosan futtathatja a docker:dind, a docker build és a docker compose parancsokat közvetlenül egy nem privilegizált gVisor (runsc) konténerben (vagy egymásba ágyazhatja a gvx-et a gvx-en belül), anélkül, hogy gyengítené a külső RuntimeClass beállítást, host socketeket tenne elérhetővé vagy --privileged jogosultságot adna.

A külső sandbox izolációjának megőrzése és megerősítése: A rungv runsc-en belüli futtatása nem kerül meg egyetlen korlátozást sem, amelyet a külső gVisor sandbox érvényesít. Ehelyett a gvx egy második, beágyazott, felhasználói térben futó gVisor Sentry kernelt hoz létre, saját hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp) védelemmel, saját memórián belüli netstack hálózattal és egyfájlos ext4lite kötettel. A belső dockerd és runc konténerek kétszeresen sandboxoltak: rendszerhívásaik, csatolásaik és nyers csomagjaik soha nem hagyják el a belső gvx felhasználói térbeli kernelt.

 1# Meglévő gVisor (runsc) konténerben vagy Kubernetes Podban:
 2# A rungv telepítése SLSA-attesztált GitHub kiadási archívumból vagy go install paranccsal:
 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# Szokásos Docker buildek és konténerek futtatása a belső gvx socketen keresztül
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# Kimenet: overlayfs Alpine Linux ... (konténerizált)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Öt ok, amiért a közvetlen Docker telepítés meghiúsul a gVisor (runsc) környezetben

Amikor egy felhasználó vagy egy AI kódoló agent egy szabványos runsc konténerben futtatja az apt-get install -y docker.io (vagy docker-ce) parancsot, és megpróbálja elindítani a dockerd folyamatot, öt egymást követő technikai akadályba ütközik.

1. Kernelmodul-betöltés (modprobe) a virtuális 4.4.0 kernel ellen

A csomagtelepítés és a dockerd indítása során az inicializáló szkriptek modprobe parancsot futtatnak az overlay, br_netfilter, ip_tables, nf_nat és xt_conntrack modulok betöltéséhez. A külső gVisor Sentry letiltja az init_module(2) és finit_module(2) rendszerhívásokat, és olyan virtuális kernelverziót (4.4.0) jelent, amelyhez nem tartozik /lib/modules/4.4.0 könyvtár:

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. iptables-nft (nf_tables Netlink) és a docker0 híd hibái

Az Ubuntu 22.04, Ubuntu 24.04 és Debian 11/12 rendszereken az apt-get install docker.io az iptables-nft csomagot (amelyet az nf_tables netlink alrendszer támogat) választja alapértelmezett /usr/sbin/iptables alternatívaként. Egy szabványos gVisor konténerben, hálózati adminisztrátori jogosultságok vagy nf_tables netlink támogatás nélkül, a dockerd összeomlik a DOCKER NAT lánc és a docker0 híd inicializálása közben:

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. overlay2 hiba gofer / 9p rootfs és vfs lemezkimerülés esetén

Hacsak a külső runsc rendszergazda kifejezetten nem konfigurált dedikált tmpfs vagy filestore csatolást a /var/lib/docker útvonalon, a konténer rootfs a gVisor gofer (9p/lisafs) rendszerére vagy egy beágyazott overlay csatolásra épül. Az overlay2 csatolása a /var/lib/docker fölé meghiúsul:

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

Amikor a felhasználók vagy az agentek a dockerd --storage-driver=vfs indításával próbálják megkerülni ezt a hibát, a Docker teljesen letiltja a Copy-on-Write rétegmegosztást. A Dockerfile minden RUN utasítása a teljes gyökérfájlrendszer rekurzív másolását (dirCopy) hajtja végre, így egy 500 MB-os image 10 build lépésben 5 GB írássá sokszorozódik, és a sandbox no space left on device hibával vagy I/O időtúllépéssel leáll.

4. Beágyazott runc cgroup- és /proc csatolási elutasítások

Még akkor is, ha a dockerd kényszerítve indul a dockerd --iptables=false --bridge=none --storage-driver=vfs paranccsal, a docker run vagy docker build futtatása meghívja a runc programot a belső konténer létrehozásához. Egy alapértelmezett, nem privilegizált gVisor konténerben a runc meghiúsul a cgroupok csatolásakor, a /proc újracsatolásakor vagy a belső seccomp szűrők betöltésekor:

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. A beágyazott runsc meghiúsul a SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY) miatt

Ha a felhasználó egy másik runsc példányt próbál futtatni runsc-en belül a systrap platform használatával, az inicializálás azonnal megszakad:

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

Mind a Linux kernel, mind a gVisor Sentry legfeljebb egy SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) listenert enged task-hierarchiánként. Mivel a külső sandbox már birtokolja ezt a listener helyet, a belső seccomp(2) hívás EBUSY (device or resource busy) hibával tér vissza.


Hogyan oldja meg a gvx a beágyazott végrehajtást a gVisor-on belül

A gvx mind az öt akadályt a saját felhasználói térbeli kernelében és platformrétegében kezeli, így a rungv képes fut egy külső gVisor (runsc) konténerben, vagy akár rekurzívan egy másik gvx konténerben belül (gvx a gvx-ben).

Beágyazott gvx végrehajtás külső gVisor-on belül

1. Automatikus systrap visszaesés az EBUSY hibától a _RUN_SYSCALL_LOOP futex ciklusra

A Linux systrap rendszerben minden stub alfolyamat kétféle szálat futtat:

  • Vendég végrehajtási szálak (sysmsg): A vendég rendszerhívásokat SECCOMP_RET_TRAP (SIGSYS) és bináris módosított jmp rel32 trambulinok (syshandler) fogják el, a regiszterállapotot pedig megosztott memórián és SYS_FUTEX segítségével cserélik ki a Sentry-vel.
  • Egy stub syscallThread (syscall_thread.go): A stub alfolyamaton belül a Sentry nevében végrehajtja a címtér-rendszerhívásokat (mmap, munmap, clone).

A upstream gVisorban a syscallThread két ébresztési módot támogat a stub_amd64.s és stub_arm64.s fájlokban:

  1. Seccomp User-Notify ciklus (_RUN_SECCOMP_LOOP = 6, ha t.seccompNotify != nil): A syscallThread.init() során a t.installSeccompNotify() egy SECCOMP_RET_USER_NOTIF listenert telepít (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), hogy a Sentry az ioctl(SECCOMP_IOCTL_NOTIF_SEND) és ioctl(SECCOMP_IOCTL_NOTIF_RECV) hívásokon keresztül továbbíthasson kéréseket.
  2. Futex rendszerhívás ciklus (_RUN_SYSCALL_LOOP = 5, ha t.seccompNotify == nil): A syscallThread a syscall_loop (stub_amd64.s / stub_arm64.s) függvényt futtatja, és a SYS_FUTEX (FUTEX_WAIT) hívásnál alszik a megosztott, 2 lapos syscallSentryMessage (a stubban PROT_READ) és syscallStubMessage (a stubban PROT_READ | PROT_WRITE) régión.

Az upstream gVisor feltétel nélkül meghívja a t.installSeccompNotify() függvényt, amint a seccompNotifyIsSupported értéke igaz, és megszakítja a stub létrehozását, ha a seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) EBUSY hibát ad vissza.

A gvx rendszerben (pkg/sentry/platform/systrap/seccomp_notify_xplat.go és syscall_thread.go) a syscallThread.init() a t.installSeccompNotifyIfFree() függvényt hívja:

 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}

Amikor a installSeccompNotify() unix.EBUSY értéket ad vissza, mert egy külső gVisor (runsc) vagy külső gvx sandbox már birtokolja a folyamat szűrőláncának egyetlen listener helyét:

  • Az installSeccompNotifyIfFree() beállítja a seccompNotifyBusy (atomic.Bool) értéket, a folyamatra nézve egyetlen figyelmeztetést naplóz, és a t.seccompNotify == nil értéket hagyja az adott stubnál és az összes későbbi stubnál.
  • A syscallThread.detach() függvényben (syscall_thread_amd64.go és syscall_thread_arm64.go) a t.seccompNotify == nil esetén regs.Rbx = _RUN_SYSCALL_LOOP (5) értéket állít be amd64 architektúrán (vagy regs.Regs[9] = _RUN_SYSCALL_LOOP értéket arm64 architektúrán) a _RUN_SECCOMP_LOOP (6) helyett.
  • A syscallThread.syscall() függvényben (syscall_thread.go) a belső Sentry a stub rendszerhívásokat kizárólag megosztott memórián és SYS_FUTEX segítségével továbbítja:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Notify the syscall thread about a new syscall request.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Wait for reply.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))

Mivel a SECCOMP_RET_TRAP szűrők a vendég szálakon tetszőlegesen mélyen egymásba ágyazódhatnak, a syscallThread pedig további felhasználói értesítési listener kérése nélkül visszaesik a _RUN_SYSCALL_LOOP módra, a gvx tisztán fut a runsc-en belül, és támogatja a rekurzív egymásba ágyazást (gvx a gvx-ben).

2. Natív overlayfs egyfájlos ext4lite köteteken

Annak elkerülésére, hogy az overlayfs a külső gVisor konténer gofer (9p/lisafs) gyökérfájlrendszerére kerüljön, a -v gvx-docker:/var/lib/docker paraméter a /var/lib/docker könyvtárat a gvx belső ext4lite fájlrendszerével támasztja alá, amely egyetlen szabályos fájlban (gvx-docker.ext4) tárolódik.

A külső runsc számára a gvx-docker.ext4 csupán egy szabályos fájl, amelyet pread64(2) és pwrite64(2) rendszerhívásokkal érnek el. A gvx belsejében az ext4lite JBD2-naplózott ext4 fájlrendszert biztosít, trusted.overlay.* és security.capability kiterjesztett attribútumokkal, karakteres eszköz whiteout csomópontokkal (0:0), d_type könyvtárbejegyzésekkel és RENAME_EXCHANGE támogatással. Ez lehetővé teszi, hogy a belső dockerd a saját natív overlayfs tárolómeghajtóját használja.

3. Önálló netstack a docker0 és iptables-legacy támogatással

A gvx belsejében a dockerd nem lép kapcsolatba a külső runsc konténer hálózati interfészeivel vagy netfilter táblázataival. Ehelyett:

  • A dockerd a docker0 hidat és a veth párokat a gvx saját felhasználói térbeli netstack rendszerében hozza létre.
  • Minden iptables-legacy szabály (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) a gvx belső netfilter implementációján hajtódik végre.
  • A beágyazott runc konténerek a /proc, /sys és /sys/fs/cgroup hierarchiáikat a gvx memórián belüli VFS2 csatolási táblázatában csatolják, és soha nem hívják meg a külső runsc mount(2) rendszerhívását.

Beágyazott docker:dind ellenőrzése a gVisor-on belül (vagy gvx a gvx-ben)

Mindkét forgatókönyv (rungv a runsc-en belül, vagy rungv a rungv-en belül) a következő parancsokkal ellenőrizhető:

 1# Egy külső gVisor (runsc) konténerben vagy egy külső rungv konténerben:
 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# Ellenőrzés: a belső Docker daemon natív overlayfs-t használ-e (nem vfs-t)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Kimenet: Storage Driver: overlayfs
16
17# Többrétegű build futtatása és egy beágyazott konténer végrehajtása
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# Kimenet: nested-build-ok
31
32kill -INT $DIND_PID

További olvasmányok ebben a sorozatban

  1. gvx: Linux konténerek, Docker és Kubernetes futtatása egyetlen nem privilegizált folyamatban: Áttekintés a gvx működéséről Windows (whpx, systrap), macOS (hvf) és Linux (systrap) rendszereken.
  2. Docker, docker build és docker compose futtatása root vagy felhasználói névterek nélkül: Hogyan futtatja a gvx a docker:dind parancsot, ha a root, sudo, unshare(CLONE_NEWUSER), mount(2) és /dev/fuse le van tiltva.
  3. Docker-in-Docker futtatása gVisor-on belül és beágyazott sandboxokban a gvx segítségével (ez a bejegyzés)
  4. Gyors, izolált Docker környezetek AI agentek RL rollout feladataihoz a gvx segítségével: Hogyan használható a gvx és az egyfájlos ext4lite copy-on-write pillanatképek több száz párhuzamos SWE-bench és RL rollout környezet futtatásához, 100 milliszekundumos visszaállítással.