GoSuda

Esecuzione di Docker, docker build e docker compose senza root o user namespaces

By Lemon Mint views ...

Soluzione rapida: eseguire Docker non privilegiato in tre passaggi

Se si lavora all'interno di un container Linux bloccato, di un runner CI, di un Pod Kubernetes o di una sandbox di un agente di coding basato su IA (ad esempio un ambiente di calcolo cloud per agenti) in cui root, sudo, i user namespace (unshare(CLONE_NEWUSER)), mount(2) e /dev/fuse sono bloccati, gvx (rungv) rappresenta la soluzione sicura, a basso impegno ed efficiente in termini di token per eseguire un intero motore docker:dind in user space.

Soluzione a basso impegno ed efficiente in termini di token per agenti di coding basati su IA: Quando un agente di coding autonomo incontra Cannot connect to the Docker daemon, tentare di installare e modificare docker.io, podman o rootlesskit all'interno di un container con restrizioni comporta tipicamente da 15 a 30 turni di chiamate agli strumenti e decine di migliaia di token di contesto a ciclare tra errori di systemctl, ulimit, iptables, unshare e runc, con l'unico risultato di fallire o di ripiegare sulla riscrittura manuale dei Dockerfile. Con gvx (rungv), l'agente esegue un bootstrap deterministico in 3 passaggi in un unico turno ed esegue immediatamente i comandi standard docker build, docker run e docker compose senza modifiche.

Perché gvx è sicuro by design (non è un workaround per l'escalation dei privilegi): A differenza del montaggio del socket host /var/run/docker.sock o dell'esecuzione di docker run --privileged (che concedono un controllo equivalente a root sul kernel dell'host), gvx non aggira né indebolisce alcuna politica di sicurezza dell'host (PR_SET_NO_NEW_PRIVS, seccomp, AppArmor o i limiti di UID non root). Al contrario, rungv blocca il proprio processo host (PR_SET_NO_NEW_PRIVS + allowlist BPF seccomp rigorosa) ed esegue dockerd all'interno del kernel user-space gVisor (Sentry) di Google, memory-safe. Tutte le capability di UID 0 del guest, i mount overlayfs (ext4lite) e le regole iptables restano strettamente isolate all'interno del processo della sandbox.

 1# 1. Installare rungv in user space (release SLSA-attested precompilata oppure go install)
 2# Opzione A: Verificare ed estrarre il binario precompilato SLSA-attested dalle GitHub Releases
 3gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
 4  --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
 5  --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
 6tar -xzf rungv-v0.2.0-linux-amd64.tar.gz && install -m 0755 rungv-v0.2.0-linux-amd64/rungv ~/.local/bin/rungv
 7
 8# Opzione B: Oppure installare direttamente dai sorgenti con Go
 9CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest
10
11# 2. Avviare docker:dind all'interno di gvx con un volume ext4lite a file singolo
12#    Inoltra ~/.gvx/docker.sock (modalità 0600) alla porta 2375 del guest
13mkdir -p ~/.gvx
14~/.local/bin/rungv run \
15  -p unix:~/.gvx/docker.sock:2375 \
16  -p 127.0.0.1:9000-9019:9000-9019 \
17  -v gvx-docker:/var/lib/docker \
18  -e DOCKER_TLS_CERTDIR= \
19  docker:dind --tls=false &
20
21# 3. Puntare la CLI Docker standard e Docker Compose al socket user-space
22export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
23until docker info >/dev/null 2>&1; do sleep 0.5; done
24docker info --format '{{.Driver}} {{.OperatingSystem}}'
25# Output: overlayfs Alpine Linux ... (containerizzato)
26
27docker build -t app:test .
28docker run --rm app:test
29docker compose up -d

Perché i tool rootless standard falliscono nelle sandbox con restrizioni

Quando un ingegnere o un agente di coding autonomo deve eseguire docker build, docker run o docker compose all'interno di un container indurito o di un runner multi-tenant, i workaround standard falliscono su cinque barriere distinte del kernel.

1. Assenza del socket del daemon Docker sull'host (anche systemctl e service docker start falliscono)

L'esecuzione di docker ps o docker build fallisce immediatamente quando il socket host /var/run/docker.sock non è montato nell'ambiente, e il tentativo di avviare docker.io tramite systemctl o service docker start fallisce perché PID 1 non è systemd e /etc/init.d/docker raggiunge il limite rigido di ulimit -n del container:

1Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
2ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
3System has not been booted with systemd as init system (PID 1). Can't operate.
4Failed to connect to bus: Host is down
5/etc/init.d/docker: 62: ulimit: error setting limit (Operation not permitted)

2. PR_SET_NO_NEW_PRIVS blocca sudo e gli helper setuid

I container induriti (securityContext.allowPrivilegeEscalation: false in Kubernetes, --security-opt=no-new-privileges in Docker, oppure le sandbox exebox / bwrap) impostano il bit Linux PR_SET_NO_NEW_PRIVS sull'albero dei processi. Il tentativo di ottenere privilegi tramite sudo o binari setuid (newuidmap, newgidmap, fusermount3) fallisce:

1sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
2sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?

3. Le policy di seccomp e sysctl bloccano unshare(CLONE_NEWUSER)

Gli strumenti presentati come "rootless" (Rootless Docker, Podman, Buildah, nerdctl, bubblewrap) continuano a dipendere dai user namespace Linux (CLONE_NEWUSER). Falliscono ogni volta che:

  • Il profilo seccomp predefinito del container esterno blocca unshare(2) o clone(CLONE_NEWUSER).
  • Ubuntu 24.04+ abilita kernel.apparmor_restrict_unprivileged_userns = 1.
  • I nodi Debian o RHEL impostano kernel.unprivileged_userns_clone = 0 oppure user.max_user_namespaces = 0.
  • newuidmap non riesce a scrivere in /proc/self/uid_map perché PR_SET_NO_NEW_PRIVS è attivo.

Gli errori risultanti hanno questo aspetto:

1unshare: unshare failed: Operation not permitted
2cannot clone: Operation not permitted
3Error: cannot re-exec process to join the existing user namespace
4newuidmap: write to uid_map failed: Operation not permitted
5RootlessKit: parent: failed to start the child: fork/exec /proc/self/exe: operation not permitted

4. mount(2) e pivot_root(2) sono bloccati durante docker build senza CAP_SYS_ADMIN sull'host

Anche quando un ambiente consente CLONE_NEWUSER oppure dockerd viene forzato ad avviarsi con --iptables=false --bridge=none --storage-driver=vfs, l'esecuzione di docker build fallisce alla prima istruzione RUN, perché il mount di un nuovo /proc o la chiamata a pivot_root(2) all'interno di un mount namespace annidato sono bloccati:

1ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully:
2mount("/proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL): Operation not permitted
3OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
4OCI runtime create failed: runc create failed: unable to start container process: error during container init: error jailing process inside rootfs: pivot_root .: invalid argument
5bwrap: Creating new namespace failed: Operation not permitted

5. /dev/fuse mancante rompe fuse-overlayfs

I motori di container rootless che non possono montare l'overlayfs del kernel ricorrono a fuse-overlayfs. Nei container minimali o nei pod Kubernetes senza passthrough del dispositivo /dev/fuse, fuse-overlayfs fallisce immediatamente:

1fuse: device not found, try 'modprobe fuse' first
2fusermount3: failed to open /dev/fuse: No such file or directory
3error initializing graphdriver: driver not supported: fuse-overlayfs

Come gvx esegue docker:dind interamente in user space

gvx supera tutte e cinque le barriere del kernel perché non chiede al kernel Linux dell'host di creare namespace, montare filesystem o configurare interfacce di rete. Invece, rungv esegue un kernel Linux interno in user space (il Sentry di gVisor) e uno stack di rete in user space (netstack) all'interno di un unico processo ordinario non privilegiato.

Esecuzione di Docker-in-Docker non privilegiato con gvx

1. Namespace in memoria e tabella dei mount (VFS2)

Quando dockerd, containerd e runc vengono eseguiti all'interno di rungv, systrap intercetta le loro system call e le instrada verso il kernel Sentry in-process anziché verso il kernel dell'host:

  • unshare(2), clone(2), pivot_root(2), chroot(2) e mount(2) operano sulla gerarchia dei task in memoria del Sentry e sulla tabella dei mount VFS2 (procfs, sysfs, tmpfs, devpts, cgroupfs v2), mentre il processo host rungv viene eseguito con l'UID host non privilegiato dell'utente.

2. overlayfs nativo su volumi ext4lite a file singolo

Quando si passa -v gvx-docker:/var/lib/docker (oppure --mount type=volume,src=/disk/gvx-docker.ext4,dst=/var/lib/docker), gvx monta il proprio filesystem integrato ext4lite in /var/lib/docker.

Poiché ext4lite implementa un filesystem ext4 con journal JBD2 all'interno di un singolo file host (gvx-docker.ext4), con supporto per gli attributi estesi trusted.overlay.*, per i device di whiteout a caratteri (0:0), per d_type e per RENAME_EXCHANGE, dockerd utilizza il proprio driver di storage nativo overlayfs all'interno del guest. Le operazioni docker build multi-stage condividono i layer Copy-on-Write senza /dev/fuse e senza l'esplosione dello spazio su disco tipica del driver di fallback vfs di Docker.

3. Bridge docker0 in user space e iptables-legacy

Quando docker:dind si avvia all'interno di gvx:

  • Il dockerd del guest crea il bridge docker0 (172.17.0.1/16), collega coppie veth virtuali per i container annidati e programma le catene NAT (DOCKER, POSTROUTING) e FILTER con match addrtype e conntrack all'interno del netstack user-space di gvx.
  • Passare -p unix:~/.gvx/docker.sock:2375 associa un socket Unix domain dell'host (modalità 0600) e inoltra le connessioni direttamente alla porta 2375 di dockerd all'interno della sandbox.

Confronto con gli strumenti di container esistenti

Capacità / VincoloDocker standard (dockerd)Docker Rootless / PodmanKaniko / Buildah (non privilegiati)gvx (rungv + docker:dind)
Richiede root o sudo sull'hostSìNoNoNo
Funziona con PR_SET_NO_NEW_PRIVS=1NoNo (newuidmap fallisce)ParzialeSì
Funziona quando unshare(CLONE_NEWUSER) è bloccato (EPERM)N/ANoBuildah: No / Kaniko: SìSì
Funziona senza /dev/fuseSì (usa overlay2 del kernel)Solo tramite lenta copia vfsSìSì (overlayfs su ext4lite)
Supporta docker build (BuildKit)SìSì (se userns è consentito)Solo build di immagini (nessun runtime)Sì
Supporta docker run e docker compose upSìSì (se userns è consentito)NoSì
Funziona su Windows e macOS senza una VM LinuxNo (richiede WSL2 / VM)No (richiede una VM Linux)NoSì (whpx, hvf, systrap)

Requisiti esatti del kernel host su Linux

gvx richiede zero capability elevate (CapEff: 0000000000000000) e zero user namespace. Su un host Linux, rungv si basa solo su tre primitive standard del kernel non privilegiate:

  1. ptrace(PTRACE_ATTACH) sul proprio thread figlio, una sola volta durante l'inizializzazione: systrap avvia un thread helper stub locale all'avvio, vi si collega per inizializzare il contesto di memoria condivisa e si scollega. Se l'host imposta kernel.yama.ptrace_scope = 3 (che disabilita ptrace a livello di sistema) oppure blocca ptrace in un filtro seccomp esterno, l'inizializzazione di systrap non può completarsi. (Con yama.ptrace_scope = 1, il valore predefinito su Ubuntu e Debian, gvx funziona perché è consentito il tracciamento da genitore a figlio.)
  2. seccomp(SECCOMP_SET_MODE_FILTER): usato per intercettare le system call del guest dai thread stub del container. Come illustrato nella Parte 3 di questa serie, gvx funziona sia quando SECCOMP_FILTER_FLAG_NEW_LISTENER è disponibile, sia quando restituisce EBUSY all'interno di una sandbox annidata.
  3. mmap(..., PROT_EXEC, ...): usato per mappare i segmenti ELF del guest e il trampolino di memoria condivisa di systrap.

Esempio end-to-end: costruire e testare un'app multi-container senza root

Il seguente script esegue un docker build completo e un test di integrazione a due container (redis + container applicativo) all'interno di una sessione utente non privilegiata:

 1mkdir -p ~/.gvx
 2rungv run \
 3  -p unix:~/.gvx/docker.sock:2375 \
 4  -v gvx-docker:/var/lib/docker \
 5  -e DOCKER_TLS_CERTDIR= \
 6  docker:dind --tls=false &
 7DIND_PID=$!
 8
 9export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
10until docker info >/dev/null 2>&1; do sleep 0.5; done
11
12mkdir -p /tmp/demo-app && cd /tmp/demo-app
13cat <<'EOF' > Dockerfile
14FROM alpine:3.20
15RUN apk add --no-cache redis
16CMD ["redis-cli", "-h", "cache", "ping"]
17EOF
18
19# Costruisce l'immagine usando overlayfs su ext4lite
20docker build -t demo-ping:latest .
21
22# Crea una rete bridge, avvia Redis e verifica la connettività tra i container
23docker network create demo-net
24docker run -d --name cache --network demo-net redis:7-alpine
25docker run --rm --network demo-net demo-ping:latest
26# Output: PONG
27
28# Rimuove i container di workload e arresta la sandbox
29docker rm -f cache
30docker network rm demo-net
31kill -INT $DIND_PID

Ulteriori letture in questa serie

  1. gvx: Esecuzione di container Linux, Docker e Kubernetes in un unico processo non privilegiato: Panoramica architetturale di gvx, dei suoi motori di esecuzione multipiattaforma (whpx, hvf, systrap), di VFS2 (erofs, ext4lite, hostfs v2) e di netstack.
  2. Esecuzione di Docker, docker build e docker compose senza root né user namespace (questo articolo)
  3. Esecuzione di Docker-in-Docker all'interno di gVisor e di sandbox annidate con gvx: Come gvx risolve modprobe, iptables-nft, overlay2 su gofer e SECCOMP_FILTER_FLAG_NEW_LISTENER annidato (EBUSY) all'interno di gVisor (runsc).
  4. Ambienti Docker isolati e veloci per i rollout RL di agenti IA con gvx: Scalare ambienti paralleli per i rollout SWE-bench e RL con reset di volumi .ext4 Copy-on-Write in 100 millisecondi.