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

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
gvxest sûr par conception (et non un contournement d'élévation de privilèges) : Contrairement au montage du socket hôte/var/run/docker.sockou à l'exécution dedocker run --privileged(qui confèrent un contrôle équivalent à root sur le noyau hôte),gvxne 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,rungvverrouille son propre processus hôte (PR_SET_NO_NEW_PRIVS+ une liste blanche BPFseccompstricte) et exécutedockerdà l'intérieur du noyau en espace utilisateur gVisor (Sentry) de Google, conçu pour la sécurité mémoire. Toutes les capacitésUID 0de l'invité, les montagesoverlayfs(ext4lite) et les règlesiptablesrestent 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)ouclone(CLONE_NEWUSER). - Ubuntu 24.04+ active
kernel.apparmor_restrict_unprivileged_userns = 1. - Les nœuds Debian ou RHEL définissent
kernel.unprivileged_userns_clone = 0ouuser.max_user_namespaces = 0. newuidmapne peut pas écrire dans/proc/self/uid_mapparce quePR_SET_NO_NEW_PRIVSest 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.

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)etmount(2)opèrent sur la hiérarchie de tâches en mémoire du Sentry et sur sa table de montageVFS2(procfs,sysfs,tmpfs,devpts,cgroupfsv2), pendant que le processus hôterungvs'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
dockerdinvité crée le pontdocker0(172.17.0.1/16), attache des pairesvethvirtuelles pour les conteneurs imbriqués, et programme les chaînesNAT(DOCKER,POSTROUTING) etFILTERavec des correspondancesaddrtypeetconntrackau sein de lanetstacken espace utilisateur degvx. - Le passage de
-p unix:~/.gvx/docker.sock:2375lie un socket de domaine Unix hôte (mode0600) et transmet directement les connexions au port2375dedockerdà l'intérieur du bac à sable.
Comparaison avec les outils de conteneurs existants
| Capacité / Contrainte | Docker standard (dockerd) | Docker rootless / Podman | Kaniko / Buildah (sans privilèges) | gvx (rungv + docker:dind) |
|---|---|---|---|---|
root ou sudo de l'hôte requis | Oui | Non | Non | Non |
Fonctionne avec PR_SET_NO_NEW_PRIVS=1 | Non | Non (newuidmap échoue) | Partiel | Oui |
Fonctionne lorsque unshare(CLONE_NEWUSER) est bloqué (EPERM) | N/A | Non | Buildah : Non / Kaniko : Oui | Oui |
Fonctionne sans /dev/fuse | Oui (utilise le overlay2 du noyau) | Uniquement via la copie vfs lente | Oui | Oui (overlayfs sur ext4lite) |
Prend en charge docker build (BuildKit) | Oui | Oui (si userns autorisé) | Construction d'images uniquement (sans runtime) | Oui |
Prend en charge docker run et docker compose up | Oui | Oui (si userns autorisé) | Non | Oui |
| S'exécute sous Windows et macOS sans VM Linux | Non (requiert WSL2 / VM) | Non (requiert une VM Linux) | Non | Oui (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 :
ptrace(PTRACE_ATTACH)sur son propre thread enfant, une seule fois lors de l'initialisation :systraplance 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éfinitkernel.yama.ptrace_scope = 3(ce qui désactiveptraceà l'échelle du système) ou bloqueptracedans un filtre seccomp externe, l'initialisation desystrapne peut pas aboutir. (Avecyama.ptrace_scope = 1, valeur par défaut sur Ubuntu et Debian,gvxréussit car le traçage du parent vers l'enfant est autorisé.)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,gvxfonctionne aussi bien lorsqueSECCOMP_FILTER_FLAG_NEW_LISTENERest disponible que lorsqu'il renvoieEBUSYà l'intérieur d'un bac à sable imbriqué.mmap(..., PROT_EXEC, ...): utilisé pour mapper les segments ELF de l'invité et le trampoline de mémoire partagée desystrap.
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
- 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), deVFS2(erofs,ext4lite,hostfsv2) et denetstack. - Exécuter Docker, docker build et docker compose sans root ni user namespaces (cet article)
- Exécuter Docker-in-Docker dans gVisor et des bacs à sable imbriqués avec gvx : comment
gvxrésoutmodprobe,iptables-nft,overlay2surgofer, etSECCOMP_FILTER_FLAG_NEW_LISTENERimbriqué (EBUSY) dansgVisor(runsc). - Environnements Docker isolés et rapides pour les rollouts RL d'agents IA avec gvx : mise à l'échelle d'environnements parallèles
SWE-benchet de rollouts RL avec des réinitialisations de volumes.ext4en copy-on-write de 100 millisecondes.