GoSuda

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

By Lemon Mint views ...

Soluzione rapida: eseguire Docker senza privilegi in tre passaggi

Se si lavora all'interno di un container Linux bloccato, di un CI runner, di un Kubernetes Pod o di una sandbox per agenti AI in cui root, sudo, i user namespaces (unshare(CLONE_NEWUSER)), mount(2) e /dev/fuse sono bloccati, gvx (rungv) è la soluzione sicura e basata sulla difesa in profondità per eseguire un motore docker:dind completo nello user space.

Perché gvx è sicuro per progettazione (non è un workaround per l'escalation dei privilegi): a differenza del mount del socket dell'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 degli 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 di gVisor (Sentry) di Google, un kernel user-space memory-safe. Tutte le capability UID 0 del guest, i mount overlayfs (ext4lite) e le regole iptables restano rigorosamente isolate all'interno del processo sandbox.

 1# 1. Installa rungv nello user space (release SLSA-attested precompilata oppure go install)
 2# Opzione A: verifica ed estrae il binario precompilato con attestazione SLSA 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: in alternativa, installa direttamente dal sorgente con Go
 9CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest
10
11# 2. Avvia 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. Indirizza 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é gli strumenti rootless standard falliscono nelle sandbox con restrizioni

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

1. Assenza del socket del daemon Docker sull'host

L'esecuzione di docker ps o docker build fallisce immediatamente quando il socket dell'host /var/run/docker.sock non è montato nell'ambiente:

1Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

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 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 politiche seccomp e sysctl bloccano unshare(CLONE_NEWUSER)

Gli strumenti presentati come «rootless» (Rootless Docker, Podman, Buildah, nerdctl, bubblewrap) si basano comunque sui user namespaces 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 può scrivere su /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) è bloccato senza CAP_SYS_ADMIN sull'host

Anche se un ambiente consente CLONE_NEWUSER, il mount di un nuovo /proc all'interno di un mount namespace annidato fallisce se il container esterno ha mascherato sottoalberi di /proc (/proc/acpi, /proc/kcore, /proc/keys) oppure ha escluso mount(2) dal seccomp:

1mount("/proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL): Operation not permitted
2OCI 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
3bwrap: Creating new namespace failed: Operation not permitted

5. L'assenza di /dev/fuse compromette fuse-overlayfs

I motori di container rootless che non possono montare l'overlayfs del kernel ripiegano su fuse-overlayfs. Nei container minimali o nei pod Kubernetes privi del 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 nello user space

gvx aggira 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. Al contrario, rungv esegue un kernel Linux interno nello user space (il Sentry di gVisor) e uno stack di rete 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 il proprio UID host non privilegiato.

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 filesystem integrato ext4lite in /var/lib/docker.

Poiché ext4lite implementa un filesystem ext4 con journal JBD2 all'interno di un singolo file dell'host (gvx-docker.ext4), con supporto per gli attributi estesi trusted.overlay.*, i dispositivi whiteout di tipo carattere (0:0), d_type e RENAME_EXCHANGE, dockerd utilizza il driver di storage overlayfs nativo 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 nello 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 di dominio Unix dell'host (modalità 0600) e inoltra le connessioni direttamente alla porta 2375 di dockerd all'interno della sandbox.

Confronto con gli strumenti container esistenti

Capacità / VincoloDocker standard (dockerd)Docker Rootless / PodmanKaniko / Buildah (non privilegiato)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/DNoBuildah: No / Kaniko: SìSì
Funziona senza /dev/fuseSì (usa overlay2 del kernel)Solo tramite il lento driver vfs di copiaSìSì (overlayfs su ext4lite)
Supporta docker build (BuildKit)SìSì (se userns è consentito)Solo build di immagini (senza 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 namespaces. Su un host Linux, rungv si affida solo a tre primitive del kernel standard 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 stacca. Se l'host imposta kernel.yama.ptrace_scope = 3 (che disabilita ptrace in tutto il 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 processo padre a figlio.)
  2. seccomp(SECCOMP_SET_MODE_FILTER): utilizzato per intercettare le system call del guest provenienti 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, ...): utilizzato 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

Lo script seguente 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 utilizzando 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 lavoro 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 cross-platform (whpx, hvf, systrap), di VFS2 (erofs, ext4lite, hostfs v2) e di netstack.
  2. Esecuzione di Docker, docker build e docker compose senza root o user namespaces (questo articolo)
  3. Esecuzione di Docker-in-Docker all'interno di gVisor e sandbox annidate con gvx: come gvx risolve modprobe, iptables-nft, overlay2 su gofer e il SECCOMP_FILTER_FLAG_NEW_LISTENER annidato (EBUSY) all'interno di gVisor (runsc).
  4. Ambienti Docker isolati e rapidi per i rollout RL di agenti AI con gvx: scalabilità di ambienti Docker paralleli per SWE-bench e rollout RL con reset dei volumi .ext4 copy-on-write in 100 millisecondi.