Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Par mis à jour le

Améliorer cette page

Avec le mode rootless, le démon Docker et les conteneurs tournent sous un compte ordinaire, sans aucun droit root sur l'hôte. Cette leçon vous apprend à installer un moteur rootless à côté du moteur habituel, à vérifier qu'il appartient bien au compte, puis à qualifier ce qu'il change : correspondance des UID, stockage, réseau, limites de ressources et persistance. Elle dit aussi ce que ce mode ne sait pas faire, pour décider s'il répond à votre besoin. Les mesures viennent d'une VM Debian 13 jetable avec Docker Engine 29.9.2 et RootlessKit 3.3.0 ; le test qui distingue modifier et supprimer vient d'un second passage sur la même VM et les mêmes versions. Ne rejouez pas l'installation sur un hôte partagé sans l'avoir comprise.

Chaque contrôle se lit sur l'état réel du système, pas sur la configuration annoncée.

  • Vous expliquez ce que le mode rootless change par rapport au groupe docker.
  • Vous vérifiez les prérequis : plage d'UID subordonnés (et comment en donner une à un compte qui n'en a pas), uidmap, cgroup v2 et paquet docker-ce-rootless-extras.
  • Vous installez un moteur rootless avec dockerd-rootless-setuptool.sh, sans toucher au moteur rootful.
  • Vous prouvez que les processus et le stockage appartiennent au compte.
  • Vous lisez la correspondance des UID et vous en déduisez ses conséquences sur les fichiers d'un volume.
  • Vous qualifiez le réseau (ports, adresse source, --network host), les limites de ressources et le démarrage sans session ouverte.
  • Vous désinstallez le mode rootless sans rien laisser derrière.

La leçon Interposer un proxy devant l'API Docker (C36b) précède celle-ci. Vous devez avoir installé Docker Engine par le dépôt officiel, comme dans la leçon Installer Docker Engine, savoir lire un ls -l et utiliser sudo.

La démonstration utilise deux rôles que le texte nomme toujours. Le moteur rootful est le moteur Docker habituel, lancé par root. Le compte c37-dev, créé pour l'occasion, joue le développeur qui veut lancer ses conteneurs sans droit root. Les commandes d'administration, qui demandent sudo, sont précédées du commentaire # administrateur. Celles du compte sont précédées de # c37-dev, en session SSH : ouvrez une vraie connexion SSH sous ce compte, car un sudo -u ne crée pas la session dont le mode rootless a besoin. Tous les conteneurs partent de la même image, épinglée à son digest, que vous définissez dans chaque session qui lance des conteneurs :

Fenêtre de terminal
ALP="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"

Qu'est-ce que le mode rootless change pour le moteur ?

Section intitulée « Qu'est-ce que le mode rootless change pour le moteur ? »

Le mode rootless fait tourner le démon, ses conteneurs et son socket sous un compte ordinaire, dans un user namespace. Un user namespace est un mécanisme du noyau qui donne à un groupe de processus sa propre vision des UID : le root de l'intérieur correspond, vu de l'hôte, à un UID sans privilège (la page Les namespaces Linux détaille le mécanisme).

La leçon Retirer les accès directs au moteur (C36) montre le problème de l'autre côté : un compte du groupe docker parle à un démon root, donc il est root. Le mode rootless est l'alternative pour un utilisateur qui doit vraiment lancer ses conteneurs : il ne reçoit pas l'accès à un démon puissant, il en démarre un à lui, qui n'a de pouvoir que celui du compte. Podman fonctionne ainsi par défaut, comme le décrit la page Les concepts de Podman.

Le schéma met face à face les deux moteurs sur le même hôte. Regardez les propriétaires, les chemins du socket et du stockage, l'espace réseau détaché où vivent seulement les conteneurs en réseau bridge, et la correspondance des UID en bas, que la section sur les UID mesure ensuite.

Deux moteurs coexistent sur un hôte. Le moteur rootful, lancé par root, expose /run/docker.sock et stocke ses données dans /var/lib/docker. Le moteur rootless, lancé par le compte c37-dev (UID 1002), expose /run/user/1002/docker.sock et stocke ses données dans ~/.local/share/docker ; RootlessKit crée son espace de noms utilisateur et un espace réseau détaché, où vivent seulement les conteneurs en réseau bridge : le démon reste dans l'espace réseau de l'hôte. Dans l'espace de noms utilisateur, l'UID 0 du conteneur correspond à l'UID 1002 de l'hôte, et les UID 1 à 65536 correspondent aux UID 231072 à 296607.Un même hôte, deux moteurs indépendantsMoteur rootful, lancé par rootMoteur rootless, lancé par c37-dev (UID 1002)Correspondance des UID dans l'espace de noms/run/docker.sockpropriétaire root, groupe dockerdockerd et containerdprocessus de root/var/lib/dockerimages et volumes de root/run/user/1002/docker.sockpropriétaire c37-devRootlessKitespace de noms utilisateurdockerd et containerdprocessus de c37-dev~/.local/share/dockerimages et volumes de c37-devEspace réseau détaché (--detach-netns)seuls les conteneurs en réseau bridge y viventle démon reste dans le réseau de l'hôteUID 0 du conteneurUID 1002 sur l'hôteUID 1 à 65536 du conteneurUID 231072 à 296607 sur l'hôtelancecréeapplique
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Ce moteur est entièrement séparé du premier : socket, processus, images, volumes et réseaux ne sont pas partagés. Ce qui s'y exécute ne voit pas les conteneurs du moteur rootful, et inversement.

Quels prérequis la machine et le compte doivent-ils remplir ?

Section intitulée « Quels prérequis la machine et le compte doivent-ils remplir ? »

Trois choses doivent exister avant l'installation : une plage d'UID subordonnés pour le compte, les deux programmes newuidmap et newgidmap du paquet uidmap, et des cgroups v2. Un UID subordonné est un UID que l'administrateur réserve à un compte, dans /etc/subuid (et /etc/subgid pour les groupes), pour qu'il les utilise dans son user namespace : sans plage, le compte n'a qu'un seul UID à y mettre, le sien. Docker rootless exige cette plage : la section de dépannage de sa documentation cite l'erreur No subuid ranges found pour un compte qui n'en a pas. Dans la démonstration ci-dessous, useradd attribue sa plage au compte de la leçon, et un compte système reçoit la sienne par usermod. Commencez par relever l'état de la machine : noyau, cgroups et paquets déjà installés.

Fenêtre de terminal
# administrateur
uname -r
stat -fc %T /sys/fs/cgroup
dpkg-query -W -f '${Package} ${Version}\n' docker-ce docker-ce-rootless-extras dbus-user-session
6.12.115+deb13.1-amd64
cgroup2fs
dbus-user-session 1.16.2-2
docker-ce 5:29.9.2-1~debian.13~trixie
docker-ce-rootless-extras 5:29.9.2-1~debian.13~trixie

cgroup2fs indique des cgroups v2. docker-ce-rootless-extras apporte dockerd-rootless.sh, dockerd-rootless-setuptool.sh et rootlesskit (RootlessKit, le programme qui crée l'espace de noms et son réseau) ; la leçon d'installation l'a déjà épinglé avec docker-ce, à la même version 29.9.2. dbus-user-session donne au gestionnaire de services du compte son bus de session. La documentation de Docker le cite dans son dépannage : sans ce bus, les conteneurs échouent au démarrage.

Sous Debian 13, useradd attribue la plage tout seul à un compte normal (pas à un compte système créé avec -r), grâce aux réglages SUB_UID_COUNT et SUB_GID_COUNT de /etc/login.defs, tant que /etc/subuid existe :

Fenêtre de terminal
# administrateur
grep -E '^(SUB_UID|SUB_GID)_(MIN|MAX|COUNT)' /etc/login.defs
SUB_UID_MIN 100000
SUB_UID_MAX 600100000
SUB_UID_COUNT 65536
SUB_GID_MIN 100000
SUB_GID_MAX 600100000
SUB_GID_COUNT 65536

Chaque nouveau compte reçoit donc 65 536 UID subordonnés, pris entre SUB_UID_MIN et SUB_UID_MAX. La documentation officielle demande une plage d'au moins 65 536 : c'est une recommandation, mais une plage plus courte fait échouer le téléchargement d'une image dont les fichiers appartiennent à des UID plus hauts, avec une erreur lchown … invalid argument. Créez le compte de travail, sans le mettre dans le groupe docker, puis relevez son UID et ses plages :

Fenêtre de terminal
# administrateur
sudo useradd -m -s /bin/bash c37-dev
id c37-dev
grep '^c37-dev:' /etc/subuid /etc/subgid
uid=1002(c37-dev) gid=1002(c37-dev) groups=1002(c37-dev)
/etc/subuid:c37-dev:231072:65536
/etc/subgid:c37-dev:231072:65536

Le compte a l'UID 1002 et les UID 231072 à 296607 (65 536 valeurs) lui sont réservés. La plage de départ, 231072, est propre à cette VM : elle dépend des comptes déjà créés, ce n'est pas une valeur par défaut. Ces chiffres reviennent dans la correspondance des UID, plus bas.

Un compte système, lui, n'a pas de plage. Pour en donner une, relevez d'abord les plages déjà prises dans /etc/subuid : deux comptes dont les plages se chevauchent se partageraient des UID. Ici, les trois lignes existantes s'arrêtent à 296607, et la plage 300000-365535 ne chevauche aucune d'elles. La valeur 300000 est propre à cette VM : calculez la vôtre à partir de votre fichier.

Fenêtre de terminal
# administrateur
sudo useradd -r -m -d /var/lib/c37-svc -s /usr/sbin/nologin c37-svc
grep -c '^c37-svc:' /etc/subuid || true
cat /etc/subuid
sudo usermod --add-subuids 300000-365535 --add-subgids 300000-365535 c37-svc; echo "usermod_rc=$?"
grep '^c37-svc:' /etc/subuid /etc/subgid
sudo userdel -r c37-svc; echo "userdel_svc_rc=$?"
grep -c '^c37-svc:' /etc/subuid || true
0
student:100000:65536
ansible:165536:65536
c37-dev:231072:65536
usermod_rc=0
/etc/subuid:c37-svc:300000:65536
/etc/subgid:c37-svc:300000:65536
userdel: c37-svc mail spool (/var/mail/c37-svc) not found
userdel_svc_rc=0
0

Le premier 0 confirme que le compte système n'a aucune ligne dans /etc/subuid. usermod --add-subuids … --add-subgids … lui en attribue une, dans les deux fichiers, et userdel -r la retire avec le compte (le dernier 0). L'avertissement sur la boîte aux lettres est sans conséquence. Le compte de la leçon, c37-dev, n'a pas besoin de cette étape : useradd lui a déjà donné sa plage.

Pour ouvrir la session SSH du compte, l'administrateur lui copie ici sa propre clé autorisée ; adaptez ce geste à votre façon d'accéder à la machine :

Fenêtre de terminal
# administrateur
sudo install -d -m 0700 -o c37-dev -g c37-dev /home/c37-dev/.ssh
sudo install -m 0600 -o c37-dev -g c37-dev "$HOME/.ssh/authorized_keys" /home/c37-dev/.ssh/authorized_keys; echo "cle_rc=$?"
cle_rc=0

Dans la session du compte, trois lectures donnent l'état de départ : qui il est, le répertoire d'exécution de sa session, et ce que lui répond le moteur rootful. L'outil d'installation vérifie ensuite ses prérequis avec le sous-programme check :

Fenêtre de terminal
# c37-dev, en session SSH
id
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
docker version --format '{{.Server.Version}}' 2>&1 | tail -1
dockerd-rootless-setuptool.sh check 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
uid=1002(c37-dev) gid=1002(c37-dev) groups=1002(c37-dev)
XDG_RUNTIME_DIR=/run/user/1002
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
[ERROR] Missing system requirements. Run the following commands to
[ERROR] install the requirements and run this tool again.
########## BEGIN ##########
sudo sh -eux <<EOF
# Install newuidmap & newgidmap binaries
apt-get install -y uidmap
EOF
########## END ##########

XDG_RUNTIME_DIR=/run/user/1002 est le répertoire que crée une vraie session, et le socket du moteur rootless y sera placé. Le compte, hors du groupe docker, se voit refuser le socket du moteur rootful : c'est voulu. Quant à check, il refuse parce que uidmap manque, et recopie la commande à lancer, que seul un administrateur peut taper : le compte ne peut pas installer lui-même uidmap, dont les deux programmes sont setuid root pour écrire la correspondance des UID. Le sed retire les codes de couleur de la sortie.

Comment installer un moteur rootless à côté du moteur rootful ?

Section intitulée « Comment installer un moteur rootless à côté du moteur rootful ? »

L'installation se fait en deux temps : l'administrateur prépare la machine, puis le compte lance dockerd-rootless-setuptool.sh install. L'administrateur installe uidmap, puis pose le lingering du compte, avant l'installation. Le lingering est un réglage de systemd-logind qui garde le gestionnaire de services d'un compte en vie même quand personne n'est connecté. Sans lui, ce gestionnaire s'arrête avec la dernière session du compte et emporte le démon rootless : le poser d'abord donne un démon stable pendant toute la leçon, avec les mêmes PID. La section sur la persistance mesure ce qui se passe sans lui.

Fenêtre de terminal
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq uidmap >/dev/null 2>&1; echo "apt_rc=$?"
dpkg-query -W -f '${Package} ${Version}\n' uidmap
ls -l /usr/bin/newuidmap /usr/bin/newgidmap
sudo loginctl enable-linger c37-dev; echo "linger_rc=$?"
loginctl show-user c37-dev -p Linger
apt_rc=0
uidmap 1:4.17.4-2
-rwsr-xr-x 1 root root 63584 Apr 19 2025 /usr/bin/newgidmap
-rwsr-xr-x 1 root root 59488 Apr 19 2025 /usr/bin/newuidmap
linger_rc=0
Linger=yes

La sortie d'apt-get est redirigée vers /dev/null pour rester courte, et DEBIAN_FRONTEND=noninteractive évite toute question. Le s du mode -rwsr-xr-x signale le bit setuid : ces deux programmes s'exécutent avec les droits de root, ce qui leur permet d'écrire dans un user namespace des UID que le compte ne possède pas. Linger=yes confirme le lingering.

Le compte relance alors check, puis install. L'outil ne demande pas --force ici : il s'interrompt quand le socket du moteur rootful est accessible en écriture au compte, et le compte, hors groupe docker, n'y a pas accès. L'option --force existe aussi pour passer outre un échec de RootlessKit : ne l'ajoutez pas par réflexe, elle masque des erreurs. Les codes de retour se lisent avec PIPESTATUS[0], qui donne celui de l'outil et non celui de sed. Les extraits ci-dessous ne gardent que les lignes utiles de l'installation :

Fenêtre de terminal
# c37-dev, en session SSH
dockerd-rootless-setuptool.sh check 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; echo "check_rc=${PIPESTATUS[0]}"
dockerd-rootless-setuptool.sh install 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; echo "install_rc=${PIPESTATUS[0]}"
[INFO] Requirements are satisfied
check_rc=0
[INFO] Creating /home/c37-dev/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
[...]
Active: active (running) since Wed 2026-11-04 20:21:58 UTC; 3s ago
[...]
├─5090 rootlesskit --state-dir=/run/user/1002/dockerd-rootless --net=gvisor-tap-vsock --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave --detach-netns /usr/bin/dockerd-rootless.sh
[...]
rootlesskit:
Version: 3.3.0
ApiVersion: 1.1.3
NetworkDriver: gvisor-tap-vsock
PortDriver: builtin
StateDir: /run/user/1002/dockerd-rootless
[...]
Nov 04 20:21:58 c45-debian.lab dockerd-rootless.sh[5132]: time="2026-11-04T20:21:58.830671352Z" level=info msg="API listen on /run/user/1002/docker.sock"
[...]
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger c37-dev`
[...]
[INFO] Creating CLI context "rootless"
Successfully created context "rootless"
[INFO] Using CLI context "rootless"
Current context is now "rootless"
[...]
[INFO] Some applications may require the following environment variable too:
export DOCKER_HOST=unix:///run/user/1002/docker.sock
[...]
install_rc=0

Cinq faits se lisent ici. L'outil a créé une unité systemd utilisateur (~/.config/systemd/user/docker.service), c'est-à-dire un service géré par le systemd du compte et non par celui du système. Cette unité lance rootlesskit (PID 5090), qui lance à son tour le démon. Le pilote réseau est gvisor-tap-vsock, avec le pilote de ports builtin. L'API écoute sur /run/user/1002/docker.sock, un chemin dans /run/user/<uid>. Enfin, l'outil a créé un contexte Docker nommé rootless et l'a activé, ce qui évite d'exporter DOCKER_HOST à la main pour la ligne de commande docker (l'outil rappelle que certaines applications demandent cette variable). La dernière ligne [INFO] rappelle le lingering : il est déjà posé.

L'état après installation se lit en une commande par objet :

Fenêtre de terminal
# c37-dev, en session SSH
ls ~/.config/systemd/user/
systemctl --user is-active docker.service
systemctl --user is-enabled docker.service
docker context ls
default.target.wants
docker.service
active
enabled
NAME DESCRIPTION DOCKER ENDPOINT ERROR
default Current DOCKER_HOST based configuration unix:///var/run/docker.sock
rootless * Rootless mode unix:///run/user/1002/docker.sock

Les deux moteurs ont chacun leur contexte : default vise toujours le socket rootful, rootless (marqué par l'étoile) vise le socket du compte. Le moteur rootful n'a pas été arrêté ni modifié.

Comment vérifier que le démon et ses processus sont bien ceux du compte ?

Section intitulée « Comment vérifier que le démon et ses processus sont bien ceux du compte ? »

La preuve du mode rootless est triple : le démon se déclare rootless, ses processus appartiennent au compte, et son stockage est distinct. Le démon déclare son mode dans SecurityOptions, avec l'entrée name=rootless, et il donne son répertoire de données et son pilote de stockage :

Fenêtre de terminal
# c37-dev, en session SSH
docker info --format 'Context={{.ClientInfo.Context}} SecurityOptions={{json .SecurityOptions}} DockerRootDir={{.DockerRootDir}} Driver={{.Driver}} CgroupDriver={{.CgroupDriver}} CgroupVersion={{.CgroupVersion}}'
ps -o user=,pid=,comm= -C rootlesskit,dockerd,containerd
Context=rootless SecurityOptions=["name=seccomp,profile=builtin","name=rootless","name=cgroupns"] DockerRootDir=/home/c37-dev/.local/share/docker Driver=overlayfs CgroupDriver=systemd CgroupVersion=2
root 706 containerd
root 1669 dockerd
c37-dev 5090 rootlesskit
c37-dev 5132 dockerd
c37-dev 5154 containerd

Regardez trois éléments. name=rootless dans SecurityOptions est l'annonce du démon. DockerRootDir pointe dans le répertoire personnel du compte : ~/.local/share/docker, non /var/lib/docker. Et ps liste deux jeux de processus : containerd et dockerd de root (le moteur rootful, toujours là), et rootlesskit, dockerd et containerd du compte c37-dev. Le pilote de stockage est overlayfs, le snapshotter de containerd, que le démon choisit sans configuration.

Le rootlesskit de ps porte le PID 5090, le même que dans la sortie de l'installation, et le démon (5132) et containerd (5154) aussi : le lingering posé avant l'installation garde le gestionnaire du compte, donc le démon, en vie entre les sessions.

Le magasin d'images est lui aussi séparé. Le compte part de zéro, télécharge l'image, puis mesure ce qu'elle occupe chez lui :

Fenêtre de terminal
# c37-dev, en session SSH
docker image ls -q | wc -l
docker pull -q "$ALP"
docker image ls --format '{{.Repository}} {{.ID}}'
du -sh ~/.local/share/docker
0
docker.io/library/alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
alpine be2656061b53
13M /home/c37-dev/.local/share/docker

Le moteur du compte ne contenait aucune image au départ (0). Après le téléchargement, 13 Mo occupent ~/.local/share/docker. L'administrateur regarde maintenant le magasin du moteur rootful :

Fenêtre de terminal
# administrateur
sudo docker image ls --format '{{.Repository}} {{.ID}}'
echo "images_rootful=$(sudo docker image ls -q | wc -l)"
alpine be2656061b53
images_rootful=1

Si votre moteur système a déjà cette image, comme ici, le magasin la liste avec le même identifiant, parce que c'est le même digest. Le compte, lui, est parti de 0 image et a dû la télécharger : chaque moteur garde sa propre copie. Corollaire pratique : chaque compte rootless retélécharge ses images, et ses données comptent dans son répertoire personnel.

Comment les UID du conteneur se traduisent-ils sur l'hôte ?

Section intitulée « Comment les UID du conteneur se traduisent-ils sur l'hôte ? »

Dans un conteneur rootless, root n'est pas root de l'hôte : c'est le compte lui-même. La table de correspondance /proc/self/uid_map l'écrit en trois colonnes : le premier UID dans le conteneur, le premier UID correspondant sur l'hôte, et la longueur de la plage. Lancez deux conteneurs, l'un en root par défaut, l'autre avec --user 1000:1000, et regardez-les depuis l'hôte :

Fenêtre de terminal
# c37-dev, en session SSH
docker run -d --name c37-m-uid --network none "$ALP" sleep 300 >/dev/null
docker exec c37-m-uid sh -c 'id -u; cat /proc/self/uid_map'
ps -o user=,uid=,pid=,args= -p "$(docker inspect -f '{{.State.Pid}}' c37-m-uid)"
docker run -d --name c37-m-uid1000 --network none --user 1000:1000 "$ALP" sleep 300 >/dev/null
docker exec c37-m-uid1000 id -u
ps -o uid=,pid=,args= -p "$(docker inspect -f '{{.State.Pid}}' c37-m-uid1000)"
docker rm -f c37-m-uid c37-m-uid1000 >/dev/null
0
0 1002 1
1 231072 65536
c37-dev 1002 5512 sleep 300
1000
232071 5611 sleep 300

La sortie se lit dans l'ordre des commandes. 0 est l'UID du premier conteneur, suivi de sa table. La première ligne de la table, 0 1002 1, dit que l'UID 0 du conteneur est l'UID 1002 de l'hôte : le root du conteneur est le compte c37-dev, et ps le confirme (c37-dev 1002). La seconde, 1 231072 65536, place les UID 1 à 65536 du conteneur sur les UID subordonnés 231072 à 296607. Le second conteneur, 1000 à l'intérieur, a donc, vu de l'hôte, l'UID 232071, soit 231072 + (1000 - 1). C'est le schéma de la section précédente, mesuré.

La conséquence concrète touche les fichiers d'un volume. Un fichier créé par root dans le conteneur appartient au compte sur l'hôte. Un fichier créé pour l'UID 1000 appartient à l'UID 232071, qu'aucun compte réel ne porte :

Fenêtre de terminal
# c37-dev, en session SSH
docker volume create c37-m-donnees >/dev/null
docker run --rm --network none -v c37-m-donnees:/d "$ALP" sh -c 'echo racine > /d/de-root; touch /d/de-1000; chown 1000:1000 /d/de-1000; ls -ln /d'
ls -ln ~/.local/share/docker/volumes/c37-m-donnees/_data/
echo "racine-modifiee" > ~/.local/share/docker/volumes/c37-m-donnees/_data/de-root; echo "ecriture_hote_rc=$?"
touch ~/.local/share/docker/volumes/c37-m-donnees/_data/de-1000; echo "touch_1000_rc=$?"
total 4
-rw-r--r-- 1 1000 1000 0 Nov 4 20:22 de-1000
-rw-r--r-- 1 0 0 7 Nov 4 20:22 de-root
total 4
-rw-r--r-- 1 232071 232071 0 Nov 4 20:22 de-1000
-rw-r--r-- 1 1002 1002 7 Nov 4 20:22 de-root
ecriture_hote_rc=0
touch: cannot touch '/home/c37-dev/.local/share/docker/volumes/c37-m-donnees/_data/de-1000': Permission denied
touch_1000_rc=1

Les deux premiers listings montrent le même volume vu de deux côtés : 0 et 1000 dans le conteneur, 1002 et 232071 sur l'hôte. Le compte modifie sans difficulté le fichier du conteneur-root (ecriture_hote_rc=0), mais la commande touch sur le fichier de l'UID 1000 est refusée (touch_1000_rc=1), parce que l'hôte voit un autre propriétaire. Un bind mount d'un répertoire de votre dépôt demande donc la même vigilance : un conteneur qui écrit en tant qu'un UID autre que root dépose des fichiers que le compte ne pourra plus modifier depuis l'hôte.

Modifier et supprimer ne suivent pas la même règle. Supprimer une entrée dépend des droits sur le répertoire qui la contient, non sur le fichier lui-même. Le test suivant crée, depuis un conteneur, un fichier et un répertoire de l'UID 1000 dans un volume neuf, puis les modifie et les supprime depuis l'hôte :

Fenêtre de terminal
# c37-dev, en session SSH
docker volume create c37-m-droits >/dev/null
docker run --rm --network none -v c37-m-droits:/d "$ALP" sh -c 'touch /d/fichier-1000; mkdir /d/rep-1000; touch /d/rep-1000/dedans; chown -R 1000:1000 /d/fichier-1000 /d/rep-1000'
V=~/.local/share/docker/volumes/c37-m-droits/_data
ls -ln "$V" "$V/rep-1000"
echo "=== modifier"
touch "$V/fichier-1000"; echo "touch_rc=$?"
echo "=== supprimer un fichier de l'UID 1000 dans un répertoire du compte"
rm -f "$V/fichier-1000"; echo "rm_fichier_rc=$?"
echo "=== vider un répertoire de l'UID 1000"
rm -rf "$V/rep-1000"; echo "rm_rep_rc=$?"
ls -ln "$V"
echo "=== nettoyage"
docker volume rm c37-m-droits
/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data:
total 4
-rw-r--r-- 1 232071 232071 0 Nov 4 20:47 fichier-1000
drwxr-xr-x 2 232071 232071 4096 Nov 4 20:47 rep-1000
/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data/rep-1000:
total 0
-rw-r--r-- 1 232071 232071 0 Nov 4 20:47 dedans
=== modifier
touch_rc=1
=== supprimer un fichier de l'UID 1000 dans un répertoire du compte
touch: cannot touch '/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data/fichier-1000': Permission denied
rm_fichier_rc=0
=== vider un répertoire de l'UID 1000
rm: cannot remove '/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data/rep-1000/dedans': Permission denied
rm_rep_rc=1
total 4
drwxr-xr-x 2 232071 232071 4096 Nov 4 20:47 rep-1000
=== nettoyage
c37-m-droits

Dans cette capture, le message de touch s'affiche sous la ligne « supprimer » : la sortie et les erreurs y ont voyagé par deux canaux distincts. Dans votre terminal, l'erreur apparaîtra juste après la commande qui l'a produite. Suivez les codes de retour. Le compte ne modifie pas le fichier de l'UID 1000 (touch_rc=1), pourtant il le supprime (rm_fichier_rc=0) : le répertoire parent _data lui appartient, et c'est ce droit qui compte pour retirer une entrée. Il ne peut pas vider rep-1000 (rm_rep_rc=1, Permission denied) : ce répertoire appartient à l'UID 232071, le compte n'y a aucun droit d'écriture, donc il ne peut pas en retirer dedans, et rep-1000 reste visible dans le dernier ls. Pour effacer ce type de données, passez par le démon tant qu'il tourne (docker volume rm), ou par rootlesskit après la désinstallation, comme le propose uninstall.

Le mode rootless change trois choses pour le réseau : le service ne reçoit plus l'adresse source des clients par défaut, un port inférieur à 1024 ne se publie pas, et --network host expose le service sur le réseau réel de la machine. Un port élevé, lui, se publie normalement. Le service de l'exemple est un petit serveur nc qui répond une ligne et affiche l'adresse de ses clients ; HOTE reçoit la première adresse IPv4 globale de la machine :

Fenêtre de terminal
# c37-dev, en session SSH
HOTE=$(ip -4 -o addr show scope global | awk '{print $4}' | cut -d/ -f1 | head -1)
SRV='while true; do printf "HTTP/1.0 200 OK\r\n\r\npage-c37\n" | nc -l -n -v -p 8080 2>&1 | grep -i connect; done'
echo "HOTE=$HOTE"
docker run -d --name c37-m-web -p 127.0.0.1:8088:8080 "$ALP" sh -c "$SRV" >/dev/null
sleep 1
docker port c37-m-web
curl -s http://127.0.0.1:8088/; echo "curl_boucle_rc=$?"
curl -s -m 3 "http://$HOTE:8088/"; echo "curl_ip_hote_rc=$?"
sleep 1
docker logs c37-m-web 2>&1 | tail -1
docker rm -f c37-m-web >/dev/null
HOTE=10.10.130.11
8080/tcp -> 127.0.0.1:8088
page-c37
curl_boucle_rc=0
curl_ip_hote_rc=7
connect to [::ffff:172.17.0.2]:8080 from [::ffff:172.17.0.1]:57558 ([::ffff:172.17.0.1]:57558)

La publication sur 127.0.0.1:8088 répond (curl_boucle_rc=0), et l'adresse de la VM, qui n'est pas dans la publication, ne répond pas (curl_ip_hote_rc=7). La ligne de journal montre une limite : le service (172.17.0.2) reçoit la connexion depuis 172.17.0.1, une adresse interne au réseau des conteneurs, et non depuis 127.0.0.1, l'adresse réelle du client. Avec la configuration par défaut (pilote de ports builtin), l'adresse source réelle n'est pas transmise, ce qui gêne un service qui journalise ou filtre par adresse.

La documentation de Docker annonce que RootlessKit 3 transmet l'adresse source en TCP, à condition de désactiver le userland-proxy. La parade jouée dans cette leçon est le pilote de ports slirp4netns, présenté plus bas.

Le pont rootless prend 172.17.0.0/16, comme le moteur rootful, sans conflit d'adresses. L'explication probable : le pont du compte vit dans l'espace de noms réseau de RootlessKit, distinct de celui de l'hôte où se trouve le pont du moteur rootful.

Passons au port 80. Un compte ordinaire ne peut pas écouter sur un port inférieur à 1024, et il en va de même pour RootlessKit. Publier le port 80 échoue donc :

Fenêtre de terminal
# c37-dev, en session SSH
docker run -d --name c37-m-web80 -p 80:8080 "$ALP" sleep 30; echo "run80_rc=$?"
docker rm -f c37-m-web80 >/dev/null
40226111cd6c425dfc6d9e514c6ae9439cf6b4f361124a55978580c9d9858d7f
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint c37-m-web80 (398ba7ab05c922a3581a9647ba99db1e738e3d8473793fe17fedfa83f466b200): error while calling RootlessKit PortManager.AddPort(): cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or set CAP_NET_BIND_SERVICE on rootlesskit binary, or choose a larger port number (>= 1024): listen tcp4 0.0.0.0:80: bind: permission denied
Run 'docker run --help' for more information
run80_rc=126

Le conteneur a été créé (son identifiant s'affiche) mais n'a pas démarré, d'où le docker rm -f. Le code de retour est 126, et le message cite sa cause, cannot expose privileged port 80, puis trois sorties qu'il propose : abaisser net.ipv4.ip_unprivileged_port_start dans /etc/sysctl.conf (valeur actuelle 1024), donner la capacité CAP_NET_BIND_SERVICE au binaire rootlesskit, ou choisir un port supérieur ou égal à 1024. La première est un réglage du noyau valable pour toute la machine. La deuxième est posée sur le fichier : elle vaut pour tout utilisateur qui lance ce binaire, pas seulement pour votre compte. La documentation de ce mode les formule ainsi : sudo setcap cap_net_bind_service=ep $(which rootlesskit) puis un redémarrage du démon, ou net.ipv4.ip_unprivileged_port_start dans /etc/sysctl.d puis sudo sysctl --system. Ces deux réglages modifient la machine entière ou un binaire partagé : ils sortent du périmètre de la démonstration, qui reste sur un compte isolé. Le plus simple reste de publier un port élevé, derrière un reverse proxy ou une redirection placés par l'administrateur si le service doit être joint sur le port 80.

Sur cette VM, la sortie vers l'extérieur fonctionne, ping compris :

Fenêtre de terminal
# c37-dev, en session SSH
docker run --rm "$ALP" sh -c 'wget -q -T 5 -O /dev/null http://deb.debian.org/debian/ && echo http_sortant=ok || echo http_sortant=echec; ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 && echo ping=ok || echo ping=echec'
http_sortant=ok
ping=ok

Le ping a répondu sur cette Debian 13. La documentation de Docker précise que son fonctionnement dépend du paramètre noyau net.ipv4.ping_group_range, dont la valeur varie selon la distribution : sur une autre, vérifiez-le.

L'option --network host demande une attention particulière. La ligne de commande de RootlessKit vue à l'installation porte --detach-netns : le démon reste dans l'espace réseau de l'hôte, et seuls les conteneurs en réseau bridge vivent dans l'espace réseau détaché de RootlessKit (leur adresse IP n'est donc pas joignable depuis l'hôte). Un conteneur lancé avec --network host voit ainsi les interfaces réelles de l'hôte, et un service qui y écoute est joignable depuis l'hôte :

Fenêtre de terminal
# c37-dev, en session SSH
docker run --rm --network host "$ALP" sh -c 'ip -4 -o addr show | awk "{print \$2, \$4}"'
docker run -d --name c37-m-hote --network host "$ALP" sh -c "$SRV" >/dev/null
sleep 1
curl -s -m 3 http://127.0.0.1:8080/; echo "curl_hote_8080_rc=$?"
ss -ltn 'sport = :8080' | tail -n +2
docker rm -f c37-m-hote >/dev/null
ss -ltn 'sport = :8080' | tail -n +2 | wc -l
lo 127.0.0.1/8
enp1s0 10.10.130.11/24
docker0 172.17.0.1/16
page-c37
curl_hote_8080_rc=0
LISTEN 0 1 *:8080 *:*
0

Le conteneur voit lo, enp1s0 (l'interface qui porte l'adresse 10.10.130.11 que HOTE a relevée) et docker0, le pont du moteur rootful : ce sont les interfaces de l'hôte. Le service répond depuis l'hôte (curl_hote_8080_rc=0), et le port 8080 écoute sur *, toutes les adresses de la machine. Le dernier 0 montre que le port est libéré une fois le conteneur retiré. C'est une limite de sécurité : le mode rootless ne cloisonne pas le réseau d'un conteneur lancé avec --network host, qui expose son service au réseau de la machine. Réservez-le à un besoin identifié, et préférez la publication de ports précis.

Le pilote réseau vu à l'installation, gvisor-tap-vsock, n'est pas imposé : d'après le script dockerd-rootless.sh, il choisit slirp4netns s'il est installé, sinon pasta, sinon vpnkit, et se rabat sur gvisor-tap-vsock. Ce pilote est embarqué dans RootlessKit, donc aucun binaire de plus à installer, mais RootlessKit le qualifie d'expérimental : à ne pas présenter comme un pilote éprouvé pour une production. La parade ci-dessous change justement de pilote.

La documentation Docker propose, dans sa section de dépannage « docker run -p does not propagate source IP addresses », une parade pour garder l'adresse source : remplacer le pilote de ports par slirp4netns. Cela demande l'administrateur, qui installe le paquet Debian (le moteur n'embarque pas slirp4netns), puis le compte :

Fenêtre de terminal
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq slirp4netns >/dev/null 2>&1; echo "apt_rc=$?"
dpkg-query -W -f '${Package} ${Version}\n' slirp4netns
apt_rc=0
slirp4netns 1.2.1-1.1

Le compte redémarre son démon, puis compare les pilotes avant et après avec la fonction pilotes. Le paquet seul suffit à changer le réseau, mais pas le pilote de ports :

Fenêtre de terminal
# c37-dev, en session SSH
pilotes() { docker version --format '{{range .Server.Components}}{{if eq .Name "rootlesskit"}}{{.Details.NetworkDriver}} {{.Details.PortDriver}}{{end}}{{end}}'; }
pilotes
systemctl --user restart docker.service; echo "restart_rc=$?"
pilotes
gvisor-tap-vsock builtin
restart_rc=0
slirp4netns builtin

Le premier appel montre le réseau par défaut, gvisor-tap-vsock builtin. Après le redémarrage, le réseau est slirp4netns, mais le pilote de ports reste builtin. Pour changer aussi ce dernier, le compte écrit une surcharge de l'unité utilisateur, recharge systemd et redémarre le démon, puis rappelle le service avec le même programme nc. ALP, HOTE et SRV sont ceux définis plus haut : si vous avez ouvert une nouvelle session, définissez-les à nouveau.

Fenêtre de terminal
# c37-dev, en session SSH
mkdir -p ~/.config/systemd/user/docker.service.d
printf '[Service]\nEnvironment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns"\nEnvironment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"\n' > ~/.config/systemd/user/docker.service.d/override.conf
cat ~/.config/systemd/user/docker.service.d/override.conf
systemctl --user daemon-reload
systemctl --user restart docker.service; echo "restart_rc=$?"
pilotes
docker run -d --name c37-m-web -p 8088:8080 "$ALP" sh -c "$SRV" >/dev/null
sleep 1
curl -s http://127.0.0.1:8088/; echo "curl_boucle_rc=$?"
sleep 1
docker logs c37-m-web 2>&1 | tail -1
curl -s -m 3 "http://$HOTE:8088/"; echo "curl_ip_hote_rc=$?"
sleep 1
docker logs c37-m-web 2>&1 | tail -1
docker rm -f c37-m-web >/dev/null
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"
restart_rc=0
slirp4netns slirp4netns
page-c37
curl_boucle_rc=0
connect to [::ffff:172.17.0.2]:8080 from [::ffff:10.0.2.2]:58892 ([::ffff:10.0.2.2]:58892)
page-c37
curl_ip_hote_rc=0
connect to [::ffff:172.17.0.2]:8080 from [::ffff:10.10.130.11]:35134 ([::ffff:10.10.130.11]:35134)

Après la surcharge, pilotes répond slirp4netns slirp4netns. Les deux journaux se lisent ainsi. Un appel sur 127.0.0.1 est vu depuis 10.0.2.2, la passerelle de slirp4netns. Un appel sur l'adresse de la machine est vu depuis 10.10.130.11 : l'adresse du client est conservée. Deux réserves : le client était l'hôte lui-même, faute d'une autre machine, et la documentation classe ce pilote de ports comme lent (« Slow »). Choisissez-le donc quand le service a besoin de l'adresse source, pas par défaut.

Le retour en arrière se fait en trois gestes : le compte retire la surcharge, l'administrateur purge le paquet, puis le compte redémarre son démon.

Fenêtre de terminal
# c37-dev, en session SSH
find ~/.config/systemd/user/docker.service.d -name override.conf -delete
systemctl --user daemon-reload
Fenêtre de terminal
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq slirp4netns >/dev/null 2>&1; echo "purge_rc=$?"
dpkg-query -W slirp4netns 2>&1
purge_rc=0
dpkg-query: no packages found matching slirp4netns

Le paquet est retiré : dpkg-query ne le trouve plus. Reste à relancer le démon du compte, qui revient au réseau par défaut :

Fenêtre de terminal
# c37-dev, en session SSH
systemctl --user restart docker.service; echo "restart_rc=$?"
docker version --format '{{range .Server.Components}}{{if eq .Name "rootlesskit"}}{{.Details.NetworkDriver}} {{.Details.PortDriver}}{{end}}{{end}}'
restart_rc=0
gvisor-tap-vsock builtin

Le démon est revenu à gvisor-tap-vsock builtin : la suite de la leçon se déroule sur le réglage d'origine.

Quelles limites de ressources le compte peut-il imposer ?

Section intitulée « Quelles limites de ressources le compte peut-il imposer ? »

Le compte ne peut limiter que les ressources dont systemd lui délègue le contrôleur de cgroup. Sur Debian 13 (systemd 257), le gestionnaire du compte reçoit par défaut cpu, memory et pids, pas io ni cpuset. La documentation officielle du mode rootless dit que seuls memory et pids sont typiquement délégués : sur cette Debian, cpu l'est aussi, la liste dépend donc de la machine. Pour déléguer plus (io, cpuset), la documentation décrit une surcharge de user@.service avec Delegate=. Pour le vérifier ici, lancez des conteneurs avec ces trois limites et relisez le cgroup depuis l'intérieur, puis lisez la liste des contrôleurs délégués :

Fenêtre de terminal
# c37-dev, en session SSH
docker run --rm --memory 64m --pids-limit 20 "$ALP" sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/pids.max'; echo "mem_pids_rc=$?"
docker run --rm --cpus 0.5 "$ALP" cat /sys/fs/cgroup/cpu.max; echo "cpus_rc=$?"
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
67108864
20
mem_pids_rc=0
50000 100000
cpus_rc=0
cpu memory pids

--memory 64m donne memory.max à 67 108 864 octets, --pids-limit 20 donne pids.max à 20, et --cpus 0.5 donne cpu.max à 50000 100000, soit la moitié d'un cœur. La dernière ligne explique pourquoi : seuls cpu, memory et pids sont délégués au compte. L'absence d'io se voit aussi au démarrage du démon, qui avertit No io.max (rbps) support (et la même ligne pour wbps, riops et wiops), dans le journal du démon à l'installation. Aucun contrôleur io n'étant délégué, les limites d'E/S comme --device-read-bps ne peuvent pas s'appliquer ; ce cas sort du périmètre de la démonstration. Si votre besoin en dépend, testez-le sur votre machine ou demandez à l'administrateur de déléguer ce contrôleur.

Non : en mode rootless, aucun profil AppArmor n'est appliqué, et l'option qui en demande un est ignorée sans erreur. La documentation Docker liste d'ailleurs AppArmor parmi les fonctions non prises en charge dans ce mode. Le démon l'indique dans ses options de sécurité, et le conteneur le confirme dans /proc/1/attr/current :

Fenêtre de terminal
# c37-dev, en session SSH
docker info --format '{{json .SecurityOptions}}'
docker run -d --name c37-m-aa --network none "$ALP" sleep 60 >/dev/null
docker inspect -f 'AppArmorProfile={{json .AppArmorProfile}}' c37-m-aa
docker exec c37-m-aa cat /proc/1/attr/current
docker rm -f c37-m-aa >/dev/null
docker run --rm --network none --security-opt apparmor=docker-default "$ALP" cat /proc/1/attr/current; echo "aa_explicite_rc=$?"
["name=seccomp,profile=builtin","name=rootless","name=cgroupns"]
AppArmorProfile=""
runc (unconfined)
runc (unconfined)
aa_explicite_rc=0

SecurityOptions ne contient pas apparmor, AppArmorProfile est vide et le processus 1 du conteneur est runc (unconfined). Demander explicitement --security-opt apparmor=docker-default réussit (code 0, mesuré sans tube) et ne change rien : une option de sécurité est ignorée en silence, ce qui est plus trompeur qu'une erreur. Le filtre seccomp reste actif (name=seccomp,profile=builtin). La leçon Confiner par profils : seccomp et AppArmor détaille ces deux couches.

Que peut atteindre un conteneur rootless qui s'échappe ?

Section intitulée « Que peut atteindre un conteneur rootless qui s'échappe ? »

Un conteneur rootless qui s'échappe par le démon ou le runtime obtient les pouvoirs du compte, pas ceux de root. La table des UID l'a montré : le root du conteneur est l'UID 1002, celui du compte, et les autres UID du conteneur tombent dans la plage subordonnée du compte. Une évasion atteindrait donc ce que ce compte peut lire ou écrire : ses clés SSH, son code, ses jetons. Le démon rootless n'est pas un démon sans pouvoir, c'est un démon qui n'a que ceux de son compte.

D'où le choix de la démonstration : un compte dédié, qui ne contient que son moteur rootless et ses données, et qui n'a ni les clés ni le code d'un autre projet. Lancer le mode rootless sous le compte personnel d'un développeur donne au démon l'accès à tout ce qu'il possède. Ajoutez à cela deux limites mesurées plus haut : aucun profil AppArmor, et un réseau hôte ouvert dès qu'un conteneur est lancé avec --network host.

Comment garder le démon rootless actif sans session ouverte ?

Section intitulée « Comment garder le démon rootless actif sans session ouverte ? »

Sans lingering, le démon s'arrête quand la dernière session du compte se ferme : le gestionnaire du compte s'arrête, rootlesskit disparaît et les commandes lancées ensuite échouent faute de socket. Le lingering, posé par l'administrateur avec sudo loginctl enable-linger, évite cela : c'est lui qui permet à une unité utilisateur de démarrer au boot et de survivre à la déconnexion. La mesure ci-dessous montre rootlesskit relancé (PID 8886) dès que enable-linger est rétabli.

Les contrôles qui suivent se font sans aucune session ouverte du compte : fermez votre session SSH de c37-dev avant ce test. Ils sont joués depuis l'administrateur. La fonction D() lance une commande Docker sous le compte, avec le socket du démon rootless. Un conteneur y est déclaré avec --restart unless-stopped, puis le lingering est retiré et rétabli. Le premier contrôle liste les sessions de la machine :

Fenêtre de terminal
# administrateur
D() { sudo -u c37-dev -H env DOCKER_HOST=unix:///run/user/1002/docker.sock docker "$@"; }
ALP="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"
loginctl list-sessions --no-legend
D run -d --name c37-m-veille --restart unless-stopped --network none "$ALP" sleep infinity >/dev/null; echo "run_rc=$?"
D ps --format '{{.Names}} {{.Status}}'
sudo loginctl disable-linger c37-dev; echo "disable_rc=$?"
for i in $(seq 1 90); do pgrep -u c37-dev rootlesskit >/dev/null || break; sleep 1; done; echo "attente=${i}s"
loginctl show-user c37-dev -p Linger -p State 2>&1
pgrep -u c37-dev -a rootlesskit || echo "aucun rootlesskit"
D ps 2>&1 | tail -1
sudo loginctl enable-linger c37-dev; echo "enable_rc=$?"
sleep 20
loginctl show-user c37-dev -p Linger -p State 2>&1
pgrep -u c37-dev -a rootlesskit | cut -c1-60
D ps --format '{{.Names}} {{.Status}}'
44 1002 c37-dev - 8217 manager - no -
46 1001 ansible - 8613 manager - no -
47 1001 ansible - 8680 user - no -
run_rc=0
c37-m-veille Up Less than a second
disable_rc=0
attente=13s
State=closing
Linger=no
aucun rootlesskit
failed to connect to the docker API at unix:///run/user/1002/docker.sock; check if the path is correct and if the daemon is running: dial unix /run/user/1002/docker.sock: connect: no such file or directory
enable_rc=0
State=lingering
Linger=yes
8886 rootlesskit --state-dir=/run/user/1002/dockerd-rootless
c37-m-veille Up 19 seconds

La première ligne de loginctl list-sessions appartient à c37-dev (UID 1002) et sa classe est manager : ce n'est pas une connexion, puisque vous avez fermé la vôtre, mais la session que systemd-logind ouvre pour le gestionnaire de services du compte, que le lingering maintient. Les deux lignes d'ansible, le compte de l'administrateur, portent une classe manager et une classe user, sa propre connexion.

La boucle for attend la fin de rootlesskit au lieu d'une durée fixe : attente=13s dit que l'arrêt a pris 13 secondes (la boucle abandonne à 90 s). Sans lingering, le compte est dans l'état closing, plus aucun rootlesskit ne tourne et le socket a disparu : l'API répond no such file or directory, et le conteneur n'est plus servi. Avec le lingering, l'état passe à lingering, rootlesskit revient (PID 8886) et le conteneur c37-m-veille, Up 19 seconds, est reparti grâce à sa politique de redémarrage. Pour qu'un service rootless survive à la déconnexion de son propriétaire, activez donc le lingering, comme le conseille la dernière ligne [INFO] de l'installation. Un point voisin : la documentation précise que lancer Docker rootless comme service système (/etc/systemd/system/docker.service), même avec User=, n'est pas pris en charge. Gardez l'unité utilisateur créée par l'outil.

La fonction D() ne donne accès qu'au démon, par DOCKER_HOST. Pour que l'administrateur pilote le gestionnaire de services du compte avec systemctl --user, un sudo -u ne suffit pas : il faut en plus que XDG_RUNTIME_DIR désigne le répertoire d'exécution du compte, et DBUS_SESSION_BUS_ADDRESS son bus quand le défaut ne convient pas. La démonstration n'en a pas besoin : elle passe par DOCKER_HOST.

Reste le redémarrage de la machine. Après un reboot, toujours sans que le compte se connecte, l'administrateur relève :

Fenêtre de terminal
# administrateur
D() { sudo -u c37-dev -H env DOCKER_HOST=unix:///run/user/1002/docker.sock docker "$@"; }
uptime -s
loginctl show-user c37-dev -p Linger -p State 2>&1
pgrep -u c37-dev -a rootlesskit | cut -c1-60
D ps --format '{{.Names}} {{.Status}}'
ip -br link show docker0
2026-11-04 20:25:19
State=lingering
Linger=yes
754 rootlesskit --state-dir=/run/user/1002/dockerd-rootless
c37-m-veille Up 14 seconds
docker0 DOWN ee:06:6b:a4:eb:6c <NO-CARRIER,BROADCAST,MULTICAST,UP>

La machine a redémarré à 20:25:19, et le démon rootless et son conteneur sont revenus (le conteneur affiche Up 14 seconds) sans que le compte se connecte : le lingering maintient le gestionnaire de services du compte. Le pont docker0 du moteur rootful est lui aussi présent sur l'hôte, à l'état DOWN.

Comment désinstaller proprement le mode rootless ?

Section intitulée « Comment désinstaller proprement le mode rootless ? »

La désinstallation se fait en trois étapes : le compte arrête et retire son moteur, il supprime ses données, puis l'administrateur retire le lingering, le compte et le paquet uidmap. Rouvrez une session SSH sous le compte, qui supprime ses objets et lance uninstall :

Fenêtre de terminal
# c37-dev, en session SSH
docker rm -f c37-m-veille >/dev/null; docker volume rm c37-m-donnees
dockerd-rootless-setuptool.sh uninstall 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; echo "uninstall_rc=${PIPESTATUS[0]}"
docker context ls
c37-m-donnees
+ systemctl --user stop docker.service
+ systemctl --user disable docker.service
Removed '/home/c37-dev/.config/systemd/user/default.target.wants/docker.service'.
[INFO] Uninstalled docker.service
[INFO] Deleted CLI context "rootless"
Current context is now "default"
[INFO] Configured CLI to use the "default" context.
[INFO]
[INFO] Make sure to unset or update the environment PATH, DOCKER_HOST, and DOCKER_CONTEXT environment variables if you have added them to `~/.bashrc`.
[INFO] This uninstallation tool does NOT remove Docker binaries and data.
[INFO] To remove data, run: `/usr/bin/rootlesskit rm -rf /home/c37-dev/.local/share/docker`
uninstall_rc=0
NAME DESCRIPTION DOCKER ENDPOINT ERROR
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock

L'outil arrête et désactive l'unité, supprime le contexte rootless et réactive default. Il dit aussi ce qu'il ne fait pas : il ne retire ni les binaires ni les données, et il propose rootlesskit rm -rf pour les données. Il faut passer par RootlessKit, parce que ces données appartiennent en partie à des UID subordonnés : le compte ne modifie pas ces fichiers depuis l'hôte, et il ne peut pas vider un répertoire de l'UID 1000, comme le rm refusé plus haut le montre. rootlesskit crée un nouvel espace de noms avec la même correspondance que le démon, où le compte est root : il n'entre pas dans un espace existant. La suppression s'y exécute donc avec les droits nécessaires :

Fenêtre de terminal
# c37-dev, en session SSH
du -sh ~/.local/share/docker
rootlesskit find ~/.local/share/docker -delete; echo "suppression_rc=$?"
ls -d ~/.local/share/docker 2>&1
13M /home/c37-dev/.local/share/docker
suppression_rc=0
ls: cannot access '/home/c37-dev/.local/share/docker': No such file or directory

Le dossier de 13 Mo a disparu. Ici, la commande jouée est rootlesskit find … -delete, qui fait le même travail que le rootlesskit rm -rf proposé par l'outil. Reste le côté administrateur. Fermez d'abord votre session SSH de c37-dev : l'administrateur retire ensuite le lingering, vérifie qu'aucun processus ne subsiste, supprime le compte et ses plages d'UID. La première commande compte les sessions du compte, pour montrer qu'il ne reste que celle du lingering :

Fenêtre de terminal
# administrateur
loginctl list-sessions --no-legend | awk '{print $3}' | grep -c c37-dev || true
sudo loginctl disable-linger c37-dev; echo "disable_rc=$?"
sleep 15
pgrep -u c37-dev -a . || echo "aucun processus de c37-dev"
sudo userdel -r c37-dev; echo "userdel_rc=$?"
grep -c '^c37-dev:' /etc/subuid /etc/subgid
ls -d /home/c37-dev 2>&1
1
disable_rc=0
aucun processus de c37-dev
userdel: c37-dev mail spool (/var/mail/c37-dev) not found
userdel_rc=0
/etc/subuid:0
/etc/subgid:0
ls: cannot access '/home/c37-dev': No such file or directory

Le 1 du début est la session manager du lingering, pas une connexion. Après disable-linger, plus aucun processus ne reste au compte, et userdel -r a retiré le compte, sa plage dans /etc/subuid et /etc/subgid (:0 partout) et son répertoire personnel. L'avertissement sur la boîte aux lettres est sans conséquence. Il reste le paquet uidmap, installé pour la leçon :

Fenêtre de terminal
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq uidmap >/dev/null 2>&1; echo "purge_uidmap_rc=$?"
dpkg-query -W uidmap 2>&1
find /tmp -maxdepth 1 -user 1002 2>/dev/null | wc -l
purge_uidmap_rc=0
dpkg-query: no packages found matching uidmap
0

uidmap est purgé, et aucun fichier de l'UID 1002 ne traîne dans /tmp (le dernier 0) : la machine est revenue à son état de départ, sans rien laisser derrière, hors les paquets Docker installés avant la leçon.

Le lab prépare un moteur rootless pour un compte dédié, par une unité utilisateur propre plutôt que par l'outil d'installation vu dans cette leçon, et laisse le moteur rootful du système intact. Il vous demande de qualifier ce moteur. Les tests mesurent l'état réel : l'unité utilisateur est active, le processus du démon appartient au compte non privilégié, le démon annonce le mode rootless, un vrai service HTTP répond sur la boucle locale, un pair du réseau est autorisé et un pair sans réseau refusé, et une donnée écrite dans un volume survit à la recréation du conteneur.

Le mode rootless remplace l'accès à un démon root par un démon qui n'a que les pouvoirs de son compte ordinaire, et ces pouvoirs sont ceux du compte : ses clés, son code.

  • Vous distinguez le groupe docker (un accès au démon root) du mode rootless (un démon et des conteneurs qui appartiennent au compte).
  • Vous vérifiez les prérequis : 65 536 UID subordonnés dans /etc/subuid et /etc/subgid (usermod --add-subuids … --add-subgids … pour un compte qui n'en a pas), le paquet uidmap, des cgroups v2, et docker-ce-rootless-extras à la version du moteur.
  • Vous posez le lingering avant l'installation, puis vous installez par dockerd-rootless-setuptool.sh install sous le compte, en session SSH ; check dit ce qui manque, et l'outil crée une unité systemd utilisateur, un contexte rootless et le socket /run/user/<uid>/docker.sock.
  • Vous prouvez le mode par name=rootless dans SecurityOptions, les processus ps du compte et DockerRootDir dans son répertoire personnel ; le magasin d'images est distinct de celui du moteur rootful.
  • Vous lisez /proc/self/uid_map : le root du conteneur est le compte, les autres UID tombent dans la plage subordonnée, et les fichiers d'un volume portent ces UID sur l'hôte : le compte ne les modifie pas, peut supprimer un fichier dont il possède le répertoire, mais ne vide pas un répertoire de l'UID 1000, d'où rootlesskit pour effacer les données.
  • Vous savez que les ports inférieurs à 1024 ne se publient pas, que l'adresse source des clients n'est pas transmise par défaut (le pilote de ports slirp4netns la conserve, plus lentement), et que --network host expose le service sur le réseau réel de la machine.
  • Vous savez que cpu, memory et pids sont délégués par défaut sur Debian 13, pas io, et qu'AppArmor n'est pas appliqué en rootless : seccomp reste actif.
  • Vous activez le lingering pour que le démon survive à la déconnexion et au redémarrage, et vous supprimez les données par rootlesskit find … -delete.

Vérifiez que l'essentiel est acquis : ce que le mode rootless change, comment lire la correspondance des UID et quelles contraintes il impose au réseau, aux ressources et à la persistance.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn