Volumes Docker : Permission refusée — stratégies UID/GID qui soulagent

Cet article vous a aidé ?

« Permission denied » sur un volume Docker n’est jamais seulement un problème de permissions. C’est un problème de désalignement d’identités, déguisé en erreur de système de fichiers, et il apparaît généralement au pire moment : pendant un déploiement, une migration, ou un « petit changement » que quelqu’un jurait sûr.

Si vous exécutez des conteneurs en production, vous ne pouvez pas traiter les UID/GID comme de la trivia. Les conteneurs écrivent des fichiers. Les hôtes appliquent la propriété. Le stockage réseau ajoute des règles. Le durcissement de la sécurité ajoute des options supplémentaires. Votre travail est d’aligner ces règles.

Pourquoi ça revient sans cesse (c’est l’identité, pas de la magie)

Sur Linux, les permissions de fichiers sont appliquées par des identifiants numériques : UID pour le propriétaire, GID pour le groupe. Des noms comme www-data ne sont qu’une table de correspondance. Lorsqu’un processus dans un conteneur écrit dans un répertoire monté, le système de fichiers de l’hôte voit un UID et un GID, pas « l’utilisateur du conteneur ».

Voici le problème central : l’idée du conteneur pour l’UID 1000 peut être « appuser », mais l’hôte peut voir l’UID 1000 comme « alice », et votre serveur NFS peut décider que c’est « nobody ». Même numéro, sens différent, ou même sens, numéro différent. Dans tous les cas, l’hôte dit : non.

Les volumes Docker rendent cela visible car ce sont littéralement des systèmes de fichiers (bind mounts) ou des répertoires gérés par l’hôte (named volumes). Votre conteneur n’est pas une petite VM avec son propre noyau et son univers de permissions. Ce sont juste des processus avec une vue restreinte de l’hôte. Le noyau fait le portier, et il vérifie les IDs, pas les excuses.

Un autre angle tranchant : beaucoup d’images officielles sont construites pour s’exécuter par défaut en root. Ça « marche » jusqu’à ce que vous montiez un chemin hôte appartenant à un utilisateur normal, ou que vous durcissiez le conteneur pour qu’il s’exécute en non-root, ou que votre backend de stockage refuse les écritures root-squashées. Alors vous découvrez que la voie « facile » n’était qu’une dette technique avec une minuterie.

Blague courte #1 : Les conteneurs n’« ont pas de permissions ». Ils empruntent les vôtres, salissent le tapis et vous laissent avec le rapport de sécurité.

Faits et historique qui expliquent la douleur d’aujourd’hui

  • Les permissions Unix sont plus anciennes que la plupart de vos infrastructures : l’application des UID/GID remonte aux débuts d’Unix ; le modèle est simple, durable et indifférent aux conteneurs.
  • Les premiers choix par défaut de Docker favorisaient la commodité : les workflows initiaux poussaient « exécuter en root » parce que cela évitait des frictions ; la sécurité en production a rendu ce choix coûteux par la suite.
  • Les bind mounts précèdent les conteneurs de décennies : le noyau ne se préoccupe pas de l’origine d’un mount ; il applique les mêmes règles de propriété.
  • Les user namespaces existaient bien avant d’être populaires : les user namespaces Linux sont apparus il y a des années, mais leur adoption a traîné car ils sont puissants et faciles à mal configurer.
  • Le root squash NFS est un dispositif d’arrêt de catastrophe volontaire : de nombreuses exports NFS mappent root distant sur nobody par conception, précisément pour empêcher que le root de conteneur devienne root du stockage.
  • SELinux/AppArmor ont changé la donne : les distributions modernes peuvent refuser l’accès même si UID/GID semblent corrects, parce que MAC (contrôle d’accès obligatoire) se situe au-dessus des permissions DAC classiques.
  • Les fichiers overlay ajoutent des symptômes confus : overlay2 peut donner l’impression que les permissions « ont changé aléatoirement », alors que vous voyez en réalité des couches fusionnées et des répertoires opaques.
  • Kubernetes a normalisé les workloads non-root : une fois que les clusters ont commencé à appliquer « runAsNonRoot », l’industrie a cessé de se permettre des suppositions laxistes sur les UID/GID.

Il y a une raison pour laquelle cela n’a pas été « corrigé » par un meilleur runtime de conteneurs : le noyau fait exactement ce qu’il est censé faire.

Une citation d’opérations à garder sur votre bureau : « L’espoir n’est pas une stratégie. » — General Gordon R. Sullivan

Méthode de diagnostic rapide (premier/deuxième/troisième)

Premier : identifiez qui le processus pense être

Si vous ne connaissez pas l’UID/GID effectif dans le conteneur, vous devinez. Obtenez les numéros. Confirmez quel utilisateur le processus applicatif exécute réellement, pas ce que le Dockerfile laisse entendre.

Second : identifiez quel est le chemin hôte et qui en est propriétaire

« Volume » peut signifier bind mount, named volume, NFS, CIFS, ou un montage géré par un plugin. Chacun a des comportements de propriété et d’ACL différents. Déterminez le chemin réel de stockage et sa propriété, ses bits de mode, ses ACL et tout label MAC.

Troisième : vérifiez les couches de politique au‑dessus des permissions POSIX

Quand les bits de mode semblent corrects mais que ça échoue encore, c’est généralement SELinux, AppArmor, ou le root squash sur le stockage réseau. Ne passez pas une heure à chmoder pour creuser dans le mauvais fossé.

Arbre de décision rapide

  • Fonctionne sans volume, échoue avec volume : désalignement d’identité ou politique de stockage.
  • Fonctionne en root, échoue en non-root : propriété/mode du répertoire incorrects pour l’UID/GID visé.
  • Fonctionne sur disque local, échoue sur NFS/CIFS : root squash, idmapping, ou permissions/ACL côté serveur.
  • N’échoue que sur Fedora/RHEL : suspectez d’abord un problème d’étiquetage SELinux.

Tâches pratiques : commandes, sorties et décisions (12+)

Voici les commandes que j’exécute réellement lors d’incidents. Chaque tâche inclut : la commande, ce que signifie la sortie, et la décision à prendre.

Task 1: confirm the container user (effective UID/GID)

cr0x@server:~$ docker exec -it app sh -lc 'id; umask'
uid=10001(app) gid=10001(app) groups=10001(app),10002(shared)
0022

Signification : L’application écrit en tant qu’UID 10001/GID 10001, umask par défaut 0022 (fichiers 644, répertoires 755 sauf override).

Décision : Le répertoire monté doit être inscriptible par l’UID 10001 ou par un groupe dont le processus fait partie (comme GID 10002), ou via une ACL.

Task 2: confirm what’s mounted where (inside container)

cr0x@server:~$ docker exec -it app sh -lc 'mount | sed -n "1,5p"; mount | grep -E "/data|/var/lib"'
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
/dev/sdb1 on /data type ext4 (rw,relatime)

Signification : /data est un montage réel, ext4, pas un chemin overlay à l’intérieur du conteneur.

Décision : Le dépannage doit inclure la propriété/mode du système de fichiers hôte pour le point de montage ext4.

Task 3: inspect the Docker mount configuration (host view)

cr0x@server:~$ docker inspect app --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/app/data","Destination":"/data","Mode":"rw","RW":true,"Propagation":"rprivate"}]

Signification : C’est un bind mount depuis /srv/app/data. C’est la permission de ce répertoire qui pose problème, pas le driver de volume Docker.

Décision : Corriger la propriété/mode/ACL sur /srv/app/data, ou changer l’UID/GID d’exécution du conteneur.

Task 4: inspect host directory ownership, perms, and ACLs

cr0x@server:~$ sudo ls -ldn /srv/app/data
drwxr-x--- 2 0 0 4096 Jan  2 10:11 /srv/app/data
cr0x@server:~$ sudo getfacl -p /srv/app/data
# file: /srv/app/data
# owner: root
# group: root
user::rwx
group::r-x
other::---

Signification : Propriété root, groupe root, et « other » sans accès. L’UID 10001 ne peut ni écrire ni même lire.

Décision : Soit chown/chgrp pour correspondre à l’identité du conteneur, soit donner l’accès via groupe/ACL. Ne faites pas chmod 777 sauf si vous aimez les comptes-rendus d’incident.

Task 5: reproduce the failure with a write test (inside container)

cr0x@server:~$ docker exec -it app sh -lc 'touch /data/.permtest && echo OK'
touch: cannot touch '/data/.permtest': Permission denied

Signification : Ce n’est pas votre application. C’est un échec fondamental d’écriture.

Décision : Corriger d’abord l’accès au système de fichiers ; ne reconfigurez pas l’application tant qu’un simple touch ne fonctionne pas.

Task 6: check if SELinux is denying (host)

cr0x@server:~$ getenforce
Enforcing
cr0x@server:~$ sudo ls -ldZ /srv/app/data
drwxr-x---. 2 root root unconfined_u:object_r:default_t:s0 4096 Jan  2 10:11 /srv/app/data

Signification : SELinux est en enforcing, et le répertoire a un label générique (default_t) que les conteneurs peuvent ne pas être autorisés à accéder.

Décision : Ajouter les options de montage SELinux appropriées (:Z ou :z) pour les bind mounts, ou relabeler le chemin vers un type compatible conteneur.

Task 7: check AppArmor profile usage (host)

cr0x@server:~$ docker inspect app --format '{{.AppArmorProfile}}'
docker-default

Signification : Le profil AppArmor par défaut est appliqué. Généralement acceptable, mais des profils personnalisés peuvent bloquer des montages ou des chemins.

Décision : Si les permissions semblent correctes et SELinux est désactivé, auditez les logs AppArmor et les règles du profil.

Task 8: identify named volume backing path (if not bind mount)

cr0x@server:~$ docker volume inspect appdata --format '{{.Mountpoint}}'
/var/lib/docker/volumes/appdata/_data
cr0x@server:~$ sudo ls -ldn /var/lib/docker/volumes/appdata/_data
drwxr-xr-x 2 0 0 4096 Jan  2 09:50 /var/lib/docker/volumes/appdata/_data

Signification : Les named volumes sont par défaut appartenants à root sur l’hôte à moins que l’image ou une étape d’initialisation ne change cela.

Décision : Créer une étape d’initialisation contrôlée pour définir la propriété une fois, ou exécuter le service avec un UID/GID correspondant.

Task 9: check if you’re on NFS and whether root squash is biting you

cr0x@server:~$ mount | grep -E ' nfs| nfs4'
10.0.2.10:/exports/app on /srv/app/data type nfs4 (rw,relatime,vers=4.1,proto=tcp,clientaddr=10.0.2.21,local_lock=none,sec=sys)
cr0x@server:~$ sudo touch /srv/app/data/.hosttest
touch: cannot touch '/srv/app/data/.hosttest': Permission denied

Signification : Même le root de l’hôte ne peut pas écrire. C’est le classique root squash ou des permissions/ACL côté serveur.

Décision : Arrêtez de traiter cela comme un problème Docker. Corrigez les permissions de l’export NFS et le mapping d’UID côté serveur, ou utilisez un UID de service dédié qui existe de manière cohérente sur tous les clients.

Task 10: verify ownership numeric mapping across host and container

cr0x@server:~$ getent passwd 10001
appuser:x:10001:10001::/nonexistent:/usr/sbin/nologin
cr0x@server:~$ docker exec -it app sh -lc 'getent passwd 10001 || true; cat /etc/passwd | tail -n 3'
app:x:10001:10001:app:/home/app:/bin/sh
messagebus:x:100:102::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin

Signification : L’UID 10001 existe sur l’hôte et dans le conteneur ; bon signe. Si l’hôte montre un utilisateur différent pour 10001, votre histoire de « UID correspondant » est une fiction.

Décision : Si possible, standardisez les UIDs entre systèmes pour les services stateful. Si vous ne le pouvez pas, utilisez des ACLs ou des montages idmapped (là où disponibles) au lieu de penser que tout ira bien.

Task 11: test group-based access (the sane middle ground)

cr0x@server:~$ sudo groupadd -g 10002 shared 2>/dev/null || true
cr0x@server:~$ sudo chgrp -R 10002 /srv/app/data
cr0x@server:~$ sudo chmod -R g+rwX /srv/app/data
cr0x@server:~$ sudo chmod g+s /srv/app/data
cr0x@server:~$ sudo ls -ldn /srv/app/data
drwxrws--- 2 0 10002 4096 Jan  2 10:15 /srv/app/data

Signification : Le répertoire appartient au groupe 10002 et le bit setgid est défini pour que les nouveaux fichiers héritent du groupe 10002.

Décision : Faites que le processus du conteneur soit membre du GID 10002 (via l’image ou l’exécution). Cela évite de chowner tout au même UID et s’intègre mieux pour l’accès partagé.

Task 12: add an ACL for a specific UID (when groups aren’t enough)

cr0x@server:~$ sudo setfacl -m u:10001:rwx /srv/app/data
cr0x@server:~$ sudo setfacl -m d:u:10001:rwx /srv/app/data
cr0x@server:~$ sudo getfacl -p /srv/app/data | sed -n '1,12p'
# file: /srv/app/data
# owner: root
# group: shared
user::rwx
user:10001:rwx
group::rwx
mask::rwx
other::---
default:user::rwx
default:user:10001:rwx
default:group::rwx

Signification : L’UID 10001 a un accès explicite, et les nouveaux fichiers héritent de cet accès via l’ACL par défaut.

Décision : Utilisez les ACLs quand vous avez besoin de plusieurs rédacteurs avec des UIDs différents, surtout sur plusieurs hôtes, sans recourir au 777.

Task 13: check for immutable attributes (the “why won’t chmod work” moment)

cr0x@server:~$ sudo lsattr -d /srv/app/data
-------------e-- /srv/app/data

Signification : Aucun drapeau immutable. Si vous voyez i, les changements échoueront silencieusement ou avec EPERM.

Décision : Si immutable est défini, retirez-le volontairement (chattr -i) et documentez pourquoi il y était.

Task 14: verify actual write path and errors via strace (surgical, not daily)

cr0x@server:~$ docker exec -it app sh -lc 'strace -f -e trace=file -o /tmp/trace.log sh -lc "touch /data/x" || true; tail -n 5 /tmp/trace.log'
openat(AT_FDCWD, "/data/x", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666) = -1 EACCES (Permission denied)
write(2, "touch: cannot touch '/data/x': Permission denied\n", 52) = 52
exit_group(1)                           = ?
+++ exited with 1 +++

Signification : Le noyau a renvoyé EACCES à la création. C’est une dénégation DAC/MAC classique, pas « le fichier existe déjà », ni « disque plein ».

Décision : Revenez à la propriété/ACL/SELinux. Ne perdez pas de temps sur la configuration applicative.

Stratégies UID/GID qui fonctionnent réellement

Strategy 1: Match the host’s ownership (run the container with the same UID/GID)

C’est l’approche la plus propre pour les bind mounts sur un hôte unique : choisissez un UID/GID de service sur l’hôte, et exécutez le processus du conteneur avec cette même identité numérique.

Dans Docker Compose, c’est généralement :

cr0x@server:~$ cat docker-compose.yml
services:
  app:
    image: yourorg/app:1.2.3
    user: "10001:10001"
    volumes:
      - /srv/app/data:/data:rw

Quand le faire : services stateful mono-nœud, déploiements simples, disques locaux, UIDs prévisibles.

Éviter si : vous partagez le même volume entre beaucoup d’hôtes avec des allocations UID incohérentes, ou si vous dépendez de groupes supplémentaires qui diffèrent selon l’environnement.

Strategy 2: Group-based access + setgid directories (best “shared volume” default)

Si plusieurs services ont besoin d’accès, ou si des opérateurs manipulent des fichiers, l’accès par groupe est votre ami. Créez un GID partagé, appliquez la propriété de groupe au répertoire, définissez setgid, et assurez-vous que les utilisateurs des conteneurs rejoignent le groupe.

Côté conteneur, vous pouvez ajouter des groupes supplémentaires :

cr0x@server:~$ cat docker-compose.yml
services:
  app:
    image: yourorg/app:1.2.3
    user: "10001:10001"
    group_add:
      - "10002"
    volumes:
      - /srv/app/data:/data:rw

Pourquoi ça marche : Ça évolue mieux que la propriété par UID, et le bit setgid évite la dérive où certains fichiers appartiennent à des groupes aléatoires.

Strategy 3: ACLs for mixed writers (precision tool, not a hammer)

Les ACLs POSIX permettent d’accorder l’accès à plusieurs UIDs/GIDs sans changer le propriétaire principal. Elles sont idéales quand un répertoire est administré par une équipe mais écrit par plusieurs services avec des identités numériques différentes.

Règle : Si vous utilisez des ACLs, utilisez aussi des ACLs par défaut, sinon vous résoudrez la panne d’aujourd’hui et casserez la création de fichiers de demain.

Strategy 4: One-time initialization (chown once, not on every start)

Beaucoup d’images résolvent cela en faisant un chown -R au démarrage. C’est acceptable uniquement quand le répertoire de données est petit et local. En production avec des données réelles, chown récursif à chaque démarrage est une auto‑attaque par déni de service.

Mieux : un job d’initialisation explicite qui s’exécute une fois par nouveau volume, définit la propriété, puis ne s’exécute plus sauf rotation intentionnelle du stockage.

Approche exemple : exécuter un conteneur temporaire pour initialiser un named volume :

cr0x@server:~$ docker run --rm -u 0:0 -v appdata:/data busybox sh -lc 'mkdir -p /data && chown -R 10001:10001 /data && ls -ldn /data'
drwxr-xr-x    2 10001    10001         4096 Jan  2 10:20 /data

Décision : Utilisez ceci pour les named volumes et le provisioning initial. N’intégrez pas de chown récursif dans le démarrage principal du service sauf si vous aimez les redémarrages lents et les longues pannes.

Strategy 5: Don’t fight the image—choose images that support non-root properly

Certaines images définissent correctement un utilisateur non-root et respectent des variables d’environnement PUID/PGID, ou acceptent proprement un --user d’exécution. D’autres supposent root et échouent ensuite de manière créative quand vous leur interdisez root.

Votre décision : si une image de service stateful ne peut pas s’exécuter en non-root sans bricolages, traitez-la comme un passif. Corrigez le Dockerfile en interne ou changez d’image. « Mais ça marche en root » n’est pas une posture de sécurité.

Strategy 6: User namespace remapping (strong isolation, extra complexity)

Les user namespaces vous permettent de mapper le root du conteneur (UID 0) sur une plage UID non privilégiée de l’hôte. Cela réduit le rayon d’explosion en cas d’évasion de conteneur. Mais cela rend également les permissions de volumes confuses si vous n’êtes pas préparé.

Quand activé, un fichier créé par « root dans le conteneur » peut apparaître sur l’hôte comme UID 165536, parce que c’est le host UID mappé. C’est un comportement correct. C’est juste surprenant si vous ne l’avez pas planifié.

Utiliser quand : vous avez besoin d’une meilleure protection de l’hôte et pouvez standardiser les mappings sur les nœuds.

Éviter quand : vous dépendez fortement de bind mounts vers des répertoires gérés par des humains et ne pouvez pas tolérer la complexité du mapping.

Strategy 7: Rootless Docker (good security baseline, still needs planning)

Rootless Docker exécute le démon et les conteneurs sans privilèges root. Cela réduit fortement les incidents où le root du conteneur devient root de l’hôte. Mais les volumes demandent encore une propriété correcte sous le home de l’utilisateur rootless et des plages UID/GID subordonnées.

Point clé : rootless change l’emplacement du stockage et la façon dont les IDs sont mappés. Ce n’est pas un remplacement transparent pour « tout sous /srv ».

Strategy 8: Kubernetes: runAsUser + fsGroup (if you’re in that world)

Dans Kubernetes, l’équivalent d’une « stratégie UID/GID » est généralement un securityContext avec runAsUser, runAsGroup et fsGroup. fsGroup aide car le kubelet peut ajuster la propriété de groupe/permissions sur les volumes montés pour permettre l’écriture par groupe.

Mais ne considérez pas fsGroup comme magique. Sur certains types de volumes, il nécessite un changement récursif des permissions, ce qui peut être douloureusement lent sur de grands jeux de données. Planifiez-le.

Blague courte #2 : « Juste chmod 777 » est l’équivalent stockage de « juste redémarrer » — parfois efficace, toujours suspect.

Modes de défaillance spécifiques au stockage (ext4, NFS, CIFS, ZFS, overlay)

Local filesystems (ext4/xfs): predictable, but still easy to sabotage

Sur disque local, UID/GID et les bits de mode racontent généralement toute l’histoire. Si c’est incorrect, ce sont des humains (ou des scripts d’initialisation) qui l’ont saboté. Les schémas courants :

  • Chemin hôte créé en root lors du provisioning, jamais chowné.
  • Le répertoire est inscriptible par le groupe, mais les fichiers n’héritent pas du groupe parce que setgid n’a pas été défini.
  • Umask trop strict (par ex. 0077) et les fichiers deviennent privés par défaut.
  • Le masque ACL restreint l’accès, même si des entrées ACL existent.

NFS: identity is political

Les problèmes de permissions NFS sont souvent des problèmes de mapping d’identité. Si vous utilisez AUTH_SYS (le classique « sec=sys »), le serveur fait confiance au client pour envoyer les UIDs. Cela signifie que la cohérence des identifiants numériques entre machines n’est pas optionnelle ; c’est tout le modèle de sécurité.

Si root squash est activé (souvent le cas par défaut), l’UID 0 sur le client est mappé à un utilisateur non privilégié sur le serveur. En monde conteneur, cela signifie que « exécuter en root » n’est pas le bouton facile que vous pensiez.

Conseil pratique : choisissez des comptes de service avec des UIDs fixes sur tous les nœuds, gérez-les centralement, et ne comptez pas sur le mapping par nom pour vous sauver. NFS ne se préoccupe pas de comment vous appelez l’utilisateur.

CIFS/SMB: permissions can be faked client-side

Les montages SMB sur Linux peuvent présenter une vue des permissions partiellement synthétique. Des options de montage comme uid=, gid=, file_mode=, et dir_mode= peuvent tout faire paraître inscriptible, jusqu’à ce que le serveur refuse via ses propres ACLs.

Mode de défaillance : Vous pensez l’avoir corrigé en changeant les options de montage, mais l’ACL serveur interdit encore les écritures. Ou vous « corrigez » en forçant la propriété, et vous cassez la traçabilité parce que chaque fichier appartient au même UID local.

ZFS: great at data, strict about ownership

ZFS n’est pas particulier vis‑à‑vis des permissions POSIX ; il est juste cohérent. Cette cohérence peut être brutale quand vous vous attendez à ce que « Docker s’en occupe ». Si vous prenez un snapshot et restaurez, les changements de propriété sont restaurés aussi. C’est une fonctionnalité, et aussi un piège pendant la réponse à incident.

overlay2: the illusion of writable layers

Le système de fichiers racine de votre conteneur est typiquement overlay2 : une union de layers image en lecture seule et d’un upper layer inscriptible. Les montages de volumes contournent cela. Donc si votre appli écrit bien dans /tmp mais échoue sur /data, c’est attendu : /tmp est dans la couche inscriptible du conteneur, tandis que /data est appliqué par le système de fichiers hôte que vous avez monté.

SELinux: permissions can be correct and still wrong

Sur systèmes SELinux, le label compte. Un répertoire bind-monté étiqueté default_t peut être illisible pour les conteneurs même si UID/GID sont parfaits. Docker prend en charge le relabeling avec des flags de montage :

  • :Z pour un label privé (exclusif à un conteneur)
  • :z pour un label partagé (plusieurs conteneurs)

Choisissez délibérément. Si vous partagez le même chemin hôte entre conteneurs et utilisez :Z, vous risquez de le relabeler dans un coin.

Trois mini-récits d’entreprise issus du terrain

Mini-story 1: The incident caused by a wrong assumption (“root can write anywhere”)

Une entreprise de taille moyenne exécutait un job ETL conteneurisé qui écrivait des fichiers parquet sur un export NFS partagé. Ça « tenait » depuis des mois, ce qui voulait surtout dire que personne n’y touchait. Puis la sécurité leur a demandé d’arrêter d’exécuter des conteneurs en root, et l’équipe a obtempéré en mettant user: "10001:10001" dans Compose.

La nuit suivante, le job ETL a échoué immédiatement : Permission denied sur le répertoire de sortie. L’ingénieur on-call a essayé le correctif classique : le relancer en root. Même erreur. C’est là que l’hypothèse est morte : « root dans le conteneur = root sur le stockage ». Ce n’était pas le cas. Le root squash NFS mappait le root client sur une identité non privilégiée côté serveur.

Ils ont ensuite essayé le correctif suivant classique : chmod 777 sur le point de montage client. Ça n’a rien changé. Parce que l’export côté serveur avait des ACLs qui interdisaient encore les écritures, et chmod côté client ne modifiait pas la politique serveur comme ils l’imaginaient.

La vraie correction était ennuyeuse : ils ont créé un compte de service dédié avec un UID/GID fixe, ont veillé à ce que ce compte existe avec les mêmes numéros sur tous les nœuds ETL, et ont réglé la propriété côté serveur en conséquence. Ils ont aussi ajouté une vérification de pré-démarrage simple : créer un fichier temporaire dans le répertoire cible avant d’exécuter le job coûteux.

Le postmortem était encore plus ennuyeux : ils ont mis à jour leurs runbooks pour traiter NFS comme un domaine de sécurité séparé avec ses propres règles. Ce paragraphe a évité aux prochains on-calls de refaire la même danse.

Mini-story 2: The optimization that backfired (recursive chown on startup)

Une autre équipe exécutait un service stateful en conteneurs avec un named volume. Un ingénieur a remarqué des erreurs de permission occasionnelles après des migrations et a décidé de « durcir » le démarrage : chaque boot de conteneur exécutait chown -R app:app /var/lib/app avant de lancer le daemon.

En dev, c’était parfait. Volume frais, peu de données, chown rapide, plus d’erreurs de permission. En production, le jeu de données était volumineux et sur des disques plus lents. Lors d’un déploiement routinier, le service a démarré lentement, puis plus lentement, puis semblait mort. Le health check a échoué. L’orchestrateur a redémarré. Ce qui a lancé un autre chown récursif. Ils se sont retrouvés dans une boucle de redémarrage effectuant des opérations lourdes sur les métadonnées du même arbre de répertoires.

Les graphiques de stockage ont montré la vérité : IOPS élevées, principalement des écritures de métadonnées, et des pics de latence affectant d’autres services. L’incident n’était pas causé par l’application. Il résultait d’une « optimisation » qui transformait chaque redémarrage en test de résistance du stockage.

Ils ont corrigé en retirant le chown au démarrage, en le remplaçant par un job d’initialisation unique qui ne s’exécute que lorsqu’un volume neuf est créé. Ils ont aussi changé leur stratégie de déploiement : ne pas redéployer toutes les instances à la fois, et ne pas redémarrer en boucle sans backoff.

La leçon : les corrections de permissions qui croissent linéairement avec la taille des données finiront par vous pousser à la page d’astreinte.

Mini-story 3: The boring but correct practice that saved the day (standard UIDs and a permissions contract)

Une grande équipe plateforme avait une règle : chaque conteneur stateful s’exécute avec un UID de service dédié, avec une allocation UID/GID gérée centralement. Les développeurs se plaignaient que c’était bureaucratique. Ils voulaient « juste utiliser 1000 ». L’équipe plateforme a refusé, poliment et fermement.

Quelques mois plus tard, un incident est survenu : un nœud a dû être reconstruit rapidement, et un service a été replanifié sur une machine fraîche. Le service est monté, a attaché son volume persistant et a commencé à écrire immédiatement. Pas d’erreurs de permission, pas de chown manuel, pas de panique.

Pourquoi ? Le mapping UID/GID était cohérent sur la flotte. La propriété du volume correspondait au compte de service partout. L’utilisateur runtime du conteneur était verrouillé. La structure des répertoires avait setgid et des ACLs par défaut là où le partage était nécessaire. Rien de brillant, rien d’excitant. Juste un contrat.

Lors du post-incident, quelqu’un a demandé si c’était de la chance. La réponse du lead SRE était essentielle : la chance c’est pour les billets de loterie ; nous avons des runbooks.

Cette équipe avait encore des incidents. Tout le monde en a. Mais ils n’avaient pas cette classe d’incident, ce qui vaut largement quelques décisions politiques prises tôt.

Erreurs courantes : symptôme → cause racine → correction

1) Symptom: works in container without volume, fails with bind mount

Cause racine : la propriété/mode du chemin hôte n’autorise pas l’UID du conteneur à écrire.

Correction : chown/chgrp le répertoire hôte pour correspondre à l’UID/GID du conteneur, ou utiliser un groupe partagé + setgid + group_add, ou utiliser des ACLs.

2) Symptom: works as root, fails as non-root

Cause racine : l’image a été conçue autour du root, ou le répertoire hôte est root-owned et non inscriptible par l’UID visé.

Correction : choisissez un UID/GID connu et définissez la propriété en conséquence ; préférez des images qui supportent proprement le non-root ; évitez les hacks de type sudo dans le conteneur.

3) Symptom: permissions look correct, still “Permission denied” on Fedora/RHEL

Cause racine : mismatch d’étiquetage SELinux pour le bind mount.

Correction : montez avec :Z/:z ou relabelez le chemin hôte de manière appropriée ; confirmez avec ls -Z et les logs audit.

4) Symptom: cannot write to NFS even as root on host

Cause racine : root squash NFS ou refus par ACL côté serveur.

Correction : corrigez les permissions/export côté serveur ; utilisez un UID de service dédié cohérent entre clients ; ne tentez pas de chmoder à travers la politique NFS.

5) Symptom: intermittent failures after deploy; sometimes fixed by restart

Cause racine : course entre scripts d’init et démarrage de l’appli, ou plusieurs conteneurs initialisant le même volume différemment.

Correction : isolez l’initialisation (job one‑time), forcez une propriété déterministe, et empêchez un comportement concurrent d’init sur plusieurs réplicas.

6) Symptom: volume directory is writable, but new files have wrong group

Cause racine : setgid manquant sur les répertoires ou ACL par défaut absente ; umask trop restrictive.

Correction : définissez le bit setgid sur les répertoires partagés ; appliquez des ACLs par défaut ; révisez umask et comportement de l’appli.

7) Symptom: changing perms on host doesn’t change what container sees

Cause racine : vous n’éditez pas le vrai chemin de backing (named volume vs bind mount), ou vous êtes sur SMB/NFS avec une politique serveur.

Correction : docker inspect sur la source du mount ; pour named volumes utilisez docker volume inspect ; pour stockage réseau, changez les règles côté serveur.

8) Symptom: after enabling userns-remap, everything “became” UID 165536

Cause racine : le mapping du user namespace fonctionne ; vos outils et attentes ne suivent pas.

Correction : planifiez les plages de mapping UID ; assurez-vous que les chemins hôtes et l’automatisation comprennent la propriété mappée ; évitez de bind-monter des répertoires gérés par des humains dans des conteneurs remappés.

Listes de contrôle / plan pas à pas

Checklist A: Stop the bleeding during an incident (10–15 minutes)

  1. Prouvez que c’est des permissions : touch dans le répertoire monté depuis le conteneur. Si ça échoue, poursuivez ; si ça réussit, c’est votre appli.
  2. Obtenez l’UID/GID effectif : id à l’intérieur du conteneur, pas depuis le Dockerfile.
  3. Confirmez le type de mount : docker inspect → est‑ce un bind ou un named volume ?
  4. Vérifiez propriété/mode/ACL sur l’hôte : ls -ldn et getfacl sur le chemin source.
  5. Vérifiez SELinux : getenforce et ls -Z sur le chemin hôte.
  6. Vérifiez le stockage réseau : mount sur l’hôte ; si NFS/CIFS, suspectez des règles côté serveur.
  7. Choisissez le correctif le moins dangereux : préférez l’écriture par groupe ou une ACL ciblée ; évitez 777 ; évitez chown récursif sur de grands arbres.

Checklist B: Make it not happen again (the production contract)

  1. Standardisez les UIDs/GIDs de service : allouez des IDs numériques fixes pour chaque service stateful à travers les environnements.
  2. Documentez le contrat du volume : « Ce chemin doit être inscriptible par UID X et/ou GID Y, avec setgid et ACL par défaut. » Mettez‑le dans le repo.
  3. Utilisez groupe + setgid pour les chemins partagés : c’est le modèle le plus simple et évolutif pour plusieurs rédacteurs.
  4. Gérez l’initialisation explicitement : job one‑time pour définir propriété/permissions sur les nouveaux volumes.
  5. Décidez de la politique SELinux : appliquez un étiquetage correct dans les manifests Compose/Kubernetes.
  6. Testez avec un preflight : CI ou check d’entrée (entrypoint) qui vérifie test -w sur les répertoires requis et échoue vite avec un message clair.
  7. Écartez les humains du chemin des données : si des opérateurs doivent toucher des fichiers, donnez‑leur l’accès de groupe ; ne « sudo éditez » pas des fichiers d’une manière qui change la propriété de façon imprévisible.

Checklist C: If you must use network storage

  1. NFS : assurez la cohérence UID/GID entre nœuds ; décidez du root squash de manière intentionnelle ; gérez les permissions côté serveur.
  2. CIFS : soyez explicite avec les options de montage ; comprenez si les permissions sont appliquées côté serveur ; n’ignorez pas que les bits de mode peuvent être synthétiques.
  3. Latence et coûts métadonnées : évitez les changements récursifs de permissions sur des arbres énormes ; planifiez le comportement de démarrage en conséquence.

FAQ

1) Why does the container user name not matter for volume permissions?

Parce que le noyau applique les permissions en utilisant des UID/GID numériques. Les noms ne sont que des entrées dans /etc/passwd (ou NSS). L’hôte voit les numéros.

2) Should I run containers as root to avoid permission issues?

Non. Vous transférez le risque de « permission denied » vers « compromission de l’hôte » et cela échoue toujours avec NFS root squash et la politique SELinux. Corrigez le mapping d’identités à la place.

3) What’s the best default for shared writable directories?

Créez un GID partagé, chgrp le répertoire, définissez g+rwX, mettez le bit setgid, et assurez‑vous que les conteneurs rejoignent ce GID. Ajoutez des ACLs par défaut si nécessaire.

4) Is chmod 777 ever acceptable?

Comme diagnostic à court terme pour prouver que c’est un problème de permissions, peut‑être. Comme solution, c’est négligent et souvent inutile. Préférez l’écriture par groupe ou les ACLs.

5) Why does it fail only on one host?

Généralement mismatch UID/GID (allocation d’utilisateurs différente), mode/labels SELinux différents, ou le « même chemin » pointe réellement vers un stockage différent (local vs NFS).

6) What’s the difference between named volumes and bind mounts for permissions?

Les bind mounts utilisent un chemin existant sur l’hôte (votre responsabilité). Les named volumes sont gérés sous le répertoire de données Docker et démarrent souvent appartenant à root à moins d’être initialisés.

7) How do I fix permissions for a named volume safely?

Exécutez un conteneur d’initialisation one‑time (ou un docker run temporaire) en root pour définir la propriété sur le volume, puis lancez le service principal en non-root.

8) Why do I see UID 165536 (or similar) on host files?

Vous avez probablement activé le remapping user namespace ou le mode rootless. Les UIDs du conteneur sont mappés dans une plage UID subordonnée sur l’hôte. C’est attendu.

9) Why do permissions look fine but writes still fail?

SELinux/AppArmor peut refuser l’accès même si les permissions POSIX l’autorisent. Sur systèmes SELinux, l’étiquetage est une cause fréquente pour les bind mounts.

10) Can I “fix” this by adding users to /etc/passwd inside the container?

Ajouter un nom aide les logs et outils, mais cela ne change pas l’UID numérique. La correction reste : faire correspondre les UIDs, utiliser groupe/ACLs, ou ajuster les mappings.

Conclusion : prochaines étapes réalisables dès aujourd’hui

Les problèmes de permissions sur volumes Docker sont prévisibles. C’est la bonne nouvelle. Ils surviennent quand des identités numériques et des couches de politique ne s’accordent pas. Votre objectif n’est pas de « chmoder jusqu’à ce que ça marche ». Votre objectif est de définir un contrat de permissions et de l’appliquer de façon cohérente.

Faites ceci ensuite :

  1. Choisissez une stratégie UID/GID par service : correspondance UID pour les cas simples, groupe partagé + setgid pour l’accès partagé, ACLs pour des rédacteurs mixtes.
  2. Supprimez le chown récursif du démarrage normal : remplacez‑le par une étape d’initialisation one‑time liée à la création du volume.
  3. Accélérez le diagnostic : intégrez des vérifications preflight (id, test -w) et documentez l’UID/GID attendu dans le repo.
  4. Gérez SELinux et le stockage réseau intentionnellement : labels et politiques côté serveur ne sont pas des « cas périphériques ». Ce sont de la production.

Une fois que vous traitez UID/GID comme partie de la spécification de déploiement, « Permission denied » cesse d’être un mystère et redevient un test unitaire que vous avez oublié d’écrire.

← Précédent
Pourquoi 640×480 semble éternel : des normes qui refusent de disparaître
Suivant →
Proxmox « impossible de résoudre l’hôte » : procédure rapide d’identification (DNS/IPv6/proxy)

Laisser un commentaire