Rien ne gâte davantage une astreinte tranquille qu’un conteneur qui démarre correctement, journalise correctement, puis tombe sur une seule ligne : permission denied. C’est toujours le socket que vous avez monté « comme d’habitude », ou le nœud de périphérique que vous « lui avez sûrement donné ». L’équipe applicative jure que ça fonctionnait sur leur laptop. Vous jurez que ça ne peut pas être un problème de permissions parce que vous l’avez lancé en root. Vous avez tous les deux torts, chacun à sa façon.
Voici la méthode pratique et adaptée à la production pour déboguer les échecs de permissions Docker sur les sockets UNIX et les périphériques /dev — sans sortir --privileged comme si c’était un extincteur et que vous vous ennuyiez.
Le modèle mental : pourquoi « root » se fait quand même refuser
Quand un conteneur dit « permission denied », votre cerveau veut que ce soit les permissions UNIX classiques : utilisateur, groupe, bits de mode. Parfois c’est ça. Souvent ce n’est pas ça. Dans l’univers des conteneurs, l’accès est une négociation entre plusieurs videurs indépendants :
- Permissions de fichier (UID/GID/mode) sur le système de fichiers hôte (y compris les bind mounts).
- Mapping de namespaces (user namespaces, Docker rootless, remappage de root) qui change ce que signifient les UID/GID à l’intérieur du conteneur par rapport à l’extérieur.
- Capabilities Linux (pouvoirs « root » granulaires comme
CAP_NET_ADMIN). - Seccomp (filtrage d’appels système) qui peut bloquer des opérations avec une erreur trompeuse.
- LSM (SELinux/AppArmor) qui refuse des actions indépendamment des bits de mode.
- Contrôleur cgroups de périphériques (listes d’autorisation/refus des périphériques) qui peut bloquer
open()sur des nœuds de périphériques.
Ces couches échouent différemment, et les correctifs ne sont pas interchangeables. « Lancer en root » n’adresse qu’une seule couche (UID/GID) et parfois même pas celle-ci si des namespaces d’utilisateur sont impliqués. « Utiliser --privileged » démolit plusieurs couches à la fois et fait marcher tout jusqu’au moment où autre chose prend feu.
Règle à retenir : privileged n’est pas une permission ; c’est un changement d’environnement. Il désactive ou assouplit plusieurs protections. Voilà pourquoi ça « marche ». Voilà aussi pourquoi c’est généralement la mauvaise réponse.
Une citation qui tient bien dans l’opérationnel : L’espoir n’est pas une stratégie.
— James Cameron
Blague n°1 : Si --privileged est votre plan de débogage, vous ne déboguez pas — vous négociez avec le noyau à coups de corne de brume.
Faits intéressants et petite histoire (utile, pas trivia)
- Les capabilities ne sont pas nouvelles. Les capabilities Linux ont morcelé le « root » en pouvoirs discrets à la fin des années 1990 ; les conteneurs les ont juste rendues visibles au grand public.
- Docker a commencé comme colle pour LXC. Les premières versions de Docker s’appuyaient sur LXC ; l’écosystème s’est ensuite standardisé autour des spécifications OCI runtime.
docker.sockest effectivement root. L’accès à l’API Docker donne typiquement le contrôle sur l’hôte, car on peut monter le système de fichiers hôte ou démarrer des conteneurs privilégiés.- Le filtrage des périphériques par cgroups est antérieur à Docker. Le contrôleur devices existait bien avant l’engouement pour les conteneurs ; Docker s’en sert simplement pour limiter les aventures avec
/dev/mem. - Le profil seccomp par défaut de Docker est conservateur. Docker fournit un profil seccomp par défaut qui bloque plusieurs appels système ; beaucoup de « permission denied » sont en réalité des refus d’appels système.
- Rootless Docker a changé le modèle de menace. Le mode rootless évite un démon root, mais il change aussi la façon dont les périphériques et opérations privilégiées se comportent. Beaucoup de choses deviennent impossibles.
- SELinux rend « permission denied » littéral. Sur les systèmes SELinux, le DAC (bits de mode) peut dire « allow » et SELinux peut quand même dire « non ». Ce sont deux types de « permissions » différents.
- Monter un socket traverse des frontières de confiance. Un conteneur qui se connecte à un socket hôte hérite de ce que ce socket peut faire (systemd, containerd, Docker, daemons d’administration personnalisés).
Guide de diagnostic rapide
Vous êtes chronométré. Ne tatonnez pas au hasard. Faites ceci dans l’ordre ; chaque étape réduit rapidement la classe de défaillance.
1) Confirmez quel chemin échoue et où il vit
- Est-ce un bind mount depuis l’hôte ? Un socket créé à l’intérieur du conteneur ? Un nœud de périphérique transmis ?
- L’erreur survient-elle sur
open(),connect(), ou un appel de bibliothèque de plus haut niveau ?
2) Déterminez l’identité : UID/GID à l’intérieur vs à l’extérieur
- Vérifiez l’UID/GID du processus dans le conteneur.
- Vérifiez le propriétaire/mode du fichier sur l’hôte et les exigences de groupe (fréquent avec les sockets).
- Si rootless ou userns-remap est impliqué, supposez que le mapping d’UID est le problème jusqu’à preuve du contraire.
3) Vérifiez les « couches de politique » : LSMs et seccomp
- Si SELinux/AppArmor est activé, recherchez des refus AVC/AppArmor.
- Si l’échec intervient sur quelque chose de « privilégié » (mount, setns, perf, bpf, raw sockets), suspectez seccomp/capabilities.
4) Pour les périphériques, vérifiez les permissions cgroup de périphérique
- Si vous voyez
/dev/...mais ne pouvez pas l’ouvrir, c’est souvent le filtrage devices cgroups ou une capability manquante.
5) Ensuite seulement, envisagez d’ajouter des capabilities ou --device
- Préférez
--cap-add,--deviceet des corrections explicites de groupe. - Réservez
--privilegedpour des cas rares, et traitez-le comme une étape de diagnostic temporaire, pas comme une solution.
Tâches pratiques : commandes, sorties, décisions (12+)
Voici les mouvements que j’utilise réellement. Chaque tâche inclut ce que la sortie signifie et la décision à prendre. Exécutez-les sur l’hôte sauf indication contraire.
Tâche 1 : Reproduire l’échec avec un maximum de contexte
cr0x@server:~$ docker logs --tail=50 myapp
...snip...
Error: connect unix /var/run/docker.sock: permission denied
...snip...
Ce que ça signifie : Vous avez un chemin d’échec concret : /var/run/docker.sock. C’est un socket UNIX. L’opération est connect(), pas seulement open().
Décision : Concentrez-vous sur la propriété du socket, le groupe, les étiquettes SELinux/AppArmor, et si l’utilisateur du conteneur fait partie du bon groupe. Ne commencez pas par les capabilities.
Tâche 2 : Identifier l’utilisateur effectif du conteneur
cr0x@server:~$ docker exec myapp id
uid=10001(app) gid=10001(app) groups=10001(app)
Ce que ça signifie : Le processus n’est pas root. Il n’a pas de groupes supplémentaires. Si le socket exige un groupe (c’est souvent le cas), cela échouera.
Décision : Soit exécuter ce processus spécifique avec un groupe correspondant au socket, soit repenser l’architecture pour que le conteneur n’ait pas besoin d’accéder à Docker sur l’hôte.
Tâche 3 : Inspecter les permissions du socket sur l’hôte
cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan 3 10:12 /var/run/docker.sock
Ce que ça signifie : Seuls root et les membres du groupe docker peuvent se connecter. Mode 660. C’est typique.
Décision : Si vous insistez pour monter ce socket, l’utilisateur du conteneur a besoin du groupe docker (la correspondance de GID importe), ou il vous faut un proxy avec une API plus restreinte.
Tâche 4 : Confirmer le GID du groupe docker (mismatch de GID classique)
cr0x@server:~$ getent group docker
docker:x:998:cr0x
Ce que ça signifie : Le groupe docker sur l’hôte a le GID 998. À l’intérieur du conteneur, les IDs de groupe peuvent ne pas s’aligner sauf si vous les configurez.
Décision : Passez le groupe dans le conteneur (--group-add 998) ou construisez l’image de sorte que le conteneur ait un groupe avec le GID 998 et que le processus l’utilise.
Tâche 5 : Lancer un conteneur ponctuel avec group-add explicite pour valider
cr0x@server:~$ docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--group-add 998 \
alpine:3.20 sh -lc 'id && apk add --no-cache docker-cli >/dev/null && docker ps >/dev/null && echo OK'
uid=0(root) gid=0(root) groups=0(root),998
OK
Ce que ça signifie : Avec le groupe 998, le conteneur peut parler à l’API Docker.
Décision : Si cette « correction » est acceptable (souvent non), implémentez le mapping de groupe proprement. Sinon, traitez toute demande de montage de docker.sock comme une exception de sécurité nécessitant revue.
Tâche 6 : Diagnostiquer les refus SELinux (si SELinux est activé)
cr0x@server:~$ getenforce
Enforcing
Ce que ça signifie : La politique SELinux est active. Un simple bind mount peut être bloqué par les règles d’étiquetage.
Décision : Vérifiez les logs d’audit pour des AVC avant de toucher aux permissions ou aux capabilities.
Tâche 7 : Chercher les AVC récents qui correspondent à votre conteneur
cr0x@server:~$ sudo ausearch -m avc -ts recent | tail -n 5
type=AVC msg=audit(1735902901.123:812): avc: denied { connectto } for pid=21456 comm="myapp" path="/var/run/docker.sock" scontext=system_u:system_r:container_t:s0:c123,c456 tcontext=system_u:object_r:docker_var_run_t:s0 tclass=unix_stream_socket permissive=0
...snip...
Ce que ça signifie : SELinux a explicitement refusé connectto sur le socket. Même des permissions UNIX correctes n’y changeront rien.
Décision : Corrigez l’étiquetage/options de montage (:Z/:z) ou la politique. Ne faites pas un chmod sur le socket par frustration.
Tâche 8 : Valider le profil AppArmor (commun sur Ubuntu)
cr0x@server:~$ docker inspect --format '{{.AppArmorProfile}}' myapp
docker-default
Ce que ça signifie : Le profil AppArmor par défaut est appliqué. Il peut refuser certaines opérations (moins souvent pour les sockets, plus souvent pour les mounts, perf, ptrace).
Décision : Si vous voyez des refus AppArmor dans dmesg, ajustez avec un profil personnalisé ou testez avec --security-opt apparmor=unconfined uniquement comme diagnostic.
Tâche 9 : Vérifier le mode seccomp
cr0x@server:~$ docker inspect --format '{{.HostConfig.SecurityOpt}}' myapp
[]
Ce que ça signifie : Aucune option de sécurité personnalisée n’est définie ; le profil seccomp par défaut de Docker est en jeu.
Décision : Si l’échec est un refus d’appel système (souvent affiché comme Operation not permitted), testez avec --security-opt seccomp=unconfined pour confirmer, puis corrigez en n’ajoutant que les appels système/capabilities nécessaires.
Tâche 10 : Pour l’accès aux périphériques, vérifiez si le nœud existe dans le conteneur
cr0x@server:~$ docker exec myvpn ls -l /dev/net/tun
crw-rw-rw- 1 root root 10, 200 Jan 3 10:12 /dev/net/tun
Ce que ça signifie : Le nœud de périphérique est présent. Si l’appli ne peut toujours pas l’utiliser, le problème n’est pas « fichier manquant ». C’est soit le filtrage devices cgroups, soit une capability manquante (souvent CAP_NET_ADMIN).
Décision : Vérifiez les règles d’autorisation de périphérique et les capabilities requises, pas le chmod.
Tâche 11 : Confirmer que le conteneur a été démarré avec le périphérique autorisé
cr0x@server:~$ docker inspect --format '{{json .HostConfig.Devices}}' myvpn
[{"PathOnHost":"/dev/net/tun","PathInContainer":"/dev/net/tun","CgroupPermissions":"rwm"}]
Ce que ça signifie : Docker a configuré le passthrough du périphérique et les permissions cgroup pour lecture/écriture/mknod.
Décision : Si les permissions échouent encore, vous aurez probablement besoin d’une capability (CAP_NET_ADMIN) ou vous êtes bloqué par un LSM/seccomp.
Tâche 12 : Inspecter les capabilities du conteneur (ensemble effectif)
cr0x@server:~$ docker exec myvpn sh -lc 'apk add --no-cache libcap >/dev/null 2>&1; capsh --print | sed -n "1,8p"'
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
...snip...
Ce que ça signifie : Pas de cap_net_admin. Beaucoup de workflows VPN/TUN en ont besoin pour configurer des interfaces ou le routage.
Décision : Ajoutez --cap-add NET_ADMIN (et éventuellement NET_RAW, selon le cas) plutôt que --privileged.
Tâche 13 : Valider en exécutant avec la capability minimale ajoutée
cr0x@server:~$ docker run --rm -it \
--device /dev/net/tun \
--cap-add NET_ADMIN \
alpine:3.20 sh -lc 'ip link show >/dev/null 2>&1 || apk add --no-cache iproute2 >/dev/null; ip tuntap add dev tun0 mode tun && echo OK'
OK
Ce que ça signifie : Avec NET_ADMIN plus le périphérique, l’opération réussit.
Décision : Intégrez ces exigences dans la configuration de lancement (Compose/equivalent Kubernetes securityContext). N’escaladez pas davantage sauf si autre chose est bloqué.
Tâche 14 : Vérifier les logs kernel/audit pour des indices « denied » (LSM/seccomp)
cr0x@server:~$ dmesg | tail -n 8
[1735902910.441] audit: type=1400 audit(1735902910.441:813): apparmor="DENIED" operation="mount" info="failed flags match" error=-13 profile="docker-default" name="/sys/fs/cgroup/" pid=21999 comm="runc"
...snip...
Ce que ça signifie : AppArmor a refusé un mount. L’erreur est -13 (EACCES), qui ressemble à un problème de permissions au niveau utilisateur.
Décision : Vous ne réparerez pas ça avec chmod. Il faut un changement AppArmor, une stratégie de montage différente, ou ne pas faire ce montage depuis l’intérieur du conteneur.
Tâche 15 : Confirmer si vous êtes en mode rootless (et cesser d’attendre des miracles)
cr0x@server:~$ docker info --format '{{.SecurityOptions}}'
[name=seccomp,profile=default name=rootless]
Ce que ça signifie : Rootless est activé. De nombreuses opérations sur périphériques et bas niveau du réseau ne fonctionneront pas de la même façon, ou pas du tout.
Décision : Si vous avez besoin d’un accès brut aux périphériques ou de fonctionnalités réseau noyau, rootless est peut-être la mauvaise plateforme pour cette charge. Choisissez vos batailles.
Capacités vs privilégié : ce dont vous avez vraiment besoin
--privileged est le marteau : toutes les capabilities, tous les périphériques, et une série de restrictions de sécurité assouplies. C’est parfait pour prouver un point et terrible pour la production. Les capabilities sont le scalpel : vous ajoutez exactement ce dont le processus a besoin et gardez le reste du bac à sable intact.
Ce que --privileged change réellement
Le comportement exact varie selon la version de Docker et le noyau, mais en pratique le mode privilégié fait couramment tout ce qui suit :
- Ajoute toutes les capabilities au conteneur (ou presque), étendant les ensembles effectifs et bounding.
- Désactive le filtre devices cgroup : le conteneur peut accéder largement aux périphériques de l’hôte.
- Relâche certaines contraintes LSM selon la configuration (ce n’est pas un contournement universel, mais ça change la donne).
- Facilite les mounts, la manipulation de la pile réseau, le chargement de modules noyau (nécessite souvent la coopération de l’hôte), et la manipulation des namespaces.
C’est pour cela que ça « corrige » les permission denied. C’est aussi pour cela que cela peut transformer une application compromise en compromission de l’hôte en très peu d’étapes supplémentaires.
Besoins courants de capabilities selon les symptômes
- Besoin de créer TUN/TAP, définir des routes, configurer iptables :
CAP_NET_ADMIN(et parfoisCAP_NET_RAW). - Besoin de binder des ports <1024 :
CAP_NET_BIND_SERVICE(Docker accorde souvent ceci par défaut). - Besoin d’ajuster l’heure ou les réglages d’horloge :
CAP_SYS_TIME(évitez autant que possible). - Besoin de monter des systèmes de fichiers :
CAP_SYS_ADMIN(c’est la capability « tout-en-un » ; évitez si possible). - Besoin d’utiliser perf events : souvent bloqué par des réglages noyau et nécessite
CAP_SYS_ADMINou des changements sysctl ; souvent aussi bloqué par seccomp. - Besoin de changer la propriété sur des bind mounts : peut nécessiter
CAP_CHOWNmais normalement vous devriez corriger la propriété sur l’hôte ou utiliser le bon mapping UID à la place.
Le conseil ennuyeux qui marche
Si --privileged vous tente, posez d’abord :
- Puis-je résoudre ça par alignement UID/GID ou appartenance à un groupe ?
- Puis-je résoudre ça avec une seule capability et éventuellement un mapping de périphérique ?
- Puis-je déplacer l’action privilégiée vers un sidecar/agent avec une API strictement définie ?
- Le vrai problème est-il que nous essayons d’administrer l’hôte depuis l’intérieur d’un conteneur applicatif ?
Sockets UNIX : docker.sock, runtimes et vos propres sockets
Les sockets de domaine UNIX sont des fichiers, mais ce ne sont pas des fichiers ordinaires. Les « permissions » s’appliquent au moment de la connexion, et le service derrière le socket décide de ce que vous pouvez faire une fois connecté. Cela signifie deux plans de contrôle distincts :
- Permissions système de fichiers contrôlent qui peut se connecter.
- Autorisation du service (si elle existe) contrôle ce que vous pouvez faire après la connexion.
docker.sock : le pied de biche avec une excellente campagne marketing
Monter /var/run/docker.sock dans un conteneur est courant. C’est aussi un effondrement de frontière de sécurité. Si un attaquant obtient l’exécution de code dans ce conteneur, il peut souvent créer un nouveau conteneur avec le système de fichiers hôte monté et l’appeler « debug ».
Ainsi, quand vous réparez un « permission denied » sur docker.sock, vous ne résolvez pas seulement un problème technique. Vous accordez le contrôle de l’hôte. Traitez-le comme des clés SSH de production : fortement limité, audité, et rarement nécessaire.
Groupe et GID : le générateur de « marche sur mon laptop »
Le socket docker est généralement possédé par le groupe docker. À l’intérieur d’un conteneur, le nom « docker » importe peu ; c’est le GID numérique qui compte. Si le socket est root:docker avec GID 998, votre processus en conteneur doit être dans le GID 998 pour se connecter.
Il y a trois options sensées :
- Utiliser
--group-addavec le GID hôte. Bien pour des correctifs rapides, mais documentez pourquoi. - Créer un groupe dans l’image avec le même GID numérique et exécuter le processus avec ce groupe. Mieux pour la reproductibilité.
- Ne pas monter le socket. Utiliser un proxy restreint ou repenser l’architecture. Meilleur pour la sécurité.
Vos propres sockets de service (Postgres, Redis, daemons système)
Même schéma : le fichier socket sur l’hôte a un propriétaire/groupe/mode. Le processus client à l’intérieur du conteneur doit correspondre à ces permissions telles que vues par l’hôte. Si vous bind-montez /run/postgresql/.s.PGSQL.5432 dans un conteneur, le client à l’intérieur doit avoir les droits pour se connecter en fonction des bits de mode de ce socket.
Autre point : beaucoup de services placent des sockets sous /run, qui est un tmpfs recréé au démarrage. Si vous avez « corrigé » les bits de mode une fois, vous n’avez rien vraiment corrigé. Vous laissez une note pour le futur vous qui sera déçu.
Périphériques : /dev, cgroups, et pourquoi mknod n’est pas votre ami
Les nœuds de périphériques sous /dev sont des fichiers spéciaux qui représentent des interfaces noyau. L’accès est contrôlé par :
- Les permissions UNIX sur le nœud de périphérique (propriétaire/groupe/mode).
- La liste d’autorisation du contrôleur devices cgroup (major/minor + r/w/m).
- Les capabilities nécessaires pour effectuer l’opération (admin réseau, IO brut, etc.).
- Les LSM et seccomp pour certaines opérations sensibles.
Les échecs de permission de périphérique les plus courants
/dev/net/tun: Vous avez transmis le périphérique mais oubliéCAP_NET_ADMIN, ou vous êtes en rootless en attendant des miracles./dev/fuse: Vous avez transmis le périphérique mais le module noyau fuse n’est pas disponible, ou le runtime conteneur le désautorise sans configuration supplémentaire.- Périphériques GPU (
/dev/nvidia*,/dev/dri) : L’appartenance à un groupe (souventvideoourender) et les hooks runtime importent autant que les nœuds de périphérique. - Périphériques bloc : Si vous essayez de monter ou formater des disques depuis un conteneur, arrêtez-vous et réfléchissez au rayon d’impact. Puis réfléchissez encore.
--device est précis ; utilisez-le
Pour l’accès aux périphériques, préférez :
--device /dev/net/tun(ou le périphérique spécifique)--cap-addpour la capability qui autorise l’opération noyau associée- Une propriété de groupe explicite (par ex., ajouter au groupe
renderpour les périphériques DRM) quand applicable
Ne lancez pas --privileged pour un périphérique manquant. Si votre appli a besoin d’un périphérique, donnez-lui ce périphérique. Si elle a besoin de vingt périphériques, votre appli n’est peut‑être pas une appli : c’est un agent, et les agents méritent un examen différent.
Blague n°2 : Le noyau ne se soucie pas que votre conteneur « essaie juste d’aider ». C’est comme les RH : la politique d’abord, les sentiments après.
SELinux, AppArmor, seccomp : la couche « on dirait des permissions »
Les bits de mode sont juste la croûte extérieure. La garniture, c’est la politique.
SELinux : quand chmod devient art de performance
Sur les systèmes SELinux, un refus est souvent enregistré comme un AVC. Le processus a un contexte de sécurité (comme container_t) et la cible a un type (comme docker_var_run_t). Si la politique dit « non », c’est non.
Opérationnellement, la correction commune pour les bind mounts est l’étiquetage correct :
:Zpour relabeler le contenu pour un usage exclusif du conteneur:zpour relabeler pour un usage partagé entre conteneurs
Si vous ne relabellez pas et que SELinux est en enforcing, vous pouvez voir un socket et être quand même bloqué pour vous y connecter. Ce n’est pas Docker qui devient bizarre. C’est SELinux qui fait son travail.
AppArmor : plus discret, mais tout de même tranchant
Les refus AppArmor apparaissent dans dmesg et peuvent bloquer des mounts, ptrace, et d’autres opérations sensibles. Ils peuvent se manifester comme permission denied ou operation not permitted, selon l’appel et la façon dont il échoue.
Le test « facile » est --security-opt apparmor=unconfined. La correction « correcte » est d’écrire un profil AppArmor qui autorise exactement ce dont vous avez besoin. Si votre conteneur a besoin de monter des choses arbitraires, c’est un signe, pas un besoin de profil.
Seccomp : la main invisible qui rend « EPERM »
Seccomp filtre les appels système. Lorsqu’un appel est bloqué, vous obtenez souvent EPERM (Operation not permitted) et un rapport de bug qui dit « permissions ». Parfois c’est exact ; parfois c’est un appel système bloqué qui n’a rien à voir avec les permissions de fichier.
Pendant le dépannage, lancez temporairement sans confinement seccomp pour confirmer le diagnostic, puis ajustez le profil ou changez d’approche. Un exemple classique : des workloads qui utilisent des appels système récents que le profil par défaut n’autorise pas.
Docker rootless : règles différentes, mêmes douleurs
Docker rootless est excellent pour réduire le risque hôte. C’est aussi un retour à la réalité : si votre application doit effectuer des actions qui nécessitent normalement root sur l’hôte (périphériques, configuration réseau bas niveau, mounts), le mode rootless rendra cela difficile ou impossible.
Comment rootless change l’histoire des permissions
- Les user namespaces ne sont pas optionnels. Les UID/GID à l’intérieur du conteneur sont mappés vers des IDs non-root sur l’hôte.
- L’accès aux périphériques est limité. Même si vous voyez les nœuds de périphérique, les opérations peuvent être bloquées parce que les privilèges sous-jacents ne sont pas disponibles.
- Le réseau peut être différent. Selon la configuration, vous pourriez utiliser slirp4netns ou équivalent, changeant la signification de « admin réseau ».
Rootless n’est pas « Docker mais plus sûr ». C’est « Docker avec des contraintes différentes ». Si vous exécutez des moteurs de stockage, des points d’accès VPN, ou toute chose voulant être partie intégrante du noyau, rootless est peut‑être le mauvais outil.
Trois mini-histoires d’entreprise du terrain
Incident : la mauvaise hypothèse (root dans le conteneur == root sur l’hôte)
Une équipe a déployé un expéditeur de logs conteneurisé qui devait lire des logs tournés depuis un chemin hôte. En staging, ils ont exécuté le conteneur en root et bind-monté /var/log. Ça a marché. Le changement a passé la revue parce que « c’est en lecture seule ».
En production, le daemon Docker avait le remappage de user namespace activé. À l’intérieur du conteneur, le processus était UID 0. Sur l’hôte, il était mappé vers une plage d’UID non privilégiés. Soudain l’expéditeur commençait à échouer avec des permission denied sur certains fichiers tournés et pas d’autres. Les erreurs étaient intermittentes car la propriété des logs tournés variait selon le service et le job de rotation.
Ils ont essayé les incantations habituelles : chmod sur l’hôte, redémarrage du conteneur, exécution en root (il l’était déjà), ajout de --privileged (qui n’aidait pas de façon consistante parce que le mapping userns s’appliquait toujours). Pendant ce temps, les alertes perdaient des données parce que l’expéditeur laissait tomber des logs de services critiques pendant un incident.
La correction fut ennuyeuse : arrêter de supposer que l’UID 0 signifie quelque chose à travers une frontière de namespace. Ils ont aligné la propriété en utilisant des ACLs sur les répertoires de logs hôtes pour la plage d’UID remappée, et ils ont fait tourner l’expéditeur avec un UID spécifique qui correspondait à la politique hôte. Ils ont aussi ajouté une vérification canari qui lit un chemin de log connu au démarrage et échoue rapidement si c’est incorrect, au lieu de perdre silencieusement des logs.
Conclusion du postmortem : les conteneurs n’« ont pas cassé les permissions ». C’est l’hypothèse de l’équipe qui a cassé. Les namespaces ne sont pas une vibe ; c’est des maths.
Optimisation qui a mal tourné : « monte docker.sock pour éviter de déployer des agents »
Une équipe plateforme voulait accélérer les jobs CI. Idée : exécuter des conteneurs de build qui parlent au démon Docker hôte via /var/run/docker.sock. Pas de virtualisation imbriquée, pas de builders séparés, moins de surcharge. Tout le monde applaudissait, parce que c’était rapide, pas cher et « standard industriel ».
Puis un développeur a ajouté une dépendance qui exécutait un script post-install d’un paquet tiers. Le script n’était pas malveillant volontairement ; il était juste bâclé et supposait pouvoir introspecter l’environnement. Il a interrogé l’API Docker, vu qu’il avait le contrôle, et a lancé un conteneur aide. Ce conteneur auxiliaire a monté des chemins hôtes pour « cacher » des choses. Le cache incluait des credentials et des fichiers de configuration qui n’auraient jamais dû être visibles pour les builds.
La sécurité a détecté cela lors d’une revue de routine parce que les logs de build commençaient à contenir des détails d’environnement étranges. Rien n’a été exploité au-delà de ça, mais c’était une quasi‑faille. Le problème sous-jacent : monter docker.sock équivaut à donner une API administrative de l’hôte au conteneur.
La remédiation fut un service de builders contrôlé avec une API restreinte : « build ce repo à ce commit avec cette config ». Pas d’exposition générale de l’API Docker. Les builds sont devenus légèrement plus lents. L’organisation a mieux dormi. L’équipe plateforme a cessé de traiter docker.sock comme une fonctionnalité de commodité.
Leçon : si votre « optimisation » court-circuite une frontière de confiance, ce n’est pas une optimisation. C’est un instrument de dette avec un taux d’intérêt variable.
Pratique ennuyeuse mais correcte qui a sauvé la mise : capabilities explicites et vérifications préalables
Un groupe produit réseau exécutait des conteneurs qui créaient des interfaces TUN et appliquaient des règles de routage. Les prototypes précoces étaient tous --privileged parce que l’objectif était de livrer une démo, pas d’impressionner le noyau. Quand le produit est passé en production, un SRE a insisté pour documenter l’ensemble minimal : --device /dev/net/tun, --cap-add NET_ADMIN, plus quelques sysctls gérés en dehors du conteneur.
Cela paraissait pédant. Ça les a aussi forcés à documenter précisément quelles opérations le conteneur réalisait et où. Ils ont ajouté un préflight au démarrage : vérifier que /dev/net/tun est présent, vérifier que CAP_NET_ADMIN existe (via une petite auto-vérification), et journaliser une erreur claire sinon.
Quelques mois plus tard, un durcissement de l’hôte a ajusté le profil seccomp par défaut pour un sous-ensemble de nœuds. Quelques conteneurs ont commencé à échouer au démarrage avec des erreurs ressemblant à des permissions. Comme le préflight était explicite, le diagnostic fut rapide : les logs indiquaient « NET_ADMIN manquant » sur les nœuds affectés. Ce n’était pas manquant ; il était tronqué par une politique mal appliquée. Ils ont rollbacké la politique puis corrigé le processus de déploiement.
Rien d’héroïque. Personne n’a SSHé sur des machines à 3 h du matin. Le système a dit la vérité tôt, et la correction fut évidente. C’est le type d’ennui que vous voulez en production.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom : « permission denied » en se connectant à /var/run/docker.sock
Cause racine : l’utilisateur du conteneur n’est pas dans le groupe du socket (ou mismatch de GID), ou SELinux refuse la connexion.
Correction : alignez le GID avec --group-add ou création de groupe dans l’image ; sous SELinux utilisez l’étiquetage ou la politique appropriée. Et reconsidérez le montage de docker.sock.
2) Symptom : le nœud de périphérique existe, mais open() échoue avec permission denied
Cause racine : le cgroup devices le refuse, ou capability manquante pour l’opération associée.
Correction : passez le périphérique via --device avec rwm ; ajoutez la capability minimale (souvent NET_ADMIN pour TUN). Validez avec docker inspect et capsh.
3) Symptom : chmod 666 ne change rien (socket ou fichier)
Cause racine : SELinux/AppArmor refuse, ou l’objet n’est pas celui que vous pensez (ex. : un nouveau socket recréé sous /run).
Correction : vérifiez les logs AVC/AppArmor, étiquetez les montages correctement, et corrigez le service qui crée le socket (unité systemd) au lieu de courir après des fichiers transitoires.
4) Symptom : marche avec –privileged, échoue sans
Cause racine : vous avez besoin soit d’un mapping de périphérique, soit d’une capability, soit d’une permission seccomp, soit d’une permission de montage.
Correction : bisectez : ajoutez d’abord --device (si pertinent), puis ajoutez une seule capability, puis testez seccomp unconfined pour confirmer. Remplacez privileged par des exigences explicites.
5) Symptom : les containers rootless ne peuvent pas accéder à /dev/kmsg, /dev/net/tun, ou monter des systèmes de fichiers
Cause racine : le mode rootless manque des privilèges root de l’hôte par conception.
Correction : ne lancez pas ces workloads en rootless. Utilisez un pool de nœuds rootful pour les workloads privilégiés, ou déplacez l’opération vers l’hôte via un agent.
6) Symptom : « Operation not permitted » lors d’un mount ou setns
Cause racine : bloqué par seccomp/AppArmor ou capability CAP_SYS_ADMIN manquante.
Correction : évitez de le faire à l’intérieur du conteneur. Si inévitable, utilisez un profil seccomp/AppArmor personnalisé et justifiez explicitement la capability. Traitez CAP_SYS_ADMIN comme « privileged-lite ».
7) Symptom : le conteneur voit le socket mais la connexion échoue seulement sur certains hôtes
Cause racine : différences de politique au niveau hôte (SELinux en enforcing sur certains, GIDs différents, permissions d’unité systemd différentes).
Correction : standardisez la configuration hôte ; ajoutez des vérifications préalables qui journalisent la propriété/GID du socket hôte au démarrage du conteneur ; traitez la dérive comme une cause d’incident.
8) Symptom : accès aux chemins fichiers ok mais pas quand ils sont bind-montés
Cause racine : options/étiquettes de montage (SELinux), ou mapping userns qui change la sémantique de propriété.
Correction : étiquetez avec :Z/:z sous SELinux ; assurez-vous que le mapping UID/GID correspond ; envisagez d’utiliser des volumes nommés avec gestion de propriété si approprié.
Checklists / plan étape par étape
Checklist A : Corriger un socket UNIX « permission denied » sans devenir privilégié
- Identifiez le chemin du socket à partir des logs ou d’un debug équivalent à strace dans l’appli.
- Sur l’hôte :
ls -ldu socket et capturez propriétaire/groupe/mode. - Dans le conteneur : vérifiez
idpour UID/GIDs. - Alignez l’accès groupe par GID numérique :
- Préférez
--group-add <gid>comme test rapide. - En production, créez le groupe dans l’image avec le même GID et exécutez le processus dedans.
- Préférez
- Si SELinux activé : vérifiez les logs AVC ; étiquetez le montage correctement.
- Si AppArmor activé : vérifiez
dmesgpour refus ; ajustez le profil si nécessaire. - Documentez le risque si le socket est administratif (docker.sock, containerd, systemd).
Checklist B : Corriger « permission denied » sur périphérique /dev de la bonne façon
- Confirmez que le nœud existe dans le conteneur (
ls -l). - Confirmez que le périphérique est passé (
docker inspect .HostConfig.Devices). - Déterminez la capability requise pour l’opération (TUN et routes :
NET_ADMIN, sockets bruts :NET_RAW). - Ajoutez une capability à la fois et retestez.
- Vérifiez les logs LSM/seccomp si l’échec persiste.
- Refusez CAP_SYS_ADMIN par défaut. Si quelqu’un le demande, faites-lui expliquer l’appel système et l’alternative.
Checklist C : Usage sûr de docker.sock (si vraiment nécessaire)
- Modélisez la menace : supposez qu’une compromission du conteneur équivaut à une compromission de l’hôte.
- Limitez qui peut le déployer et où il peut tourner (nœuds dédiés, politiques réseau strictes).
- Exécutez en non-root et ajoutez seulement le groupe docker par GID numérique.
- Préférez un proxy qui expose seulement les endpoints nécessaires, pas l’API Docker complète.
- Ajoutez du monitoring pour les créations de conteneurs inattendues et les montages hôtes initiés via ce socket.
FAQ
1) Pourquoi un conteneur lancé en root obtient-il encore « permission denied » ?
Parce que « root » à l’intérieur d’un conteneur peut ne pas être root sur l’hôte (user namespaces), et parce que LSMs, seccomp et règles cgroup devices peuvent refuser l’accès indépendamment de l’UID.
2) Dois-je monter /var/run/docker.sock dans un conteneur ?
Rarement. Traitez-le comme si vous donniez le contrôle administratif de l’hôte. Si vous le faites, utilisez un mapping GID explicite, des contrôles de déploiement stricts, et documentez l’exception.
3) Quelle est la différence entre --cap-add et --privileged ?
--cap-add accorde une capability kernel spécifique. --privileged accorde un large ensemble de capabilities, relâche les restrictions devices, et affaiblit l’isolation de plusieurs façons. L’un est un scalpel ; l’autre est un chariot élévateur.
4) Mon appli a besoin de CAP_SYS_ADMIN. Est‑ce acceptable ?
Partons du principe « non » jusqu’à preuve du contraire. CAP_SYS_ADMIN couvre une vaste gamme d’opérations (mounts et interactions de namespace notamment). Souvent il existe une alternative : faire le mount sur l’hôte, utiliser un plugin de volume, ou repenser le workflow.
5) Pourquoi ça marche sur mon laptop mais pas en prod ?
Politiques hôte différentes : SELinux en enforcing en prod, profils AppArmor différents, GID du groupe docker différent, userns-remap activé, ou versions noyau/seccomp différentes. Les conteneurs sont portables ; les politiques hôtes ne le sont pas.
6) Comment savoir si SELinux est le problème ?
Si getenforce renvoie Enforcing et que vous voyez des AVC dans les logs d’audit référant votre contexte de conteneur et l’objet cible, SELinux est la cause. Corrigez l’étiquetage ou la politique ; ne faites pas un chmod comme solution.
7) Quelle est la manière la plus sûre de donner à un conteneur l’accès à un socket UNIX hôte ?
Assurez-vous que le socket sert une API non-administrative ; définissez le mode du socket pour exiger un groupe dédié ; ajoutez seulement ce GID numérique au conteneur ; évitez d’exécuter l’ensemble du conteneur en root. Si c’est un socket admin, reconsidérez fortement.
8) En quoi les permissions des périphériques diffèrent-elles des permissions normales de fichiers ?
Les nœuds de périphérique passent aussi par la liste d’autorisation devices cgroup et nécessitent souvent des capabilities pour les opérations noyau associées. Voir /dev/net/tun ne signifie pas que vous pouvez l’utiliser.
9) Docker rootless est-il une solution à ces problèmes de permissions ?
Il réduit le risque, mais il enlève aussi la capacité à faire beaucoup d’opérations privilégiées. Si votre workload a besoin de périphériques ou de configuration réseau noyau, rootless rendra cela plus difficile, pas plus facile.
Prochaines étapes que vous pouvez vraiment faire
Si vous fixez un « permission denied » aujourd’hui, arrêtez de deviner et commencez à classifier :
- Est‑ce un socket ou un périphérique ? Ce choix détermine le chemin le plus rapide.
- Vérifiez l’identité (UID/GID et mapping) avant de toucher aux capabilities.
- Consultez les logs SELinux/AppArmor/seccomp quand chmod ne change rien.
- Remplacez privileged par des exigences explicites : un
--device, une--cap-add, un--group-add, plus l’étiquetage si nécessaire. - Écrivez des vérifications préalables dans les conteneurs qui dépendent d’intégrations hôtes, pour que les échecs soient bruyants et précis.
Et si quelqu’un vous demande de « juste monter docker.sock », demandez quel problème il cherche vraiment à résoudre. La plupart du temps, la bonne correction n’est pas une permission. C’est une question d’architecture.