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

Retirer les accès directs au moteur

Par mis à jour le

Améliorer cette page

Un compte du groupe docker et un conteneur qui monte docker.sock ont le même pouvoir : ils parlent au démon Docker, et le démon tourne en root. Cette leçon vous apprend à dresser la liste de ces accès directs, à comprendre pourquoi chacun vaut un accès root sur l'hôte, puis à les retirer et à vérifier que la voie Docker est réellement fermée. Cette vérification a une limite : elle ne dit pas ce qu'un compte a pu faire pendant qu'il était root par Docker, et la leçon dit où elle s'arrête. Les mesures viennent d'une VM Debian 13 jetable avec Docker Engine 29.9.2 : ne rejouez pas les retraits sur un hôte partagé.

Chaque retrait se vérifie sur l'état réel, pas sur la configuration annoncée.

  • Vous inventoriez les voies d'accès au démon : groupe, mode du socket, ACL, écoute TCP et sudo.
  • Vous démontrez qu'appartenir au groupe docker équivaut à être root.
  • Vous retirez un compte du groupe, vous fermez ses processus encore ouverts et vous retirez une ACL qui rouvre l'accès sans le groupe.
  • Vous auditez tous les conteneurs, arrêtés compris, qui montent le socket, directement ou par un répertoire parent (/run ou /), et vous le prouvez par l'inode.
  • Vous constatez qu'un montage en lecture seule n'empêche pas d'arrêter un conteneur.
  • Vous retirez un montage en recréant le conteneur sans perdre sa configuration.
  • Vous savez ce que le retrait ne prouve pas.

La leçon Faire tourner les journaux, et reprendre du disque (C35b) précède celle-ci. Vous devez savoir lancer un conteneur avec un montage -v, lire un ls -l et utiliser sudo. Les droits Unix sur un fichier sont détaillés dans la page Affiner les permissions avec les ACL, qui explique aussi getfacl et setfacl.

La démonstration utilise le compte stagiaire-ops et les conteneurs c36-tableau, c36-racine, c36-renomme, c36-arrete et c36-sain. Les deux labs de fin de leçon travaillent sur d'autres noms. Tous les conteneurs partent de la même image, épinglée à son digest :

Fenêtre de terminal
IMG='alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a'
docker version --format 'client={{.Client.Version}} serveur={{.Server.Version}}'
client=29.9.2 serveur=29.9.2

Le démon Docker écoute par défaut sur un socket Unix, un fichier spécial /run/docker.sock : quiconque peut l'ouvrir peut envoyer des ordres au démon. L'inventaire consiste donc à lister qui peut ouvrir ce fichier, ou joindre le démon autrement.

Fenêtre de terminal
ls -l /run/docker.sock
readlink -f /var/run/docker.sock
getent group docker
srw-rw---- 1 root docker 0 Nov 4 18:46 /run/docker.sock
/run/docker.sock
docker:x:989:ansible

Le mode srw-rw---- donne la lecture et l'écriture à root et au groupe docker, et rien aux autres. getent group docker liste les membres de ce groupe, ici ansible, le compte d'exploitation de la VM.

readlink -f montre ensuite que /var/run/docker.sock, le chemin que les outils utilisent, désigne le même fichier que /run/docker.sock, car /var/run est un lien vers /run. Cette résolution servira pour les montages.

Un + à la fin du mode, comme dans srw-rw----+, signale une ACL, une liste de droits supplémentaires attachée au fichier, que ls -l ne détaille pas. Pour la lire, il faut getfacl, absent de la Debian 13 minimale de la VM : le paquet acl l'apporte.

Fenêtre de terminal
sudo apt-get install -y acl
getfacl -p /run/docker.sock

Sans ACL, getfacl -p ne rend que les trois entrées du mode classique (propriétaire, groupe, autres). La sortie d'apt-get n'est pas reproduite :

/run/docker.sock
# owner: root
# group: docker
user::rw-
group::rw-
other::---

Le démon peut aussi écouter en TCP, ce qui donnerait son API à tout le réseau qui l'atteint. Cela se lit dans la ligne de démarrage du service et dans les ports en écoute :

Fenêtre de terminal
systemctl cat docker | grep -E '^ExecStart='
sudo ss -lntp | grep -E 'dockerd' || echo "aucune écoute TCP de dockerd"
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
aucune écoute TCP de dockerd

-H fd:// signifie que le démon reçoit son socket de systemd, sans option tcp://. Cherchez dockerd dans ss -lntp, pas le mot « docker » : le processus docker-proxy écoute légitimement en TCP pour les ports publiés, et il n'est pas l'API. Avec ce ExecStart, une clé hosts dans /etc/docker/daemon.json n'ouvre pas de port : sur la VM, le démon refuse de démarrer, avec le message the following directives are specified both as a flag and in the configuration file: hosts. Une écoute TCP vient donc d'un -H tcp:// dans le service ou dans une surcharge systemd, que systemctl cat docker affiche en entier. Les deux contrôles se font ensemble, parce que ss voit l'écoute réelle, quelle que soit son origine.

Dernière voie à contrôler, sudo. La commande sudo -l -U <compte> dit si un compte peut passer root autrement : avec un sudo large, le groupe docker est superflu, puisque le compte est déjà root. Une règle sudo limitée à /usr/bin/docker donne aussi root : le compte peut lancer sudo docker run -v /:/hote … et obtenir la même racine montée que le groupe.

Pourquoi appartenir au groupe docker revient-il à être root ?

Section intitulée « Pourquoi appartenir au groupe docker revient-il à être root ? »

Le démon exécute les ordres de n'importe quel client qui ouvre son socket, avec ses propres droits, ceux de root. Un membre du groupe peut donc lui demander de lancer un conteneur qui monte les fichiers de l'hôte, puis d'y lire ou d'y écrire comme root.

Pour le montrer, créez un compte de travail sans sudo et ajoutez-le au groupe :

Fenêtre de terminal
sudo useradd -m -s /bin/bash stagiaire-ops
sudo usermod -aG docker stagiaire-ops
id stagiaire-ops
uid=1002(stagiaire-ops) gid=1002(stagiaire-ops) groups=1002(stagiaire-ops),989(docker)

Avec ce compte, sans aucun sudo, lancez deux conteneurs. Le premier monte /etc de l'hôte en lecture seule et lit la première ligne de shadow, le fichier des empreintes de mots de passe. Le second monte toute la racine de l'hôte et exécute id dans cette racine :

Fenêtre de terminal
sudo -n -u stagiaire-ops docker run --rm -v /etc:/hote-etc:ro "$IMG" head -1 /hote-etc/shadow
sudo -n -u stagiaire-ops docker run --rm -v /:/hote "$IMG" chroot /hote id
root:*:20760:0:99999:7:::
uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo)

La première sortie est la première ligne de /etc/shadow de l'hôte. La seconde montre que chroot /hote, qui prend la racine de l'hôte comme racine, ouvre une session uid=0 dans le système réel. Le même compte, sans Docker, n'obtient rien :

Fenêtre de terminal
sudo -n -u stagiaire-ops head -1 /etc/shadow
head: cannot open '/etc/shadow' for reading: Permission denied

Le groupe docker n'est donc pas un droit « de gérer des conteneurs » : c'est un accès root sur l'hôte, qu'il faut accorder comme tel.

Comment retirer un compte du groupe docker, et que faire des sessions ouvertes ?

Section intitulée « Comment retirer un compte du groupe docker, et que faire des sessions ouvertes ? »

Avant de retirer le compte, il faut une session de ce compte déjà ouverte, pour voir ce qu'elle devient. Ouvrez un second terminal et lancez-y la commande ci-dessous. Elle affiche les groupes du compte, attend 30 secondes, les affiche de nouveau, puis lance docker ps. Dès qu'elle tourne, revenez au premier terminal sans attendre : vous avez 30 secondes pour y lancer le retrait. Si c'est trop court, remplacez sleep 30 par sleep 60.

Fenêtre de terminal
# second terminal, à lancer en premier
sudo -n -u stagiaire-ops bash -c 'id -nG; sleep 30; echo "--- après le retrait, dans la même session :"; id -nG; docker ps >/dev/null; echo "docker ps rc=$?"'

Dans le premier terminal, retirez le compte avec gpasswd -d. Vérifiez ensuite la liste du groupe, les groupes du compte et une nouvelle commande docker ps, dont vous relevez le code de retour :

Fenêtre de terminal
# premier terminal, juste après avoir lancé la commande du second
sudo gpasswd -d stagiaire-ops docker
getent group docker
id stagiaire-ops
sudo -n -u stagiaire-ops docker ps; echo "rc=$?"
Removing user stagiaire-ops from group docker
docker:x:989:ansible
uid=1002(stagiaire-ops) gid=1002(stagiaire-ops) groups=1002(stagiaire-ops)
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
rc=1

Le groupe ne liste plus le compte et id non plus. La nouvelle commande est refusée, avec le message permission denied while trying to connect to the docker API, et son code de retour vaut 1. Le retrait semble acquis.

Regardez pourtant le second terminal : une session ouverte avant le retrait garde son accès. Au bout de ses 30 secondes, il affiche :

stagiaire-ops docker
--- après le retrait, dans la même session :
stagiaire-ops docker
docker ps rc=0

Les deux premières lignes donnent les groupes avant et après le retrait : la session voit encore docker, et son docker ps réussit (rc=0). Les groupes d'un processus sont fixés à l'ouverture de la session, et le retrait ne les modifie pas : cette session reste utilisable jusqu'à sa fermeture.

Un retrait ne vaut donc qu'une fois les sessions ouvertes fermées, sinon l'accès retiré dans /etc/group reste ouvert en pratique. Contrôlez par un appel réel, docker ps lancé depuis une nouvelle connexion du compte, plutôt que par la ligne de /etc/group.

Comment fermer les processus encore ouverts du compte ?

Section intitulée « Comment fermer les processus encore ouverts du compte ? »

Pour fermer ces sessions, listez d'abord les processus du compte avec pgrep -u. Un processus lancé par sudo -u, qui joue ici le rôle d'une session oubliée, apparaît dans la liste :

Fenêtre de terminal
sudo -u stagiaire-ops bash -c 'sleep 600' &
pgrep -u stagiaire-ops -a
38840 sleep 600

loginctl terminate-user ferme les sessions de connexion d'un compte (SSH, console), mais il ne voit pas ce processus :

Fenêtre de terminal
sudo loginctl terminate-user stagiaire-ops; echo "rc=$?"
Could not terminate user: User ID 1002 is not logged in or lingering
rc=1

Le message dit que loginctl ne connaît aucune session de ce compte : le processus existe, mais il n'est rattaché à aucune connexion. pkill -KILL -u termine tous les processus du compte, quelle que soit leur origine, et pgrep confirme qu'il n'en reste aucun :

Fenêtre de terminal
sudo pkill -KILL -u stagiaire-ops; echo "pkill rc=$?"
pgrep -u stagiaire-ops -a || echo "aucun processus restant après pkill"
pkill rc=0
aucun processus restant après pkill

Un multiplexeur de terminal, une tâche cron ou un service systemd --user laissés par le compte sont aussi des processus de ce compte, donc couverts par pgrep -u et pkill -u. Ces cas n'ont pas été mesurés ici : relancez pgrep après coup pour vérifier qu'ils n'ont pas repris vie.

Quelles autres voies rouvrent l'accès sans le groupe ?

Section intitulée « Quelles autres voies rouvrent l'accès sans le groupe ? »

Le groupe n'est qu'une façon d'ouvrir le socket. Une ACL peut donner le même droit à un compte précis, sans passer par le groupe. Le compte est aujourd'hui refusé ; après setfacl, il passe :

Fenêtre de terminal
sudo setfacl -m u:stagiaire-ops:rw /run/docker.sock
getfacl -p /run/docker.sock
ls -l /run/docker.sock
sudo -n -u stagiaire-ops docker ps --format '{{.Names}}' >/dev/null; echo "rc=$?"
/run/docker.sock
# owner: root
# group: docker
user::rw-
user:stagiaire-ops:rw-
group::rw-
mask::rw-
other::---
srw-rw----+ 1 root docker 0 Nov 4 18:46 /run/docker.sock
rc=0

La ligne user:stagiaire-ops:rw- est l'ACL, et le + de ls -l la signale : le compte appelle le démon alors qu'il n'est pas dans le groupe. Un inventaire qui lit seulement getent group docker la manque. Lisez aussi ls -l et getfacl.

Pour retirer cette ACL sans redémarrer le démon, supprimez l'entrée du compte, puis relisez-la avec getfacl et appelez le démon depuis le compte :

Fenêtre de terminal
sudo setfacl -x u:stagiaire-ops /run/docker.sock
getfacl -p /run/docker.sock
ls -l /run/docker.sock
sudo -n -u stagiaire-ops docker ps; echo "rc=$?"
/run/docker.sock
# owner: root
# group: docker
user::rw-
group::rw-
mask::rw-
other::---
srw-rw----+ 1 root docker 0 Nov 4 18:46 /run/docker.sock
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
rc=1

La ligne user:stagiaire-ops:rw- a disparu de getfacl et l'appel est refusé (rc=1). Remarquez que ls -l affiche encore le + : il reste après un setfacl -x, parce que le masque subsiste. Le + seul ne permet donc pas de conclure, c'est getfacl qui tranche.

Redémarrer le socket (systemctl restart docker.socket docker) efface aussi l'ACL, mais il redémarre le démon : sans live-restore, les conteneurs sont relancés. La leçon Mettre à jour le moteur détaille ce comportement ; sur un hôte en service, préférez setfacl -x.

Ce constat ne vaut que pour une ACL posée à la main. Une ACL posée par une unité systemd ou par une règle udev serait réappliquée à chaque création du socket ; ce cas n'a pas été mesuré, cherchez-le dans les unités et les règles de l'hôte.

Reste sudo. Sur la VM, sudo -l -U stagiaire-ops rend User stagiaire-ops is not allowed to run sudo on c45-debian. (c45-debian est le nom de la VM), donc aucune voie par là. Un compte autorisé à tout faire avec sudo serait root sans passer par Docker.

Un conteneur qui monte docker.sock donne à son processus le pouvoir de tout compte du groupe docker. Le schéma résume les voies d'accès vues jusqu'ici : les flèches épaisses sont des accès directs au socket, et les flèches en pointillés passent par un répertoire qui le contient, /run ou la racine /. Tout aboutit au même socket, donc au même démon root.

Cinq voies mènent au socket du démon : un compte du groupe docker, un compte avec une ACL, un conteneur qui monte le socket, un conteneur qui monte un répertoire parent contenant le socket, et un conteneur qui monte la racine de l'hôte. Le socket mène à dockerd, qui tourne en root et exécute les ordres sur l'hôte.contient le socketcontient le socketexécute en rootGroupedockerACLsur le socketConteneur,socket montéConteneur,répertoire parent montéConteneur,racine / montéeSocket du démon/run/docker.sockdockerdtourne en rootHôtefichiers, processus, comptes
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Cinq conteneurs de démonstration couvrent ces formes, et une absence. Le premier monte le socket par son chemin et un volume nommé, avec les labels, la variable d'environnement et la politique de redémarrage d'un petit tableau de bord. Les trois suivants montent /, /run sous un autre nom, et /run dans un conteneur arrêté créé avec --restart always. Le dernier ne monte rien :

Fenêtre de terminal
docker run -d --name c36-tableau --label equipe=supervision --restart unless-stopped -e MODE=lecture \
-v /var/run/docker.sock:/var/run/docker.sock:ro -v c36-cache:/cache "$IMG" sleep 3600 >/dev/null
docker run -d --name c36-racine -v /:/hote:ro "$IMG" sleep 3600 >/dev/null
docker run -d --name c36-renomme -v /run:/data:ro "$IMG" sleep 3600 >/dev/null
docker create --name c36-arrete --restart always -v /run:/r "$IMG" sleep 3600 >/dev/null
docker run -d --name c36-sain "$IMG" sleep 3600 >/dev/null

Pour les auditer, un script lit les montages de chaque conteneur avec docker inspect, résout chaque source avec readlink -f, puis la compare au socket résolu. Une source qui est le socket, ou un répertoire qui le contient, l'expose. Comparer des chaînes de caractères laisserait passer /var/run/docker.sock, qui est /run/docker.sock sous un autre nom, et /run.

Fenêtre de terminal
SOCK=$(readlink -f /var/run/docker.sock)
for c in $(docker ps -a --format '{{.Names}}'); do
docker inspect -f '{{range .Mounts}}{{.Source}} {{.Destination}}{{"\n"}}{{end}}' "$c" |
while read -r src dst; do
[ -z "$src" ] && continue
r=$(sudo readlink -f "$src")
if [ "$SOCK" = "$r" ] || [ "${SOCK#"${r%/}/"}" != "$SOCK" ]; then
echo "$c : $src -> $dst EXPOSE"
else
echo "$c : $src -> $dst"
fi
done
done
c36-arrete : /run -> /r EXPOSE
c36-renomme : /run -> /data EXPOSE
c36-racine : / -> /hote EXPOSE
c36-tableau : /var/lib/docker/volumes/c36-cache/_data -> /cache
c36-tableau : /var/run/docker.sock -> /var/run/docker.sock EXPOSE

Chaque ligne donne la source du montage et sa destination ; EXPOSE marque une source qui est le socket ou qui le contient. Quatre conteneurs sont signalés, dont c36-racine (la racine contient /run/docker.sock) et c36-arrete, qui est arrêté mais que --restart always relancera. Le volume nommé c36-cache n'est pas signalé, et c36-sain n'apparaît pas faute de montage. Monter /run ou / n'est donc pas plus sûr que monter le socket.

Deux détails du script évitent des pièges faciles à écrire. Le premier concerne la racine. Pour r=/, un motif simple comme case "$SOCK" in "$r"/*) devient //*, qui ne correspond à aucun chemin absolu : un conteneur qui monte / passerait inaperçu. Le test du script retire la barre finale de la source, ${r%/}, puis cherche ce préfixe suivi de / au début du socket ; pour la racine, il retire le / initial de /run/docker.sock et le signale.

Le second piège concerne readlink -f. Sans sudo, la commande échoue sur les chemins de /var/lib/docker, que seul root peut traverser : la variable r reste vide, et un test mal écrit déclarerait alors exposé un simple volume nommé. Avec sudo readlink -f, le volume c36-cache est résolu et correctement non signalé. La boucle porte enfin sur docker ps -a, pas docker ps, pour ne pas oublier un conteneur arrêté.

Ce script se fie à la configuration des conteneurs. Pour une preuve vue de l'intérieur, comparez l'inode, le numéro qui identifie un fichier pour le noyau, accompagné du numéro de périphérique. Un chemin est ce qu'on a écrit, l'inode est ce que le noyau désigne : deux noms qui mènent au même couple sont le même fichier. La boucle suivante cherche tous les sockets visibles dans chaque conteneur actif, sans chemin écrit à l'avance, et ne garde que ceux dont le couple est celui du socket de l'hôte :

Fenêtre de terminal
H=$(stat -c '%d:%i' "$SOCK"); echo "hôte : $H"
for c in $(docker ps --format '{{.Names}}'); do
docker exec "$c" find / -path /proc -prune -o -type s -print 2>/dev/null |
while read -r s; do
[ "$(docker exec "$c" stat -c '%d:%i' "$s")" = "$H" ] && echo "$c : $s"
done
done
hôte : 25:18226
c36-renomme : /data/docker.sock
c36-racine : /hote/run/docker.sock
c36-tableau : /run/docker.sock

Les trois conteneurs exposés montrent le même couple 25:18226 que l'hôte, sous trois noms différents : le nom ne prouve rien, le couple oui. c36-sain n'affiche rien. Le filtre cache pourtant un fait grave : un conteneur qui monte / ou /run voit aussi des dizaines d'autres sockets de l'hôte, ceux de containerd, de dbus et de systemd, et l'agent SSH d'un utilisateur (/run/user/1001/openssh_agent). Retirer l'accès à docker.sock ne suffit donc pas à rendre ces montages inoffensifs : c'est le montage lui-même qu'il faut retirer.

Pourquoi le montage en lecture seule ne protège-t-il pas ?

Section intitulée « Pourquoi le montage en lecture seule ne protège-t-il pas ? »

Les deux premiers conteneurs portent :ro, et ce :ro ne protège de rien : il s'applique au fichier du socket, pas aux requêtes qui y circulent. Depuis c36-tableau, un POST d'arrêt visant c36-sain réussit. La commande installe d'abord curl dans le conteneur depuis les dépôts d'Alpine : la VM doit donc joindre Internet, sinon elle ne rend pas 204.

Fenêtre de terminal
docker exec c36-tableau sh -c 'apk add -q --no-cache curl >/dev/null 2>&1; curl -s -o /dev/null -w "%{http_code}\n" --unix-socket /var/run/docker.sock -X POST http://x/containers/c36-sain/stop'
docker ps -a --filter name=c36-sain --format '{{.Names}} {{.Status}}'
204
c36-sain Exited (137) Less than a second ago

Le code 204 dit que le démon a exécuté l'arrêt, et c36-sain est sorti avec le code 137. Si un outil a vraiment besoin de l'API, par exemple un tableau de bord qui liste les conteneurs, la solution n'est ni le :ro ni le socket brut : c'est un proxy qui filtre les requêtes. La leçon Interposer un proxy devant l'API Docker (C36b) enseigne ce filtrage et ses limites.

Un montage se décide à la création du conteneur : on ne le retire pas d'un conteneur qui tourne. Il faut recréer le conteneur sans le montage du socket. Recréer, c'est supprimer l'ancien conteneur et en lancer un nouveau : tout ce qui n'est pas répété dans la nouvelle commande, labels, variables d'environnement, politique de redémarrage, autres montages, est perdu. Relevez donc la configuration avant de supprimer :

Fenêtre de terminal
docker inspect -f 'labels={{json .Config.Labels}} env={{json .Config.Env}} restart={{.HostConfig.RestartPolicy.Name}} mounts={{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' c36-tableau
labels={"equipe":"supervision"} env=["MODE=lecture","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"] restart=unless-stopped mounts=/var/lib/docker/volumes/c36-cache/_data:/cache /var/run/docker.sock:/var/run/docker.sock

Le conteneur porte un label, une variable MODE, la politique unless-stopped et deux montages : le volume c36-cache et le socket (la variable PATH vient de l'image). Recréez-le avec les mêmes options, sauf le socket, puis relisez la configuration et cherchez le socket :

Fenêtre de terminal
docker rm -f c36-tableau >/dev/null
docker run -d --name c36-tableau --label equipe=supervision --restart unless-stopped -e MODE=lecture \
-v c36-cache:/cache "$IMG" sleep 3600 >/dev/null
docker inspect -f 'labels={{json .Config.Labels}} env={{json .Config.Env}} restart={{.HostConfig.RestartPolicy.Name}} mounts={{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' c36-tableau
docker exec c36-tableau ls -l /var/run/docker.sock; echo "rc=$?"
labels={"equipe":"supervision"} env=["MODE=lecture","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"] restart=unless-stopped mounts=/var/lib/docker/volumes/c36-cache/_data:/cache
ls: /var/run/docker.sock: No such file or directory
rc=1

Les labels, la variable et la politique de redémarrage sont identiques, le volume nommé a survécu (il vit hors du conteneur) et seul le montage du socket a disparu. Le message d'erreur dit que le conteneur recréé ne voit plus le socket : c'est une preuve plus fiable que la relecture de la commande de lancement. La recréation interrompt pourtant le service pendant quelques secondes, puisqu'il s'agit d'un nouveau conteneur : prévenez les utilisateurs avant.

Quand le conteneur est décrit dans un fichier Compose, préférez-le : supprimez la ligne de volumes qui monte le socket, puis lancez docker compose up -d. Compose détecte le changement, reprend le reste de la configuration du fichier et affiche Recreated pour le conteneur ; ls -l /var/run/docker.sock y rend ensuite No such file or directory, comme ci-dessus.

Il reste à nettoyer la démonstration et le compte de travail :

Fenêtre de terminal
docker rm -f c36-tableau c36-racine c36-renomme c36-arrete c36-sain >/dev/null
docker volume rm c36-cache >/dev/null
sudo userdel -r stagiaire-ops
docker ps -a --filter name=c36- --format '{{.Names}}'; id stagiaire-ops

userdel signale que le compte n'avait pas de boîte aux lettres, ce qui est sans conséquence. id confirme que le compte n'existe plus, et la liste des conteneurs reste vide :

userdel: stagiaire-ops mail spool (/var/mail/stagiaire-ops) not found
id: ‘stagiaire-ops’: no such user

Que prouve un docker ps refusé, et que reste-t-il à vérifier ?

Section intitulée « Que prouve un docker ps refusé, et que reste-t-il à vérifier ? »

Un docker ps refusé prouve une seule chose : la voie Docker du compte est fermée. Il ne prouve pas que le compte a perdu tout accès à l'hôte. Un compte qui a été root par Docker a pu laisser un conteneur lancé avec --restart qui monte /, une clé SSH dans /root/.ssh/authorized_keys ou un binaire setuid : aucun de ces accès ne passe plus par le socket, et le retrait du groupe n'y change rien.

Ce qui relève de la leçon, c'est l'audit des conteneurs : docker ps -a, ceux créés pendant la période où le compte avait l'accès (docker inspect -f '{{.Created}}' <nom> rend une date comme 2026-11-04T18:47:34.250972599Z, en UTC) et les montages de /, de /run ou du socket, par le script d'audit de la section « Quels conteneurs montent le socket du démon ? ». La recherche d'une clé SSH ajoutée ou d'un binaire setuid relève d'une revue d'incident, hors de cette leçon.

Le premier lab demande de retirer l'accès Docker d'un compte d'audit temporaire, sans le supprimer et sans toucher à l'accès de l'opérateur. Deux contrôles mesurent l'état réel de l'hôte : le compte n'appartient plus au groupe docker, et un appel réel au démon depuis ce compte échoue. Garder le compte et l'accès de l'opérateur sont des interdictions du scénario, vérifiées avant les mesures : elles ne rapportent aucun point.

Le second lab part d'un conteneur applicatif qui monte /var/run/docker.sock en lecture seule. Vous constatez d'abord qu'un POST d'arrêt passe, ce qui n'est pas noté. Vous retirez ensuite la capacité d'atteindre le socket par tout chemin, en laissant le conteneur actif et son label en place. Deux contrôles le mesurent. Le premier résout chaque source de montage avec readlink -f et refuse celle qui est le socket ou qui le contient. Le second relève depuis le conteneur le couple périphérique-inode de chaque socket visible et refuse celui du démon.

Un accès direct au démon est un accès root sur l'hôte, et l'inventaire doit chercher toutes ses formes.

  • Vous inventoriez le groupe docker, le mode du socket, le + d'une ACL dans ls -l avec getfacl, une écoute TCP de dockerd et les droits sudo.
  • Vous savez qu'un membre du groupe docker est root sur l'hôte : il lit /etc/shadow et obtient uid=0 par chroot dans une racine montée.
  • Vous retirez un compte avec gpasswd -d, puis vous fermez ses sessions ouvertes : elles gardent leur accès.
  • Vous contrôlez le retrait par un appel réel, pas par /etc/group, car une ACL posée à la main rouvre l'accès sans le groupe ; setfacl -x la retire et getfacl tranche, pas le + de ls -l.
  • Vous fermez les processus du compte avec pgrep -u et pkill -KILL -u.
  • Vous résolvez chaque source de montage avec sudo readlink -f et vous comparez au socket ou à ses répertoires parents, /run et / compris, sur tous les conteneurs, arrêtés inclus.
  • Vous prouvez l'exposition par l'inode, vu depuis le conteneur, et le retrait par l'absence du socket dans le conteneur recréé avec la même configuration.
  • Vous savez qu'un docker ps refusé ne ferme que la voie Docker : l'audit des conteneurs en fait partie, le reste relève d'une revue d'incident.
  • Vous savez que le :ro n'empêche pas un POST (204) : un outil qui a besoin de l'API passe par un proxy.

Vérifiez que l'essentiel est acquis : ce que donne le groupe docker, ce qu'un retrait laisse ouvert et la façon de prouver un montage du socket.

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