GoSuda

Docker-in-Docker-ympäristön ajaminen gVisorin ja sisäkkäisten Sandboxien sisällä gvx:n avulla

By Lemon Mint views ...

Pikaratkaisu: docker:dind-kontin turvallinen suoritus etuoikeudettomassa gVisor (runsc) -Sandboxissa

Pilvipohjaiset tekoälyagenttialustat, Kubernetes-agent-sandbox-käyttöönotot (RuntimeClass: gvisor) ja vahvistusoppimisen arviointiympäristöt eristävät luottamattomat työkuormat gVisor (runsc) -kontteihin nimenomaan isäntäytimen suojaamiseksi. Kun oletusarvoisesti etuoikeudettomassa runsc-kontissa toimiva työkuorma yrittää asentaa paketin docker.io tai docker-ce komennolla apt-get ja käynnistää dockerd:n, se epäonnistuu, vaikka sillä olisi kontin sisällä root (UID 0).

gvx (rungv):n avulla voit turvallisesti suorittaa docker:dind-, docker build- ja docker compose -komennot suoraan etuoikeudettomassa gVisor (runsc) -kontissa (tai sijoittaa gvx:n gvx:n sisälle) heikentämättä ulomman RuntimeClass:n turvallisuutta, paljastamatta isännän socketeja tai myöntämättä --privileged-oikeutta.

Ulomman Sandbox-eristyksen säilyttäminen ja vahvistaminen: rungv:n suorittaminen runsc:n 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 -ytimen, jolla on oma hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), oma muistinvarainen netstack ja yksitiedostoinen ext4lite-volyymi. Sisäiset dockerd- ja runc-kontit ovat kaksinkertaisesti sandboxattuja: niiden järjestelmäkutsut, liitokset ja raakapaketit eivät koskaan poistu sisäisen gvx:n käyttäjätilaytimen sisältä.

 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 sisäistä gvx-socketia vasten
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# Tulos: overlayfs Alpine Linux ... (containerized)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Viisi syytä, miksi suora Docker-asennus epäonnistuu gVisor:n (runsc) sisällä

Kun käyttäjä tai tekoälyn koodausagentti suorittaa komennon apt-get install -y docker.io (tai docker-ce) tavallisen runsc-kontin sisällä ja yrittää käynnistää dockerd:n, se kohtaa viisi peräkkäistä teknistä estettä.

1. Ytimen moduulien lataaminen (modprobe) virtuaalista ydintä 4.4.0 vastaan

Paketin asennuksen ja dockerd:n käynnistyksen aikana alustuskomentosarjat suorittavat modprobe-komennon ladatakseen moduulit overlay, br_netfilter, ip_tables, nf_nat ja xt_conntrack. Ulompi gVisor Sentry estää kutsut init_module(2) ja finit_module(2) ja raportoi virtuaalisen ytimen version (4.4.0), jolle ei ole hakemistoa /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. iptables-nft:n (nf_tables Netlink) ja docker0-sillan virheet

Ubuntu 22.04:ssä, Ubuntu 24.04:ssä ja Debian 11/12:ssa apt-get install docker.io valitsee oletusvaihtoehdoksi polulle /usr/sbin/iptables vaihtoehdon iptables-nft (jota tukee nf_tables-netlink-alijärjestelmä). Tavallisessa gVisor-kontissa ilman verkon ylläpitäjän oikeuksia tai nf_tables-netlink-tukea dockerd kaatuu alustaessaan DOCKER NAT -ketjua ja docker0-siltaa:

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

Ellei ulomman runsc:n ylläpitäjä ole nimenomaisesti esikonfiguroinut omistettua tmpfs- tai filestore-liitosta kohteeseen /var/lib/docker, kontin juurijärjestelmä perustuu gVisorin gofer- (9p/lisafs) tai sisäkkäiseen overlay-liitokseen. overlay2:n liittäminen kohteen /var/lib/docker päälle 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 ohittaa tämän virheen käynnistämällä dockerd --storage-driver=vfs:n, Docker poistaa kokonaan Copy-on-Write-kerrosten jakamisen käytöstä. Jokainen Dockerfile:n RUN-ohje suorittaa koko juurijärjestelmän täyden rekursiivisen kopioinnin (dirCopy), mikä kertoo 500 MB:n kuvan 10 buildivaiheessa 5 GB:n kirjoitukseksi ja päättää sandboxin virheeseen no space left on device tai I/O-aikakatkaisuun.

4. Sisäkkäisten runc-cgroup- ja /proc-liitosten hylkäämiset

Vaikka dockerd pakotettaisiin käynnistymään komennolla dockerd --iptables=false --bridge=none --storage-driver=vfs, komennot docker run tai docker build kutsuvat runc:n luomaan sisäisen kontin. Oletusarvoisessa etuoikeudettomassa gVisor-kontissa runc epäonnistuu liittäessään cgroupeja, liittäessään /proc:n uudelleen tai ladatessaan sisäisiä seccomp-suodattimia:

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

Jos käyttäjä yrittää suorittaa toisen runsc-instanssin runsc:n sisällä systrap-alustaa käyttäen, 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-ydin että gVisor Sentry sallivat tehtävähierarkiaa kohden enintään yhden SECCOMP_FILTER_FLAG_NEW_LISTENER- (SECCOMP_RET_USER_NOTIF) -kuuntelijan. Koska ulompi sandbox on jo varannut kuuntelijapaikan, sisäinen seccomp(2)-kutsu palauttaa arvon EBUSY (device or resource busy).


Miten gvx ratkaisee sisäkkäisen suorituksen gVisor:n sisällä

gvx ratkaisee kaikki viisi estettä omassa käyttäjätilaytimessään ja alustakerroksessaan, mikä mahdollistaa rungv:n 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 kahdentyyppisiä säikeitä:

  • Vierasjärjestelmän suoritussäikeet (sysmsg): Kaappaavat vierasjärjestelmäkutsut SECCOMP_RET_TRAP-mekanismilla (SIGSYS) ja binääripatchatuilla jmp rel32 -trampoliineilla (syshandler), vaihtaen rekisteritilan Sentryn kanssa jaetun muistin ja SYS_FUTEX:n kautta.
  • 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()-vaiheen aikana t.installSeccompNotify() asentaa SECCOMP_RET_USER_NOTIF-kuuntelijan (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), jotta Sentry voi lähettää pyynnöt 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) ja nukkuu SYS_FUTEX:n (FUTEX_WAIT) varassa jaetulla 2-sivun alueella syscallSentryMessage (stubissa PROT_READ) ja syscallStubMessage (stubissa PROT_READ | PROT_WRITE).

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

gvx:ssä (pkg/sentry/platform/systrap/seccomp_notify_xplat.go ja syscall_thread.go) syscallThread.init() kutsuu funktiota 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}

Kun installSeccompNotify() palauttaa arvon unix.EBUSY, koska ulompi gVisor (runsc) tai ulompi gvx-sandbox on jo varannut prosessin suodatinketjun ainoan kuuntelijapaikan:

  • installSeccompNotifyIfFree() asettaa muuttujan seccompNotifyBusy (atomic.Bool), kirjaa prosessille yhden varoituksen ja jättää t.seccompNotify == nil-arvoksi kyseiselle stubille ja kaikille sitä seuraaville stubeille.
  • syscallThread.detach()-funktiossa (syscall_thread_amd64.go ja syscall_thread_arm64.go) t.seccompNotify == nil asettaa amd64-arkkitehtuurilla arvon regs.Rbx = _RUN_SYSCALL_LOOP (5) (tai arm64-arkkitehtuurilla regs.Regs[9] = _RUN_SYSCALL_LOOP) arvon _RUN_SECCOMP_LOOP (6) sijaan.
  • syscallThread.syscall()-funktiossa (syscall_thread.go) sisäinen Sentry välittää stub-järjestelmäkutsut yksinomaan jaetun muistin ja SYS_FUTEX:n kautta:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Ilmoita syscall-säikeelle uudesta järjestelmäkutsupyynnö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 vierasäikeillä voivat pinota mielivaltaisen syviksi ja syscallThread palaa _RUN_SYSCALL_LOOP-silmukkaan pyytämättä toista käyttäjäilmoituskuuntelijaa, gvx toimii moitteettomasti runsc:n sisällä ja tukee rekursiivista sisäkkäisyyttä (gvx gvx:n sisällä).

2. Natiivi overlayfs yksitiedostoisilla ext4lite-volyymeilla

Jotta overlayfs:ää ei liitettäisi ulomman gVisor-kontin gofer- (9p/lisafs) juurijärjestelmän päälle, -v gvx-docker:/var/lib/docker tukee kohdetta /var/lib/docker gvx:n sisäisellä ext4lite-tiedostojärjestelmällä, joka on tallennettu yhteen tavalliseen tiedostoon (gvx-docker.ext4).

Ulomman runsc:n näkökulmasta gvx-docker.ext4 on vain yksi tavallinen tiedosto, johon päästään käsiksi pread64(2)- ja pwrite64(2)-kutsuilla. gvx:n sisällä ext4lite tarjoaa JBD2-lokitetun ext4-tiedostojärjestelmän, jossa on trusted.overlay.*- ja security.capability-laajennetut attribuutit, merkkilaitteen whiteout-solmut (0:0), d_type-hakemistomerkinnät ja RENAME_EXCHANGE, mikä mahdollistaa sisäisen dockerd:n käyttää natiivia overlayfs-tallennusajuriaan.

3. Itsenäinen netstack, jossa docker0 ja iptables-legacy

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

  • dockerd luo docker0-siltansa ja veth-parinsa gvx:n oman käyttäjätilan netstack-pinon sisällä.
  • 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 muistissa olevaan VFS2-liitostauluun, eivätkä koskaan kutsu ulomman runsc:n mount(2)-järjestelmäkutsua.

Sisäkkäisen docker:dind-kontin todentaminen gVisor:n sisällä (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ä sisäinen Docker-daemon käyttää natiivia overlayfs-tiedostojärjestelmää (ei vfs:ää)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Tulos: Storage Driver: overlayfs
16
17# Suorita monikerroksinen build ja suorita 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# Tulos: nested-build-ok
31
32kill -INT $DIND_PID

Lisälukemista tästä sarjasta

  1. gvx: Linux-konttien, Dockerin ja Kubernetesin ajaminen yhdessä etuoikeudettomassa prosessissa: Yleiskatsaus gvx:stä Windowsissa (whpx, systrap), macOS:ssä (hvf) ja Linuxissa (systrap).
  2. Docker-, docker build- ja docker compose -komentojen suorittaminen ilman juurioikeuksia tai käyttäjänimiavaruuksia: Miten gvx suorittaa docker:dind-kontin, kun root, sudo, unshare(CLONE_NEWUSER), mount(2) ja /dev/fuse on estetty.
  3. Docker-in-Docker-kontin suorittaminen gVisorin ja sisäkkäisten sandboxien sisällä gvx:llä (tämä kirjoitus)
  4. Nopeat eristetyt Docker-ympäristöt tekoälyagenttien RL-rollouteihin gvx:llä: Miten käyttää gvx:ää ja yksitiedostoisia ext4lite copy-on-write -tilannekuvia satojen rinnakkaisten SWE-bench- ja RL-rollout-ympäristöjen ajamiseen 100 millisekunnin nollauksilla.