---
id: 40e9d44f33fa847eb498f09bac5bf144
author: Lemon Mint
title: Exécuter Docker, docker build et docker compose sans root ni espaces de noms d'utilisateurs
description: Comment exécuter de manière sécurisée un Docker Engine complet (dockerd, BuildKit, overlayfs et docker compose) au sein d'un sandbox gVisor isolé en user-space, en tant qu'utilisateur non privilégié, lorsque root, sudo, unshare(CLONE_NEWUSER), mount(2) et /dev/fuse sont bloqués.
language: fr
date: 2026-11-06T12:02:56.050897958Z
path: /blog/posts/run-docker-without-root-or-userns-gvx-z48916d02
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: ops
---

## 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](https://github.com/gfire-sigs/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.

```sh
# 1. Installer rungv dans l'espace utilisateur (version préconstruite attestée SLSA ou go install)
# Option A : Vérifier et extraire le binaire préconstruit attesté SLSA depuis GitHub Releases
gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
  --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
  --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
tar -xzf rungv-v0.2.0-linux-amd64.tar.gz && install -m 0755 rungv-v0.2.0-linux-amd64/rungv ~/.local/bin/rungv

# Option B : Ou installer directement depuis les sources avec Go
CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest

# 2. Démarrer docker:dind dans gvx avec un volume ext4lite à fichier unique
#    Transmet ~/.gvx/docker.sock (mode 0600) au port invité 2375
mkdir -p ~/.gvx
~/.local/bin/rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -p 127.0.0.1:9000-9019:9000-9019 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &

# 3. Diriger le CLI Docker standard et Docker Compose vers le socket de l'espace utilisateur
export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done
docker info --format '{{.Driver}} {{.OperatingSystem}}'
# Sortie : overlayfs Alpine Linux ... (containerisé)

docker build -t app:test .
docker run --rm app:test
docker 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 :

```text
Cannot 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 :

```text
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
sudo: 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 :

```text
unshare: unshare failed: Operation not permitted
cannot clone: Operation not permitted
Error: cannot re-exec process to join the existing user namespace
newuidmap: write to uid_map failed: Operation not permitted
RootlessKit: 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 :

```text
mount("/proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL): Operation not permitted
OCI 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
bwrap: 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 :

```text
fuse: device not found, try 'modprobe fuse' first
fusermount3: failed to open /dev/fuse: No such file or directory
error 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](/assets/images/gvx/gvx-unprivileged-dind.png)

### 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é / 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 :

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](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03), `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 :

```sh
mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
DIND_PID=$!

export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done

mkdir -p /tmp/demo-app && cd /tmp/demo-app
cat <<'EOF' > Dockerfile
FROM alpine:3.20
RUN apk add --no-cache redis
CMD ["redis-cli", "-h", "cache", "ping"]
EOF

# Construire l'image avec overlayfs sur ext4lite
docker build -t demo-ping:latest .

# Créer un réseau bridge, démarrer Redis et vérifier la connectivité entre les conteneurs
docker network create demo-net
docker run -d --name cache --network demo-net redis:7-alpine
docker run --rm --network demo-net demo-ping:latest
# Sortie : PONG

# Supprimer les conteneurs de charge de travail et arrêter le bac à sable
docker rm -f cache
docker network rm demo-net
kill -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](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)** : 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](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)** (cet article)
3. **[Exécuter Docker-in-Docker dans gVisor et des bacs à sable imbriqués avec gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** : 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](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)** : 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.
