GoSuda

Docker-in-Docker futtatása gVisor-on belül és Nested Sandboxes-ban 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

A felhőalapú MI-ügynök számítási környezetek (például a Google Agent Compute Engine sandboxai), a Kubernetes agent-sandbox telepítések (RuntimeClass: gvisor) és a megerősítéses tanulási kiértékelő keretrendszerek gVisor (runsc) konténerekbe zárják a megbízhatatlan munkaterheléseket, kifejezetten a gazdagép kernelének védelme érdekében. Amikor egy alapértelmezett, nem privilegizált runsc konténerben futó munkaterhelés az apt-get paranccsal a docker.io vagy docker-ce csomagot próbálja telepíteni, a service docker start vagy dockerd parancsot elindítani, és docker build parancsot futtatni, akkor hibával jár, még akkor is, ha a konténeren belül root (UID 0) jogosultsággal rendelkezik.

A 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 a gvx-et egy másik gvx belsejében is elindíthatja), anélkül, hogy gyengítené a külső RuntimeClass beállításait, host socketeket tenne elérhetővé, vagy --privileged jogosultságot adna.

Kis erőfeszítésű, tokenhatékony megoldás MI-ügynökök számára: Egy gVisor-alapú felhőbeli ügynöki környezetben a docker.io telepítésének és a /etc/init.d/docker ulimit hibáinak, az iptables-nft hibáknak, az iptables-legacy nyers socket tiltásainak, az overlay2 csatolási hibáknak, valamint a docker build runc / pivot_root összeomlásainak manuális kikerülése jellemzően 20+ eszközhívási körön és tízezernyi tokenen emészti fel a munkamenetet, mielőtt teljesen kudarcot vallana. A gvx (rungv) egy kis erőfeszítésű, tokenhatékony, közvetlenül behelyettesíthető megoldás: az ügynök egyetlen körben 3 paranccsal elindít egy működő, felhasználói térben futó Docker démont, majd projektfájlok módosítása nélkül azonnal végrehajtja a docker build és docker compose parancsokat.

A külső sandbox izoláció megőrzése és megerősítése: A rungv runsc-en belüli futtatása nem kerüli meg a külső gVisor sandbox egyetlen korlátozását sem. Ehelyett a gvx egy második, beágyazott, felhasználói térben működő gVisor Sentry kernelt hoz létre saját hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp) védelemmel, saját memóriabeli netstack hálózattal és egyetlen fájlból álló ext4lite kötettel. A belső dockerd és runc konténerek kettős sandboxban futnak: 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-val igazolt 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 build és konténerfuttatás 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 MI-kódoló ügynök 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 démont, öt egymást követő műszaki akadályba ütközik.

1. Init-szkript (systemctl / service docker start ulimit) és kernelmodul (modprobe 4.4.0) hibák

A apt-get install -y docker.io (vagy docker-ce) futtatása után a démon indítása systemctl start docker vagy service docker start paranccsal azonnal meghiúsul, mert a PID 1 nem systemd, a /etc/init.d/docker pedig megpróbálja a ulimit -n értékét a sandbox kemény RLIMIT_NOFILE határa fölé emelni. Amikor a dockerd ezután közvetlenül indul, az inicializáló szkriptek a modprobe paranccsal próbálják betölteni az overlay, bridge, br_netfilter, ip_tables és nf_nat modulokat a gVisor virtuális kernelverziójához (4.4.0) képest, amelyhez nincs /lib/modules/4.4.0 könyvtár:

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), iptables-legacy nyers socket tiltások és docker0 híd hibák

Az Ubuntu 22.04, Ubuntu 24.04 és Debian 11/12 rendszereken az apt-get install docker.io az iptables-nft eszközt (amely az nf_tables netlink alrendszeren alapul) választja alapértelmezett /usr/sbin/iptables alternatívaként. Ráadásul, még ha a felhasználó vagy az ügynök át is vált az iptables-legacy eszközre (update-alternatives --set iptables /usr/sbin/iptables-legacy), az alapértelmezett nem privilegizált gVisor sandboxok (--net-raw=false) EPERM hibával elutasítják az AF_INET, SOCK_RAW socketeket, aminek következtében az iptables-legacy akkor is Permission denied (you must be root) hibával leáll, ha UID 0 felhasználóként fut:

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

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-e a gVisor gofer (9p/lisafs) vagy egy beágyazott overlay csatolás mögött található. 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 ügynökök a dockerd --storage-driver=vfs indításával próbálják megkerülni ezt a hibát, a Docker teljesen kikapcsolja 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) végzi el, így egy 500 MB-os image 10 build lépés alatt 5 GB írássá szorzódik, és a sandbox no space left on device hibával vagy I/O időtúllépéssel leáll.

4. docker build (BuildKit) és beágyazott runc hibák (pivot_root, /proc, cgroupok és DNS)

Még ha a dockerd a dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs paranccsal kényszerítetten is elindul, a docker build -t app . vagy docker run parancs azonnal meghiúsul, amikor a BuildKit vagy a runc elindítja a build konténert:

 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. Beágyazott runsc hiba SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY) miatt

Ha a felhasználó a systrap platformot használva egy másik runsc példányt próbál futtatni egy runsc belsejében, 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 feladatlánconké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 futtatást a gVisor környezetben

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 futni 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 futtatás külső gVisor környezetben

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

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

  • Vendég végrehajtási szálak (sysmsg): A vendég rendszerhívásokat SECCOMP_RET_TRAP (SIGSYS) és binárisan patchelt jmp rel32 trampolinok (syshandler) fogják el, a regiszterállapotot pedig megosztott memórián és SYS_FUTEX hívásokon keresztül cserélik ki a Sentry-vel.
  • Egy stub syscallThread (syscall_thread.go): A Sentry megbízásából a stub alfolyamaton belül végrehajtja a címtér-kezelő rendszerhívásokat (mmap, munmap, clone).

Az 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 küldhessen kéréseket.
  2. Futex rendszerhívási 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 SYS_FUTEX (FUTEX_WAIT) hívásra 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, amikor a seccompNotifyIsSupported igaz, és megszakítja a stub létrehozását, ha a seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) EBUSY értékkel tér vissza.

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

 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 az installSeccompNotify() a unix.EBUSY értékkel tér 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) változót, a folyamatra nézve egyszer naplóbejegyzést ír, és az adott stubnál, valamint az összes későbbi stubnál a t.seccompNotify == nil állapotot hagyja érvényben.
  • A syscallThread.detach() függvényben (syscall_thread_amd64.go és syscall_thread_arm64.go) a t.seccompNotify == nil feltétel esetén amd64 architektúrán a regs.Rbx = _RUN_SYSCALL_LOOP (5), arm64 architektúrán pedig a regs.Regs[9] = _RUN_SYSCALL_LOOP értéket állítja be 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 hívásokon keresztül 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 tetszőlegesen mélyen egymásba ágyazódhatnak a vendég szálakon, a syscallThread pedig a _RUN_SYSCALL_LOOP módra vált vissza anélkül, hogy második felhasználói értesítési listenert kérne, a gvx tisztán fut az runsc belsejében, és támogatja a rekurzív beá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ájlrendszere alá helyezi, 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) hívásokkal érnek el. A gvx belsejében az ext4lite JBD2 naplózással ellátott ext4 fájlrendszert biztosít, trusted.overlay.* és security.capability bővített 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 eszközökkel

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 rétegében hozza létre.
  • Az összes iptables-legacy szabály (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) a gvx belső netfilter implementációjában fut le.
  • A beágyazott runc konténerek a /proc, /sys és /sys/fs/cgroup hierarchiáikat a gvx memóriabeli VFS2 csatolási táblájá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 gVisor környezetben (vagy gvx a gvx-ben)

Mindkét forgatókönyvet (rungv runsc-en belül, vagy rungv rungv-n belül) az alábbi parancsokkal ellenőrizheti:

 1# Külső gVisor (runsc) konténerben vagy 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, hogy a belső Docker démon 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 beágyazott konténer indítá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 olvasnivaló 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 jogosultság vagy felhasználói névterek nélkül: Hogyan futtatja a gvx a docker:dind parancsot akkor, ha a root, a sudo, az unshare(CLONE_NEWUSER), a mount(2) és a /dev/fuse le van tiltva.
  3. Docker-in-Docker futtatása gVisorban és beágyazott sandboxokban a gvx segítségével (ez a bejegyzés)
  4. Gyors, izolált Docker környezetek MI-ügynökök RL rollout-jaihoz a gvx segítségével: Hogyan használható a gvx és az egyfájlos ext4lite copy-on-write pillanatkép több száz párhuzamos SWE-bench és RL rollout környezet futtatására, 100 milliszekundumos visszaállítással.