GoSuda

Ejecución de Docker, docker build y docker compose sin privilegios de root ni espacios de nombres de usuario

By Lemon Mint views ...

Solución rápida: ejecutar Docker sin privilegios en tres pasos

Si trabaja dentro de un contenedor Linux bloqueado, un runner de CI, un Kubernetes Pod o el entorno sandbox de un agente de codificación con IA (como un entorno de cómputo en la nube para agentes) donde root, sudo, los user namespaces (unshare(CLONE_NEWUSER)), mount(2) y /dev/fuse están bloqueados, gvx (rungv) es la solución segura, de bajo esfuerzo y eficiente en tokens para ejecutar un motor docker:dind completo en espacio de usuario.

Solución de bajo esfuerzo y eficiente en tokens para agentes de codificación con IA: Cuando un agente de codificación autónomo encuentra Cannot connect to the Docker daemon, intentar instalar y parchear docker.io, podman o rootlesskit dentro de un contenedor restringido suele desperdiciar entre 15 y 30 turnos de llamadas a herramientas y decenas de miles de tokens de contexto alternando entre errores de systemctl, ulimit, iptables, unshare y runc, para terminar fracasando o recurriendo a reescribir los Dockerfile a mano. Con gvx (rungv), el agente ejecuta un arranque determinista de 3 pasos en un solo turno y de inmediato ejecuta los comandos estándar docker build, docker run y docker compose sin modificaciones.

Por qué gvx es seguro por diseño (no es un workaround de escalada de privilegios): A diferencia de montar el /var/run/docker.sock del host o de ejecutar docker run --privileged (que otorgan un control equivalente a root sobre el kernel del host), gvx no evita ni debilita ninguna política de seguridad del host (PR_SET_NO_NEW_PRIVS, seccomp, AppArmor o los límites de UID no root). En cambio, rungv bloquea su propio proceso del host (PR_SET_NO_NEW_PRIVS + una allowlist BPF seccomp estricta) y ejecuta dockerd dentro del kernel de espacio de usuario gVisor (Sentry) de Google, que tiene seguridad de memoria. Todas las capabilities de UID 0 del invitado, los montajes overlayfs (ext4lite) y las reglas de iptables permanecen estrictamente aisladas dentro del proceso del sandbox.

 1# 1. Instalar rungv en el espacio de usuario (binario precompilado con atestación SLSA o go install)
 2# Opción A: Verificar y extraer el binario precompilado con atestación SLSA desde 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# Opción B: o instalar directamente desde el código fuente con Go
 9CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest
10
11# 2. Iniciar docker:dind dentro de gvx con un volumen ext4lite de un solo archivo
12#    Reenvía ~/.gvx/docker.sock (modo 0600) al puerto 2375 del invitado
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. Apuntar la CLI estándar de Docker y Docker Compose al socket del espacio de usuario
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# Salida: overlayfs Alpine Linux ... (containerized)
26
27docker build -t app:test .
28docker run --rm app:test
29docker compose up -d

Por qué las herramientas estándar sin root fallan en sandboxes restringidos

Cuando un ingeniero o un agente de codificación autónomo necesita ejecutar docker build, docker run o docker compose dentro de un contenedor endurecido o de un runner multiinquilino, los workarounds habituales fallan en cinco barreras distintas del kernel.

1. No hay socket del daemon de Docker en el host (systemctl y service docker start también fallan)

Ejecutar docker ps o docker build falla de inmediato cuando el /var/run/docker.sock del host no está montado en el entorno, e intentar iniciar docker.io mediante systemctl o service docker start también falla, porque PID 1 no es systemd y /etc/init.d/docker choca con el límite duro ulimit -n del contenedor:

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 bloquea sudo y los helpers setuid

Los contenedores endurecidos (securityContext.allowPrivilegeEscalation: false en Kubernetes, --security-opt=no-new-privileges en Docker, o sandboxes exebox / bwrap) activan el bit PR_SET_NO_NEW_PRIVS de Linux en el árbol de procesos. Intentar escalar privilegios mediante sudo o binarios setuid (newuidmap, newgidmap, fusermount3) falla:

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. Las políticas de seccomp y sysctl bloquean unshare(CLONE_NEWUSER)

Las herramientas promocionadas como "sin root" (Rootless Docker, Podman, Buildah, nerdctl, bubblewrap) siguen dependiendo de los user namespaces de Linux (CLONE_NEWUSER). Fallan siempre que:

  • El perfil seccomp predeterminado del contenedor externo bloquea unshare(2) o clone(CLONE_NEWUSER).
  • Ubuntu 24.04+ habilita kernel.apparmor_restrict_unprivileged_userns = 1.
  • Los nodos Debian o RHEL establecen kernel.unprivileged_userns_clone = 0 o user.max_user_namespaces = 0.
  • newuidmap no puede escribir en /proc/self/uid_map porque PR_SET_NO_NEW_PRIVS está activo.

Los errores resultantes tienen este aspecto:

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) y pivot_root(2) están bloqueados durante docker build sin CAP_SYS_ADMIN en el host

Incluso si un entorno permite CLONE_NEWUSER o si dockerd se fuerza a iniciar con --iptables=false --bridge=none --storage-driver=vfs, ejecutar docker build falla en la primera instrucción RUN, porque montar un /proc nuevo o llamar a pivot_root(2) dentro de un mount namespace anidado está bloqueado:

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. La falta de /dev/fuse rompe fuse-overlayfs

Los motores de contenedores sin root que no pueden montar el overlayfs del kernel recurren a fuse-overlayfs. En contenedores mínimos o pods de Kubernetes sin passthrough del dispositivo /dev/fuse, fuse-overlayfs falla de inmediato:

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

Cómo gvx ejecuta docker:dind completamente en espacio de usuario

gvx evita las cinco barreras del kernel porque no pide al kernel Linux del host que cree namespaces, monte sistemas de archivos ni configure interfaces de red. En cambio, rungv ejecuta un kernel Linux interno de espacio de usuario (el Sentry de gVisor) y una pila de red de espacio de usuario (netstack) dentro de un único proceso ordinario sin privilegios.

Ejecutando Docker-in-Docker sin privilegios con gvx

1. Namespaces en memoria y tabla de montajes (VFS2)

Cuando dockerd, containerd y runc se ejecutan dentro de rungv, systrap intercepta sus llamadas al sistema y las envía al kernel Sentry en proceso, en lugar de al kernel del host:

  • unshare(2), clone(2), pivot_root(2), chroot(2) y mount(2) operan sobre la jerarquía de tareas en memoria del Sentry y sobre la tabla de montajes VFS2 (procfs, sysfs, tmpfs, devpts, cgroupfs v2), mientras el proceso del host rungv se ejecuta bajo su UID de host sin privilegios.

2. overlayfs nativo sobre volúmenes ext4lite de un solo archivo

Cuando pasa -v gvx-docker:/var/lib/docker (o --mount type=volume,src=/disk/gvx-docker.ext4,dst=/var/lib/docker), gvx monta su sistema de archivos integrado ext4lite en /var/lib/docker.

Dado que ext4lite implementa un sistema de archivos ext4 con journaling JBD2 dentro de un único archivo del host (gvx-docker.ext4), con soporte para los atributos extendidos trusted.overlay.*, los dispositivos whiteout de caracteres (0:0), d_type y RENAME_EXCHANGE, dockerd utiliza su controlador de almacenamiento nativo overlayfs dentro del invitado. Las operaciones docker build de varias etapas comparten capas Copy-on-Write sin necesidad de /dev/fuse y sin la explosión de espacio en disco del controlador de respaldo vfs de Docker.

3. Puente docker0 en espacio de usuario e iptables-legacy

Cuando docker:dind se inicia dentro de gvx:

  • El dockerd del invitado crea el puente docker0 (172.17.0.1/16), conecta pares veth virtuales para los contenedores anidados y programa las cadenas NAT (DOCKER, POSTROUTING) y FILTER con coincidencias addrtype y conntrack dentro del netstack de espacio de usuario de gvx.
  • Pasar -p unix:~/.gvx/docker.sock:2375 enlaza un socket de dominio Unix del host (modo 0600) y reenvía las conexiones directamente al puerto 2375 de dockerd dentro del sandbox.

Comparación con las herramientas de contenedores existentes

Capacidad / RestricciónDocker estándar (dockerd)Docker sin root / PodmanKaniko / Buildah (sin privilegios)gvx (rungv + docker:dind)
Requiere root o sudo en el hostSíNoNoNo
Funciona con PR_SET_NO_NEW_PRIVS=1NoNo (newuidmap falla)ParcialSí
Funciona cuando unshare(CLONE_NEWUSER) está bloqueado (EPERM)N/ANoBuildah: No / Kaniko: SíSí
Funciona sin /dev/fuseSí (usa overlay2 del kernel)Solo mediante vfs lento con copiaSíSí (overlayfs sobre ext4lite)
Admite docker build (BuildKit)SíSí (si userns está permitido)Solo construcción de imágenes (sin runtime)Sí
Admite docker run y docker compose upSíSí (si userns está permitido)NoSí
Se ejecuta en Windows y macOS sin una VM LinuxNo (requiere WSL2 / VM)No (requiere VM Linux)NoSí (whpx, hvf, systrap)

Requisitos exactos del kernel del host en Linux

gvx requiere cero capabilities elevadas (CapEff: 0000000000000000) y cero user namespaces. En un host Linux, rungv depende solo de tres primitivas estándar del kernel sin privilegios:

  1. ptrace(PTRACE_ATTACH) sobre su propio hilo hijo una vez durante la inicialización: systrap crea un hilo auxiliar stub local al iniciarse, se adjunta a él para inicializar el contexto de memoria compartida y luego se desadjunta. Si el host establece kernel.yama.ptrace_scope = 3 (lo que deshabilita ptrace en todo el sistema) o bloquea ptrace en un filtro seccomp externo, la inicialización de systrap no puede completarse. (Con yama.ptrace_scope = 1, el valor predeterminado en Ubuntu y Debian, gvx funciona porque se permite el trazado de padre a hijo.)
  2. seccomp(SECCOMP_SET_MODE_FILTER): se usa para interceptar las llamadas al sistema del invitado desde los hilos stub del contenedor. Como se explica en la Parte 3 de esta serie, gvx funciona tanto cuando SECCOMP_FILTER_FLAG_NEW_LISTENER está disponible como cuando devuelve EBUSY dentro de un sandbox anidado.
  3. mmap(..., PROT_EXEC, ...): se usa para mapear los segmentos ELF del invitado y el trampolín de memoria compartida de systrap.

Ejemplo completo: construir y probar una aplicación multicontenedor sin root

El siguiente script ejecuta un docker build completo y una prueba de integración con dos contenedores (redis + contenedor de la aplicación) dentro de una sesión de usuario sin privilegios:

 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# Construir la imagen usando overlayfs sobre ext4lite
20docker build -t demo-ping:latest .
21
22# Crear una red bridge, iniciar Redis y verificar la conectividad entre contenedores
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# Salida: PONG
27
28# Limpiar los contenedores de carga de trabajo y detener el sandbox
29docker rm -f cache
30docker network rm demo-net
31kill -INT $DIND_PID

Lecturas adicionales en esta serie

  1. gvx: Ejecutar contenedores Linux, Docker y Kubernetes en un solo proceso sin privilegios: visión general de la arquitectura de gvx, sus motores de ejecución multiplataforma (whpx, hvf, systrap), VFS2 (erofs, ext4lite, hostfs v2) y netstack.
  2. Ejecutar Docker, docker build y docker compose sin root ni user namespaces (esta entrada)
  3. Ejecutar Docker-in-Docker dentro de gVisor y sandboxes anidados con gvx: cómo gvx resuelve modprobe, iptables-nft, overlay2 sobre gofer y SECCOMP_FILTER_FLAG_NEW_LISTENER anidado (EBUSY) dentro de gVisor (runsc).
  4. Entornos Docker aislados y rápidos para rollouts de RL de agentes de IA con gvx: escalado de entornos paralelos de SWE-bench y rollouts de RL con reinicios de volúmenes .ext4 Copy-on-Write de 100 milisegundos.