GoSuda

Docker-in-Docker suorittaminen gVisorin sisällä ja sisäkkäisissä sandboxeissa gvx:n avulla

By Lemon Mint views ...

Pikaratkaisu: docker:dind-ympäristön turvallinen suorittaminen rajoitetussa gVisor (runsc) -sandboxissa

Pilvipohjaiset tekoälyagenttien laskentaympäristöt (kuten Google Agent Compute Engine -sandboxit), Kubernetes-agent-sandbox-käyttöönotot (RuntimeClass: gvisor) ja vahvistusoppimisen arviointiympäristöt eristävät luotettamattomat työkuormat gVisor (runsc) -konteissa erityisesti isäntäkernelin suojaamiseksi. Kun työkuorma oletusarvoisesti rajoitetussa runsc-kontissa yrittää asentaa docker.io- tai docker-ce-paketin apt-get-komennolla, käynnistää service docker start- tai dockerd-komennon sekä suorittaa docker build-komennon, se epäonnistuu, vaikka prosessilla on root (UID 0) kontin sisällä.

gvx (rungv) -työkalulla voit turvallisesti suorittaa docker:dind-, docker build- ja docker compose-komennot suoraan rajoitetun gVisor (runsc) -kontin sisällä (tai sisäkkäin gvx gvx:n sisällä) heikentämättä ulomman RuntimeClass-määrityksen turvallisuutta, paljastamatta isännän socketeja tai myöntämättä --privileged-oikeuksia.

Matalan vaivan ja tokenitehokas ratkaisu tekoälyagenteille: Kun toimitaan gVisor-pohjaisessa pilvi-agenttiympäristössä, docker.io-paketin asentaminen ja manuaalinen kiertäminen /etc/init.d/docker-skriptin ulimit-virheiden, iptables-nft-virheiden, iptables-legacy-raakasocket-estojen, overlay2-liitoshäiriöiden ja docker build -komennon runc- / pivot_root-kaatumisten kanssa kuluttaa tyypillisesti yli 20 työkalukutsukierrosta ja kymmeniätuhansia tokeneita ennen täydellistä epäonnistumista. gvx (rungv) on matalan vaivan ja tokenitehokas suoraan käyttöön otettava ratkaisu: agentti suorittaa 3 komentoa yhdellä kierroksella käynnistääkseen toimivan käyttäjätilan Docker-daemonin ja suorittaa välittömästi docker build- ja docker compose-komennot muokkaamatta projektitiedostoja.

Ulomman sandboxin eristyksen säilyttäminen ja vahvistaminen: rungv-komennon suorittaminen runsc-kontin sisällä ei ohita yhtäkään ulomman gVisor-sandboxin asettamaa rajoitusta. Sen sijaan gvx luo toisen, sisäkkäisen käyttäjätilan gVisor-Sentry-kernelin, jolla on oma hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), oma muistinvarainen netstack ja yksitiedostoinen ext4lite-volyymi. Sisemmät dockerd- ja runc-kontit ovat kaksinkertaisesti sandboxattuja: niiden järjestelmäkutsut, liitokset ja raakapaketit eivät koskaan poistu sisemmästä gvx-käyttäjätilan kernelistä.

 1# Olemassa olevan gVisor (runsc) -kontin tai Kubernetes-Podin sisällä:
 2# Asenna rungv SLSA-todennetusta GitHub-julkaisuarkistosta tai go install -komennolla:
 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# Suorita tavalliset Docker-buildit ja -kontit sisemmän gvx-socketin kautta
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# Tuloste: overlayfs Alpine Linux ... (konteroitu)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Viisi syytä, miksi suora Docker-asennus epäonnistuu gVisor-ympäristössä (runsc)

Kun käyttäjä tai tekoälykoodausagentti suorittaa apt-get install -y docker.io-komennon (tai docker-ce-komennon) tavallisessa runsc-kontissa ja yrittää käynnistää dockerd-daemonin, se kohtaa viisi peräkkäistä teknistä estettä.

1. Alustusskriptin (systemctl / service docker start ulimit) ja kernel-moduulin (modprobe 4.4.0) virheet

Kun apt-get install -y docker.io (tai docker-ce) on suoritettu, daemonin käynnistäminen komennolla systemctl start docker tai service docker start epäonnistuu välittömästi, koska PID 1 ei ole systemd ja /etc/init.d/docker yrittää nostaa ulimit -n-arvoa sandboxin kovan RLIMIT_NOFILE-katon yli. Kun dockerd käynnistetään sitten suoraan, alustusskriptit kutsuvat modprobe-komentoa ladatakseen moduulit overlay, bridge, br_netfilter, ip_tables ja nf_nat gVisorin virtuaalikernelin versiota (4.4.0) vastaan. Hakemistoa /lib/modules/4.4.0 ei ole olemassa:

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 -raakasocket-estot ja docker0-sillan virheet

Ubuntu 22.04:ssä, Ubuntu 24.04:ssä ja Debian 11/12:ssa apt-get install docker.io valitsee oletusarvoisesti /usr/sbin/iptables-vaihtoehdoksi iptables-nft-työkalun (joka perustuu nf_tables-netlink-alijärjestelmään). Lisäksi vaikka käyttäjä tai agentti vaihtaisi iptables-legacy-työkaluun (update-alternatives --set iptables /usr/sbin/iptables-legacy), oletusarvoiset rajoitetut gVisor-sandboxit (--net-raw=false) hylkäävät AF_INET, SOCK_RAW-socketit virheellä EPERM. Tämän seurauksena iptables-legacy epäonnistuu virheellä Permission denied (you must be root) myös silloin, kun suoritus tapahtuu UID 0 -käyttäjänä:

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-virhe gofer- / 9p-juurijärjestelmässä ja vfs-levytilan ehtyminen

Ellei ulomman runsc-kontin ylläpitäjä ole nimenomaisesti määrittänyt erillistä tmpfs- tai filestore-liitosta kohteeseen /var/lib/docker, kontin juurijärjestelmä perustuu gVisorin gofer-palvelimeen (9p/lisafs) tai sisäkkäiseen overlay-liitokseen. overlay2-liitos kohteeseen /var/lib/docker epäonnistuu:

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

Kun käyttäjät tai agentit yrittävät kiertää tämän virheen käynnistämällä dockerd --storage-driver=vfs-komennon, Docker poistaa kokonaan Copy-on-Write-kerrosten jakamisen käytöstä. Jokainen Dockerfile-tiedoston RUN-ohje suorittaa koko juurijärjestelmän täyden rekursiivisen kopioinnin (dirCopy). Tämä kertoo 500 MB:n imagen kymmenen rakennusvaiheen aikana 5 GB:n kirjoituksiksi ja päättää sandboxin virheeseen no space left on device tai I/O-aikakatkaisuun.

4. docker build (BuildKit) ja sisäkkäisen runc:n virheet (pivot_root, /proc, cgroupit ja DNS)

Vaikka dockerd pakotettaisiin käynnistymään komennolla dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs, komennon docker build -t app . tai docker run suoritus epäonnistuu välittömästi, kun BuildKit tai runc käynnistää rakennuskontin:

 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. Sisäkkäinen runsc epäonnistuu virheellä SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)

Jos käyttäjä yrittää suorittaa toista runsc-instanssia runsc:n sisällä systrap-alustalla, alustus keskeytyy välittömästi:

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

Sekä Linux-kernel että gVisor Sentry sallivat enintään yhden SECCOMP_FILTER_FLAG_NEW_LISTENER- (SECCOMP_RET_USER_NOTIF) -kuuntelijan tehtäväpuuta kohden. Koska ulompi sandbox pitää jo tuon kuuntelijapaikan hallussaan, sisemmän seccomp(2)-kutsun paluuarvo on EBUSY (device or resource busy).


Miten gvx ratkaisee sisäkkäisen suorituksen gVisor-ympäristössä

gvx käsittelee kaikki viisi estettä omassa käyttäjätilan kernelissään ja alustakerroksessaan. Tämä mahdollistaa rungv-komennon suorittamisen ulomman gVisor (runsc) -kontin sisällä tai jopa rekursiivisesti toisen gvx-kontin sisällä (gvx gvx:ssä).

Sisäkkäinen gvx-suoritus ulomman gVisorin sisällä

1. Automaattinen systrap-varajärjestelmä EBUSY-tilanteesta _RUN_SYSCALL_LOOP futex-silmukkaan

Linuxin systrap-alustassa jokainen stub-aliprosessi suorittaa kahdenlaisia säikeitä:

  • Vierassuorituksen säikeet (sysmsg): Vangitsevat vieraan järjestelmäkutsut SECCOMP_RET_TRAP-mekanismilla (SIGSYS) ja binääripatchatuilla jmp rel32-trampoliineilla (syshandler). Ne vaihtavat rekisteritilan Sentryn kanssa jaetun muistin ja SYS_FUTEX-kutsun avulla.
  • Stub-syscallThread (syscall_thread.go): Suorittaa osoiteavaruuden järjestelmäkutsut (mmap, munmap, clone) stub-aliprosessin sisällä Sentryn puolesta.

Upstream-gVisorissa syscallThread tukee kahta heräätystilaa tiedostoissa stub_amd64.s ja stub_arm64.s:

  1. Seccomp User-Notify -silmukka (_RUN_SECCOMP_LOOP = 6, kun t.seccompNotify != nil): syscallThread.init()-vaiheessa t.installSeccompNotify() asentaa SECCOMP_RET_USER_NOTIF-kuuntelijan (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), jotta Sentry voi lähettää pyyntöjä ioctl(SECCOMP_IOCTL_NOTIF_SEND)- ja ioctl(SECCOMP_IOCTL_NOTIF_RECV)-kutsujen kautta.
  2. Futex-järjestelmäkutsusilmukka (_RUN_SYSCALL_LOOP = 5, kun t.seccompNotify == nil): syscallThread suorittaa syscall_loop-silmukan (stub_amd64.s / stub_arm64.s). Se nukkuu SYS_FUTEX-kutsun (FUTEX_WAIT) varassa jaetun 2-sivuisen syscallSentryMessage-alueen (PROT_READ stubissa) ja syscallStubMessage-alueen (PROT_READ | PROT_WRITE stubissa) yllä.

Upstream-gVisor kutsuu ehdoitta t.installSeccompNotify()-funktiota aina, kun seccompNotifyIsSupported on tosi, ja keskeyttää stubin luomisen, jos seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) palauttaa EBUSY.

gvx:ssä (pkg/sentry/platform/systrap/seccomp_notify_xplat.go ja syscall_thread.go) syscallThread.init() kutsuu t.installSeccompNotifyIfFree()-funktiota:

 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}

Kun installSeccompNotify() palauttaa unix.EBUSY-virheen, koska ulompi gVisor (runsc) tai ulompi gvx-sandbox pitää jo prosessin suodatinketjun ainoaa kuuntelijapaikkaa hallussaan:

  • installSeccompNotifyIfFree() asettaa seccompNotifyBusy-muuttujan (atomic.Bool), kirjaa yhden varoituksen prosessille ja jättää t.seccompNotify == nil-arvon kyseiselle stubille sekä kaikille seuraaville stubeille.
  • syscallThread.detach()-funktiossa (syscall_thread_amd64.go ja syscall_thread_arm64.go) ehdon t.seccompNotify == nil ollessa tosi asetetaan regs.Rbx = _RUN_SYSCALL_LOOP (5) arkkitehtuurilla amd64 (tai regs.Regs[9] = _RUN_SYSCALL_LOOP arkkitehtuurilla arm64) _RUN_SECCOMP_LOOP-arvon (6) sijaan.
  • syscallThread.syscall()-funktiossa (syscall_thread.go) sisempi Sentry lähettää stub-järjestelmäkutsut yksinomaan jaetun muistin ja SYS_FUTEX-kutsun kautta:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Ilmoita syscall-säikeelle uudesta syscall-pyynnöstä.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Odota vastausta.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))

Koska SECCOMP_RET_TRAP-suodattimet pinoavat vieraiden säikeiden kohdalla mielivaltaisen syvälle ja syscallThread siirtyy varajärjestelmänä silmukkaan _RUN_SYSCALL_LOOP pyytämättä toista käyttäjäilmoituskuuntelijaa, gvx toimii siististi runsc:n sisällä ja tukee rekursiivista sisäkkäisyyttä (gvx gvx:ssä).

2. Natiivi overlayfs yksitiedostoisilla ext4lite-volyymeilla

Jotta overlayfs-liitosta ei tarvitse asentaa ulomman gVisor-kontin gofer-juurijärjestelmän (9p/lisafs) päälle, -v gvx-docker:/var/lib/docker tallentaa kohteen /var/lib/docker sisäisen ext4lite-tiedostojärjestelmän gvx:stä yhteen tavalliseen tiedostoon (gvx-docker.ext4).

Ulomman runsc:n näkökulmasta gvx-docker.ext4 on vain yksi tavallinen tiedosto, jota käsitellään kutsuilla pread64(2) ja pwrite64(2). gvx:n sisällä ext4lite tarjoaa JBD2-journaloidun ext4-tiedostojärjestelmän, jossa on laajennetut attribuutit trusted.overlay.* ja security.capability, merkkilaitteiden whiteout-solmut (0:0), d_type-hakemistomerkinnät sekä RENAME_EXCHANGE-tuki. Tämän ansiosta sisempi dockerd voi käyttää omaa natiivia overlayfs-tallennusajuriaan.

3. Itsenäinen netstack docker0-sillalla ja iptables-legacy-työkalulla

gvx:n sisällä dockerd ei ole vuorovaikutuksessa ulomman runsc-kontin verkkoliittymien tai netfilter-taulujen kanssa. Sen sijaan:

  • dockerd luo docker0-siltansa ja veth-parinsa gvx:n omassa käyttäjätilan netstack-kerroksessa.
  • Kaikki iptables-legacy-säännöt (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) suoritetaan gvx:n sisäistä netfilter-toteutusta vastaan.
  • Sisäkkäiset runc-kontit liittävät /proc-, /sys- ja /sys/fs/cgroup-hierarkiansa gvx:n muistinvaraiseen VFS2-liitostauluun eivätkä koskaan kutsu ulomman runsc:n mount(2)-järjestelmäkutsua.

Sisäkkäisen docker:dind-ympäristön todentaminen gVisor-ympäristössä (tai gvx gvx:ssä)

Voit todentaa molemmat skenaariot (rungv runsc:n sisällä tai rungv rungv:n sisällä) seuraavilla komennoilla:

 1# Ulomman gVisor (runsc) -kontin tai ulomman rungv-kontin sisällä:
 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# Varmista, että sisempi Docker-daemon käyttää natiivia overlayfs-tallennusajuria (ei vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Tuloste: Storage Driver: overlayfs
16
17# Suorita monikerroksinen build ja käynnistä sisäkkäinen kontti
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# Tuloste: nested-build-ok
31
32kill -INT $DIND_PID

Lisälukemista tästä sarjasta

  1. gvx: Running Linux Containers, Docker, and Kubernetes in One Unprivileged Process: Yleiskatsaus gvx:stä Windowsissa (whpx, systrap), macOS:ssä (hvf) ja Linuxissa (systrap).
  2. Running Docker, docker build, and docker compose Without Root or User Namespaces: Miten gvx suorittaa docker:dind-ympäristön silloin, kun root, sudo, unshare(CLONE_NEWUSER), mount(2) ja /dev/fuse on estetty.
  3. Running Docker-in-Docker Inside gVisor and Nested Sandboxes with gvx (tämä kirjoitus)
  4. Fast Isolated Docker Environments for AI Agent RL Rollouts with gvx: Miten gvx:n ja yksitiedostoisten ext4lite-copy-on-write-tilannekuvien avulla voi ajaa satoja rinnakkaisia SWE-bench- ja RL-rollout-ympäristöjä 100 millisekunnin palautuksilla.