GoSuda

Exécuter Docker, docker build et docker compose sans root ni espaces de noms d'utilisateurs

By Lemon Mint views ...

Solution rapide : sécuriser Docker sans privilèges en trois étapes

Si vous travaillez dans un conteneur Linux verrouillé, un runner CI, un Pod Kubernetes ou un bac à sable d'agent IA où root, sudo, les user namespaces (unshare(CLONE_NEWUSER)), mount(2) et /dev/fuse sont bloqués, gvx (rungv) constitue la solution sécurisée et de défense en profondeur permettant d'exécuter un moteur docker:dind complet dans l'espace utilisateur.

Pourquoi gvx est sûr par conception (et non un contournement d'élévation de privilèges) : Contrairement au montage du socket hôte /var/run/docker.sock ou à l'exécution de docker run --privileged (qui confèrent un contrôle équivalent à root sur le noyau hôte), gvx ne contourne ni n'affaiblit aucune politique de sécurité de l'hôte (PR_SET_NO_NEW_PRIVS, seccomp, AppArmor, ou les limites des UID non root). À la place, rungv verrouille son propre processus hôte (PR_SET_NO_NEW_PRIVS + une liste blanche BPF seccomp stricte) et exécute dockerd à l'intérieur du noyau en espace utilisateur gVisor (Sentry) de Google, conçu pour la sécurité mémoire. Toutes les capacités UID 0 de l'invité, les montages overlayfs (ext4lite) et les règles iptables restent strictement isolés à l'intérieur du processus de bac à sable.

 1# 1. Installer rungv dans l'espace utilisateur (version préconstruite attestée SLSA ou go install)
 2# Option A : Vérifier et extraire le binaire préconstruit attesté SLSA depuis 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# Option B : Ou installer directement depuis les sources avec Go
 9CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest
10
11# 2. Démarrer docker:dind dans gvx avec un volume ext4lite à fichier unique
12#    Transmet ~/.gvx/docker.sock (mode 0600) au port invité 2375
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. Diriger le CLI Docker standard et Docker Compose vers le socket de l'espace utilisateur
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# Sortie : overlayfs Alpine Linux ... (containerisé)
26
27docker build -t app:test .
28docker run --rm app:test
29docker compose up -d

Pourquoi les outils rootless standard échouent dans les bacs à sable restreints

Lorsqu'un ingénieur ou un agent de codage autonome doit exécuter docker build, docker run ou docker compose dans un conteneur durci ou un runner multi-locataires, les contournements standard échouent face à cinq barrières distinctes du noyau.

1. Absence de socket du démon Docker hôte

L'exécution de docker ps ou de docker build échoue immédiatement lorsque le socket hôte /var/run/docker.sock n'est pas monté dans l'environnement :

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

2. PR_SET_NO_NEW_PRIVS bloque sudo et les assistants setuid

Les conteneurs durcis (securityContext.allowPrivilegeEscalation: false dans Kubernetes, --security-opt=no-new-privileges dans Docker, ou les bacs à sable exebox / bwrap) positionnent le bit Linux PR_SET_NO_NEW_PRIVS sur l'arborescence de processus. Toute tentative d'élévation via sudo ou des binaires setuid (newuidmap, newgidmap, fusermount3) échoue :

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. Les politiques seccomp et sysctl bloquent unshare(CLONE_NEWUSER)

Les outils présentés comme « rootless » (Rootless Docker, Podman, Buildah, nerdctl, bubblewrap) reposent néanmoins sur les user namespaces Linux (CLONE_NEWUSER). Ils échouent dès que :

  • Le profil seccomp par défaut du conteneur externe bloque unshare(2) ou clone(CLONE_NEWUSER).
  • Ubuntu 24.04+ active kernel.apparmor_restrict_unprivileged_userns = 1.
  • Les nœuds Debian ou RHEL définissent kernel.unprivileged_userns_clone = 0 ou user.max_user_namespaces = 0.
  • newuidmap ne peut pas écrire dans /proc/self/uid_map parce que PR_SET_NO_NEW_PRIVS est actif.

Les erreurs obtenues ressemblent à ceci :

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) est bloqué sans CAP_SYS_ADMIN sur l'hôte

Même si un environnement autorise CLONE_NEWUSER, le montage d'un nouveau /proc dans un espace de montage imbriqué échoue si le conteneur externe a masqué des sous-arborescences de /proc (/proc/acpi, /proc/kcore, /proc/keys) ou retiré mount(2) du filtre 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'absence de /dev/fuse empêche fuse-overlayfs de fonctionner

Les moteurs de conteneurs rootless qui ne peuvent pas monter le overlayfs du noyau basculent vers fuse-overlayfs. Dans les conteneurs minimalistes ou les Pods Kubernetes sans transmission du périphérique /dev/fuse, fuse-overlayfs échoue immédiatement :

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

Comment gvx exécute docker:dind entièrement dans l'espace utilisateur

gvx contourne les cinq barrières du noyau car il ne demande pas au noyau Linux de l'hôte de créer des espaces de noms, de monter des systèmes de fichiers ou de configurer des interfaces réseau. Il exécute plutôt un noyau Linux interne en espace utilisateur (le Sentry de gVisor) et une pile réseau en espace utilisateur (netstack) à l'intérieur d'un unique processus ordinaire sans privilèges.

Exécution de Docker-in-Docker sans privilèges avec gvx

1. Espaces de noms en mémoire et table de montage (VFS2)

Lorsque dockerd, containerd et runc s'exécutent dans rungv, systrap intercepte leurs appels système et les route vers le noyau Sentry en processus, plutôt que vers le noyau hôte :

  • unshare(2), clone(2), pivot_root(2), chroot(2) et mount(2) opèrent sur la hiérarchie de tâches en mémoire du Sentry et sur sa table de montage VFS2 (procfs, sysfs, tmpfs, devpts, cgroupfs v2), pendant que le processus hôte rungv s'exécute sous votre UID hôte non privilégié.

2. overlayfs natif sur des volumes ext4lite à fichier unique

Lorsque vous passez -v gvx-docker:/var/lib/docker (ou --mount type=volume,src=/disk/gvx-docker.ext4,dst=/var/lib/docker), gvx monte son système de fichiers intégré ext4lite sur /var/lib/docker.

Comme ext4lite implémente un système de fichiers ext4 journalisé JBD2 au sein d'un unique fichier hôte (gvx-docker.ext4), avec la prise en charge des attributs étendus trusted.overlay.*, des périphériques whiteout de caractères (0:0), de d_type et de RENAME_EXCHANGE, dockerd utilise son pilote de stockage natif overlayfs à l'intérieur de l'invité. Les opérations docker build multi-étapes partagent des couches Copy-on-Write sans /dev/fuse et sans l'explosion de l'espace disque propre au pilote de repli vfs de Docker.

3. Pont docker0 en espace utilisateur et iptables-legacy

Lorsque docker:dind démarre dans gvx :

  • Le dockerd invité crée le pont docker0 (172.17.0.1/16), attache des paires veth virtuelles pour les conteneurs imbriqués, et programme les chaînes NAT (DOCKER, POSTROUTING) et FILTER avec des correspondances addrtype et conntrack au sein de la netstack en espace utilisateur de gvx.
  • Le passage de -p unix:~/.gvx/docker.sock:2375 lie un socket de domaine Unix hôte (mode 0600) et transmet directement les connexions au port 2375 de dockerd à l'intérieur du bac à sable.

Comparaison avec les outils de conteneurs existants

Capacité / ContrainteDocker standard (dockerd)Docker rootless / PodmanKaniko / Buildah (sans privilèges)gvx (rungv + docker:dind)
root ou sudo de l'hôte requisOuiNonNonNon
Fonctionne avec PR_SET_NO_NEW_PRIVS=1NonNon (newuidmap échoue)PartielOui
Fonctionne lorsque unshare(CLONE_NEWUSER) est bloqué (EPERM)N/ANonBuildah : Non / Kaniko : OuiOui
Fonctionne sans /dev/fuseOui (utilise le overlay2 du noyau)Uniquement via la copie vfs lenteOuiOui (overlayfs sur ext4lite)
Prend en charge docker build (BuildKit)OuiOui (si userns autorisé)Construction d'images uniquement (sans runtime)Oui
Prend en charge docker run et docker compose upOuiOui (si userns autorisé)NonOui
S'exécute sous Windows et macOS sans VM LinuxNon (requiert WSL2 / VM)Non (requiert une VM Linux)NonOui (whpx, hvf, systrap)

Exigences exactes du noyau hôte sous Linux

gvx ne requiert aucune capacité élevée (CapEff: 0000000000000000) ni aucun user namespace. Sur un hôte Linux, rungv s'appuie uniquement sur trois primitives noyau standard non privilégiées :

  1. ptrace(PTRACE_ATTACH) sur son propre thread enfant, une seule fois lors de l'initialisation : systrap lance un thread auxiliaire local au démarrage, s'y attache pour initialiser le contexte de mémoire partagée, puis s'en détache. Si l'hôte définit kernel.yama.ptrace_scope = 3 (ce qui désactive ptrace à l'échelle du système) ou bloque ptrace dans un filtre seccomp externe, l'initialisation de systrap ne peut pas aboutir. (Avec yama.ptrace_scope = 1, valeur par défaut sur Ubuntu et Debian, gvx réussit car le traçage du parent vers l'enfant est autorisé.)
  2. seccomp(SECCOMP_SET_MODE_FILTER) : utilisé pour intercepter les appels système de l'invité depuis les threads stub du conteneur. Comme expliqué dans la partie 3 de cette série, gvx fonctionne aussi bien lorsque SECCOMP_FILTER_FLAG_NEW_LISTENER est disponible que lorsqu'il renvoie EBUSY à l'intérieur d'un bac à sable imbriqué.
  3. mmap(..., PROT_EXEC, ...) : utilisé pour mapper les segments ELF de l'invité et le trampoline de mémoire partagée de systrap.

Exemple de bout en bout : construire et tester une application multi-conteneurs sans root

Le script suivant exécute un docker build complet ainsi qu'un test d'intégration à deux conteneurs (redis + conteneur applicatif) dans une session utilisateur non privilégiée :

 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# Construire l'image avec overlayfs sur ext4lite
20docker build -t demo-ping:latest .
21
22# Créer un réseau bridge, démarrer Redis et vérifier la connectivité entre les conteneurs
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# Sortie : PONG
27
28# Supprimer les conteneurs de charge de travail et arrêter le bac à sable
29docker rm -f cache
30docker network rm demo-net
31kill -INT $DIND_PID

Pour aller plus loin dans cette série

  1. gvx : exécuter des conteneurs Linux, Docker et Kubernetes dans un seul processus sans privilèges : présentation architecturale de gvx, de ses moteurs d'exécution multiplateformes (whpx, hvf, systrap), de VFS2 (erofs, ext4lite, hostfs v2) et de netstack.
  2. Exécuter Docker, docker build et docker compose sans root ni user namespaces (cet article)
  3. Exécuter Docker-in-Docker dans gVisor et des bacs à sable imbriqués avec gvx : comment gvx résout modprobe, iptables-nft, overlay2 sur gofer, et SECCOMP_FILTER_FLAG_NEW_LISTENER imbriqué (EBUSY) dans gVisor (runsc).
  4. Environnements Docker isolés et rapides pour les rollouts RL d'agents IA avec gvx : mise à l'échelle d'environnements parallèles SWE-bench et de rollouts RL avec des réinitialisations de volumes .ext4 en copy-on-write de 100 millisecondes.