Docker sur hôtes SELinux/AppArmor : les erreurs de permission que personne n’explique

Cet article vous a aidé ?

Vous avez fait le rituel : chmod -R 777, peut‑être un chown pour faire bonne mesure, redémarré le conteneur,
et il affiche toujours « permission denied ». Le chemin sur l’hôte est inscriptible. L’UID correspond. Le système de fichiers n’est pas en lecture seule.
Pourtant votre conteneur se comporte comme s’il était privé d’accès à sa propre maison.

C’est à ce moment que les ingénieurs commencent à blâmer Docker, puis Linux, puis la « sécurité », puis entre eux. La vérité est
ennuyeuse : vous êtes bloqué par un système de contrôle d’accès obligatoire (SELinux ou AppArmor), et il fait exactement ce
pour quoi il a été conçu. Le message d’erreur ne prend juste pas la peine de vous l’expliquer.

Pourquoi « permission denied » survit à chmod et chown

Les permissions Unix (propriétaire/groupe/bits de mode) sont un contrôle d’accès discrétionnaire. Elles sont locales et négociables.
Si vous possédez le fichier, vous pouvez accorder l’accès. Si vous êtes root, vous pouvez généralement forcer le passage. Les conteneurs ajoutent
des namespaces et des capabilities, mais au final, quand un processus tente d’ouvrir un fichier, c’est le noyau qui décide.

SELinux et AppArmor sont des contrôles d’accès obligatoires (MAC). Ce ne sont pas des « permissions de fichier ». Ce sont une couche
de politique supplémentaire qui peut refuser l’accès même lorsque les bits de mode l’autorisent. C’est la clé : le noyau peut renvoyer
EACCES pour des raisons qui n’ont rien à voir avec ls -l.

Voici le schéma :

  • Problème classique de permissions : mauvais UID/GID, mauvais mode, mauvaise ACL. Réglage avec chown/chmod/setfacl.
  • Problème SELinux : les labels (contexts) n’autorisent pas le domaine du conteneur à toucher ce chemin. Réparer par étiquetage, booleans ou politique.
  • Problème AppArmor : le profil du conteneur interdit un chemin, un montage, une capacité ou un syscall. Corriger le profil ou le changer.

Vous pouvez repérer les problèmes MAC parce que votre conteneur se comporte comme un cambrioleur bien élevé : il a les clés (UID/mode),
mais le système d’alarme appelle quand même la police (SELinux/AppArmor).

Blague n°1 : Si chmod 777 réglait tout, la sécurité serait un simple alias Bash et nous serions tous au chômage.

SELinux vs AppArmor : différentes approches, mêmes excommunications

SELinux en un paragraphe orienté production

SELinux est basé sur des labels. Tout a un contexte (user:role:type:level). La politique décide quels « domaines » (types de processus)
peuvent accéder à quels « types » (labels d’objets) avec quelles permissions. Les conteneurs tournent typiquement dans un domaine confiné
comme container_t, et les fichiers de l’hôte doivent être étiquetés de façon à ce que ce domaine soit autorisé à les toucher. Si vous
montez des chemins arbitraires de l’hôte dans un conteneur sans labels appropriés, SELinux bloquera l’accès même si le système de fichiers
dit « pas de problème ».

AppArmor en un paragraphe orienté production

AppArmor est fondé sur les chemins. Les profils définissent quels chemins un processus peut lire/écrire/exécuter, plus quelles capabilities et
interfaces noyau il peut utiliser. Docker applique souvent un profil par défaut (habituellement docker-default) sauf si vous le surchargez.
Si votre conteneur doit monter quelque chose, accéder à /sys d’une manière particulière, ou toucher un chemin hôte non prévu par le profil,
AppArmor peut le refuser.

Pourquoi vous êtes confus

Ils échouent de la même façon au niveau de l’application. Votre app voit « permission denied ». Vos logs montrent une trace de pile.
Votre équipe discute de la propriété des fichiers. Pendant ce temps, le vrai refus se trouve dans la piste d’audit du noyau, et votre conteneur
ne reçoit pas d’erreur plus claire parce que la couche VFS ne fait pas dans la thérapie.

Votre travail est de déterminer quel moteur de politique est en jeu, puis de lire les bons logs, puis de faire le plus petit changement sûr.
Le reste de ce guide suit ce chemin, avec moins de métaphores spirituelles.

Faits intéressants et courte histoire (pour que l’étrangeté ait du sens)

  • SELinux a commencé comme projet de recherche (initialement issu de la NSA) et a été conçu pour faire appliquer la politique même contre root. C’est pourquoi « mais je suis root » ne l’impressionne pas.
  • AppArmor a commencé comme SubDomain et est devenu AppArmor après acquisition et rebranding ; il a gagné en popularité car il est plus facile à raisonner si vous pensez en chemins de fichiers.
  • Docker n’a pas inventé la confinement ; il a réutilisé ce que le noyau offrait déjà : namespaces, cgroups, hooks LSM, seccomp. Docker assemble surtout tout ça et reçoit les reproches.
  • L’étiquetage des conteneurs SELinux a beaucoup évolué à mesure que les conteneurs sont devenus courants ; les premières pratiques étaient plus grossières, et les distributions modernes ont de meilleurs paramètres par défaut pour container_t et consorts.
  • Les options de montage :z et :Z existent parce que les bind mounts brisent l’hypothèse « les labels correspondent à l’intention » ; ces options relabelisent le contenu pour que les conteneurs y accèdent.
  • Les journaux d’audit ne sont pas des « logs debug » ; ce sont des événements de sécurité. Quand les équipes les centralisent correctement, les problèmes SELinux cessent d’être mystérieux.
  • Le modèle basé sur les chemins d’AppArmor peut être trompé par des jeux de renommage/liens si les profils ne sont pas soigneux ; le modèle de labels de SELinux évite certains de ces cas mais ajoute la charge opérationnelle de l’étiquetage.
  • Les systèmes de fichiers overlay ont changé l’histoire du stockage conteneur ; SELinux a dû apprendre à étiqueter correctement les couches overlay, et un mauvais étiquetage peut créer des échecs qui ressemblent à des bugs Docker.

Playbook de diagnostic rapide (quoi vérifier en premier/deuxième/troisième)

Premier : identifier quel système MAC est actif (et pour Docker, lequel compte)

  • Si SELinux est en enforcing et Docker l’utilise, commencez par les refus AVC.
  • Si AppArmor est activé et le conteneur tourne sous un profil, commencez par les refus AppArmor.
  • Si aucun n’est actif, revenez aux permissions classiques et aux user namespaces.

Deuxième : confirmer l’opération en échec et le chemin/périphérique hôte exact impliqué

  • Est‑ce un chemin de bind mount ? Un volume nommé ? Une socket (comme le socket Docker) ? Un nœud de périphérique ?
  • Le refus concerne‑t‑il la lecture, l’écriture, l’exécution, la création, le relabel, le montage, ou autre chose ?
  • Échoue‑t‑il seulement sur un hôte ? Si oui, suspectez des différences de politique, pas votre YAML.

Troisième : récupérez les preuves noyau/audit, pas des suppositions

  • SELinux : cherchez type=AVC dans les logs d’audit, puis interprétez contexts et permissions demandées.
  • AppArmor : cherchez apparmor="DENIED" et le nom du profil, puis mappez aux chemins/capacités.

Quatrième : choisissez la plus petite correction qui préserve le confinement

  • SELinux : utilisez des labels appropriés (:z/:Z, chcon, ou semanage fcontext), évitez de désactiver SELinux.
  • AppArmor : ajustez ou créez un profil, ou changez pour un profil non confiné seulement si vous comprenez le rayon d’impact.

Cinquième : validez avec un test répétable et laissez des traces

  • Reproduisez avec un conteneur minimal et un seul montage.
  • Documentez l’attente de label/profil dans les manifests Compose/Kubernetes et dans le provisioning de l’hôte.
  • Faites en sorte que la solution survive aux redémarrages et aux relabels.

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

Voici les commandes que j’utilise réellement lorsque qu’un conteneur ne peut pas lire/écrire un montage ou est bloqué en faisant
quelque chose « manifestement permis ». Chaque tâche inclut : commande, sortie d’exemple, ce que cela signifie, et la décision à prendre.

Task 1: Is SELinux enabled and enforcing?

cr0x@server:~$ getenforce
Enforcing

Signification : la politique SELinux refuse activement les actions non autorisées (pas seulement en journalisation).
Décision : Traitez « permission denied » comme potentiellement dû à SELinux jusqu’à preuve du contraire. Allez chercher les AVC.

Task 2: Quick SELinux status sanity check

cr0x@server:~$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Max kernel policy version:      33

Signification : SELinux est actif en mode targeted (commun sur Fedora/RHEL et dérivés).
Décision : Confirmez que les paquets de politique Docker/contener existent et que l’étiquetage host est correct.

Task 3: Is AppArmor enabled?

cr0x@server:~$ aa-status
apparmor module is loaded.
24 profiles are loaded.
22 profiles are in enforce mode.
2 profiles are in complain mode.
0 processes are unconfined but have a profile defined.

Signification : AppArmor applique activement des profils.
Décision : Si l’hôte est de type Ubuntu/Debian, AppArmor est un suspect de premier plan. Ensuite : vérifiez le profil du conteneur.

Task 4: Check what AppArmor profile Docker applied to a container

cr0x@server:~$ docker inspect --format '{{.Name}} -> AppArmor={{.AppArmorProfile}}' web-1
/web-1 -> AppArmor=docker-default

Signification : Le conteneur est confiné sous docker-default.
Décision : Si vous voyez des refus AppArmor, vous modifierez/remplacerez ce profil ou le surchargerez par conteneur.

Task 5: Confirm the exact mount and where Docker thinks it is

cr0x@server:~$ docker inspect --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' web-1
bind /srv/web/data -> /var/lib/app/data
volume web-cache -> /var/cache/app

Signification : Vous avez à la fois un bind mount et un volume nommé. Le comportement SELinux/AppArmor diffère entre eux.
Décision : Concentrez-vous d’abord sur le bind mount ; c’est souvent là que l’étiquetage/la politique pose problème.

Task 6: Reproduce with a minimal write test inside the container

cr0x@server:~$ docker exec -it web-1 sh -lc 'id; touch /var/lib/app/data/.probe && echo ok'
uid=1000(app) gid=1000(app) groups=1000(app)
touch: cannot touch '/var/lib/app/data/.probe': Permission denied

Signification : L’utilisateur de l’application ne peut pas créer un fichier dans le répertoire monté.
Décision : Si les permissions sur l’hôte semblent correctes, passez immédiatement aux preuves MAC (AVC/AppArmor).

Task 7: Check classic host permissions anyway (because humans)

cr0x@server:~$ ls -ldn /srv/web/data
drwxrwx--- 5 1000 1000 4096 Jan  3 10:12 /srv/web/data

Signification : Le mode et la propriété correspondent à l’utilisateur du conteneur (UID/GID 1000).
Décision : Arrêtez de discuter chown. C’est presque certainement SELinux/AppArmor (ou userns/shiftfs, mais commencez par le MAC).

Task 8: On SELinux hosts, inspect the context of the host directory

cr0x@server:~$ ls -ldZ /srv/web/data
drwxrwx---. 5 1000 1000 unconfined_u:object_r:default_t:s0 /srv/web/data

Signification : Le répertoire est étiqueté default_t, le label « je ne sais pas ce que c’est ». Les conteneurs ne peuvent généralement pas le toucher.
Décision : Relabelisez le répertoire pour l’accès conteneur (idéalement de façon persistante). Ne désactivez pas SELinux.

Task 9: Find the SELinux denial in audit logs (the smoking gun)

cr0x@server:~$ sudo ausearch -m avc -ts recent | tail -n 5
type=AVC msg=audit(1704286512.911:412): avc:  denied  { create } for  pid=23841 comm="touch" name=".probe" scontext=system_u:system_r:container_t:s0:c123,c456 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0

Signification : Le domaine du processus est container_t ; la cible est default_t ; la permission refusée est create.
Décision : Corrigez l’étiquetage de /srv/web/data (contexte cible), pas les UID du conteneur.

Task 10: Quick temporary relabel using Docker mount flags (:z vs :Z)

cr0x@server:~$ docker run --rm -v /srv/web/data:/data:Z alpine sh -lc 'touch /data/ok && ls -l /data/ok'
-rw-r--r--    1 root     root             0 Jan  3 10:21 /data/ok

Signification : Le relabel a fonctionné ; le conteneur peut écrire maintenant.
Décision : Choisissez entre :Z (label privé pour un conteneur) et :z (label partageable) selon que plusieurs conteneurs ont besoin du même chemin.

Task 11: Make SELinux labeling persistent with semanage fcontext

cr0x@server:~$ sudo semanage fcontext -a -t container_file_t "/srv/web/data(/.*)?"
cr0x@server:~$ sudo restorecon -Rv /srv/web/data
restorecon reset /srv/web/data context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:container_file_t:s0
restorecon reset /srv/web/data/ok context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:container_file_t:s0

Signification : Vous avez déclaré la correspondance de label attendue et l’avez appliquée. Elle survivra aux opérations de relabel.
Décision : Préférez cela pour des serveurs gérés. Utilisez chcon seulement pour des tests rapides.

Task 12: Confirm the new SELinux label

cr0x@server:~$ ls -ldZ /srv/web/data
drwxrwx---. 5 1000 1000 unconfined_u:object_r:container_file_t:s0 /srv/web/data

Signification : Le répertoire est maintenant étiqueté pour être accessible aux conteneurs.
Décision : Retestez l’écriture du conteneur. Si cela échoue encore, inspectez d’autres refus : peut‑être un sous‑chemin, une socket, ou une autre classe.

Task 13: On AppArmor hosts, find the denial in kernel logs

cr0x@server:~$ sudo journalctl -k -g 'apparmor="DENIED"' -n 5
Jan 03 10:24:11 server kernel: audit: type=1400 audit(1704287051.112:96): apparmor="DENIED" operation="open" profile="docker-default" name="/srv/web/data/.probe" pid=24910 comm="touch" requested_mask="wc" denied_mask="wc" fsuid=1000 ouid=1000

Signification : Le profil AppArmor docker-default a refusé l’écriture/la création (wc) sur un chemin hôte.
Décision : Soit modifiez le profil pour autoriser ce chemin, soit choisissez une autre approche (volume nommé, montage différent, ou override du profil).

Task 14: Identify the container’s init process label (SELinux) or profile (AppArmor) from inside

cr0x@server:~$ docker exec -it web-1 sh -lc 'cat /proc/1/attr/current 2>/dev/null || true; cat /proc/1/attr/apparmor/current 2>/dev/null || true'
system_u:system_r:container_t:s0:c123,c456
docker-default (enforce)

Signification : Vous voyez des métadonnées de confinement fournies par le noyau. (Sur certains hôtes vous verrez l’un ou l’autre.)
Décision : Si SELinux indique container_t, concentrez‑vous sur les labels. Si AppArmor affiche un profil, concentrez‑vous sur les règles du profil.

Task 15: Check Docker daemon security options (SELinux/AppArmor/seccomp)

cr0x@server:~$ docker info --format '{{json .SecurityOptions}}'
["name=seccomp,profile=builtin","name=selinux","name=apparmor"]

Signification : Docker connaît SELinux et AppArmor ; les deux sont en jeu sur cet hôte.
Décision : N’assumez pas « nous sommes une distro AppArmor » ou « nous sommes SELinux ». Votre parc peut être mixte.

Task 16: Check if a named volume avoids the bind mount problem

cr0x@server:~$ docker run --rm -v web-cache:/cache alpine sh -lc 'touch /cache/ok && ls -l /cache/ok'
-rw-r--r--    1 root     root             0 Jan  3 10:28 /cache/ok

Signification : Le volume nommé fonctionne car Docker le crée sous un emplacement déjà étiqueté/autorisée pour les conteneurs.
Décision : Préférez les volumes nommés sauf si vous avez vraiment besoin des sémantiques d’un chemin hôte (sauvegardes, audits, outils partagés).

Task 17: Verify mount options and filesystem type (NFS and friends get spicy)

cr0x@server:~$ findmnt -T /srv/web/data
TARGET SOURCE              FSTYPE OPTIONS
/      /dev/mapper/rootvg ext4   rw,relatime,seclabel

Signification : Le système de fichiers supporte les labels SELinux (seclabel). Bien.
Décision : Si vous ne voyez pas seclabel (ou si vous êtes sur NFS/CIFS), prévoyez un traitement spécial et testez soigneusement.

Task 18: For overlay2 weirdness, check the container’s merged directory label (SELinux hosts)

cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' web-1)
cr0x@server:~$ sudo ls -ldZ /var/lib/docker/overlay2/*/merged 2>/dev/null | head -n 2
drwxr-xr-x. 1 root root system_u:object_r:container_file_t:s0:c87,c912 4096 Jan  3 10:30 /var/lib/docker/overlay2/3a3d.../merged
drwxr-xr-x. 1 root root system_u:object_r:container_file_t:s0:c21,c333 4096 Jan  3 10:30 /var/lib/docker/overlay2/9b71.../merged

Signification : Les répertoires merged overlay ont des labels conteneur avec catégories MCS. C’est attendu sur les hôtes SELinux.
Décision : Si ces labels sont erronés ou manquants, vous êtes dans un cas « mismatch daemon/storage driver/policy », pas dans du « chmod ».

Bind mounts, volumes et étiquetage : la mécanique réelle

Pourquoi les bind mounts sont l’endroit où les bonnes intentions meurent

Un volume Docker nommé est créé sous les répertoires gérés par Docker, avec le bon contexte de sécurité par défaut (SELinux)
ou avec des chemins déjà autorisés (AppArmor), selon les choix de la distribution. Un bind mount est un chemin hôte brut que vous avez choisi.
Le noyau ne se soucie pas que vous l’ayez choisi « pour le conteneur ». C’est juste un chemin hôte avec les labels/implications de profil qu’il a déjà.

C’est la distinction opérationnelle centrale : les volumes ont tendance à « juste fonctionner », les bind mounts échouent souvent de façon
déroutante parce qu’ils traversent des domaines de sécurité.

SELinux : le label est la permission, pas le mode

Avec SELinux, votre processus conteneur s’exécute dans un domaine comme container_t plus un ensemble de catégories MCS.
Les fichiers sous /var/lib/docker (ou la racine de stockage conteneur) sont étiquetés pour correspondre à ce domaine et incluent souvent
des catégories correspondantes. Votre chemin hôte aléatoire sous /srv pourrait être default_t ou
var_t ou un type d’application quelconque. La politique peut interdire l’accès des conteneurs.

La correction n’est pas « le rendre inscriptible pour tous ». La correction est « appliquer un type SELinux que le domaine conteneur est autorisé à utiliser ».
Approches communes :

  • Utiliser les labels de montage Docker : :z pour le contenu partagé, :Z pour le contenu privé.
  • Définir des labels persistants : semanage fcontext + restorecon, en utilisant container_file_t.
  • Utiliser les outils container-selinux (varie selon la distribution) pour garder la politique alignée avec le comportement Docker.

Ce que font réellement :z et :Z (et pourquoi ça surprend)

Les options :z/:Z demandent à Docker de relabeliser le chemin source pour que le conteneur puisse y accéder.
:Z donne typiquement au contenu un label privé (incluant des catégories) pour un seul conteneur. :z
le rend partageable entre conteneurs. Si vous mettez :Z sur un chemin utilisé par plusieurs conteneurs, l’un d’eux
pourra soudainement perdre l’accès. Ce n’est pas Docker qui est capricieux ; c’est vous qui demandez des labels privés sur du contenu partagé.

Un autre angle aigu : relabeliser un répertoire hôte peut avoir des effets secondaires pour des processus non conteneur. Les labels SELinux
sont une vérité système, pas un souhait par conteneur. Si ce répertoire est utilisé par un autre service avec des attentes strictes,
vous pouvez le casser.

AppArmor : chemins et capabilities sont le champ de bataille

Les refus AppArmor ressemblent souvent à :

  • Impossible d’écrire sur un chemin monté (le profil ne l’autorise pas).
  • Impossible de mount à l’intérieur du conteneur (capability refusée ou règle de montage refusée).
  • Impossible d’accéder à /proc ou à des fonctionnalités de /sys attendues par l’application.
  • Impossible d’utiliser des opérations privilégiées même en root dans le conteneur (capabilities filtrées).

La correction consiste généralement à ajuster un profil (autoriser le chemin spécifique) ou à exécuter avec un profil différent.
La solution paresseuse est « unconfined », ce qui peut être acceptable pour une fenêtre de dépannage contrôlée et presque jamais acceptable
comme état permanent en production.

Un mot sur NFS, CIFS, FUSE et autres joies réseau

Les systèmes de fichiers réseau compliquent la donne :

  • Certains montages ne supportent pas les labels SELinux comme les systèmes locaux, ou nécessitent des options de montage explicites.
  • Le root-squash sur NFS peut transformer le « root du conteneur » en « nobody », et vous allez chasser le mauvais coupable.
  • AppArmor peut toujours refuser des chemins indépendamment du système de fichiers sous-jacent.

Quand des conteneurs ne peuvent pas écrire sur du NFS, vérifiez à la fois MAC et règles d’export NFS. C’est rarement une seule cause.

Profils AppArmor : ce que fait Docker, ce que fait votre distro

docker-default est un compromis, pas une promesse

Le profil AppArmor par défaut de Docker vise à bloquer quelques opérations manifestement dangereuses tout en laissant la plupart
des conteneurs fonctionner. Il n’est pas adapté à votre application, vos montages ou votre régime de conformité. C’est une ceinture de sécurité générique.
Utile, mais pas sur mesure.

Quand vous rencontrez un refus AppArmor, vous avez des choix :

  • Changer la charge de travail : privilégier les volumes nommés ; éviter les montages étranges ; réduire le comportement privilégié.
  • Changer le profil : autoriser les chemins et opérations exacts nécessaires.
  • Changer la configuration du conteneur : surcharger le profil pour ce conteneur.

Diagnostiquer AppArmor : ce que le refus vous dit

La ligne de refus inclut généralement :

  • profile= quel profil a bloqué
  • operation= open/mount/ptrace/etc.
  • name= le chemin
  • requested_mask / denied_mask quel type d’accès a été tenté

Traitez‑le comme un puzzle ciblé. S’il refuse l’écriture sur un chemin hôte spécifique, soit ne montez pas ce chemin, soit autorisez‑le explicitement.
S’il refuse mount/capabilities, demandez‑vous si vous en avez vraiment besoin. La plupart des apps n’en ont pas besoin.

Overrider AppArmor : utilisez‑le comme un scalpel

Docker permet de surcharger AppArmor par conteneur via des security opts. Cela peut sauver la mise pour une image fournisseur ponctuelle qui
a besoin d’une capability spécifique, mais c’est aussi la façon dont des personnes finissent par exécuter la production sans MAC.

La règle opérationnelle : si vous surchargez des profils, intégrez cette décision dans le code d’infrastructure et évaluez le risque.
Ne laissez pas cela devenir un contournement tribal qui vit dans un wiki que personne ne lit.

Blague n°2 : AppArmor, c’est comme une politique de voyage d’entreprise — votre voyage est « approuvé » jusqu’à ce que vous essayiez de rembourser le taxi.

Trois mini-récits d’entreprise depuis le terrain

Incident 1 : la mauvaise hypothèse (« les permissions, ce sont les permissions »)

Une entreprise de taille moyenne a migré plusieurs services de VM vers des conteneurs. Ils avaient soigné le mapping UID/GID et
standardisé l’exécution des apps non‑root. Tout avait l’air propre. Puis un service a commencé à échouer uniquement sur les hôtes
RHEL récents, alors qu’il fonctionnait sur l’ancien parc Ubuntu.

L’astreinte a fait le classique : vérifié la propriété, exécuté chmod, redéployé. Le service ne pouvait toujours pas écrire
dans son répertoire de données. Au bout de deux heures, quelqu’un a suggéré « peut‑être SELinux », proposition accueillie par le silence
qu’on réserve à « peut‑être c’est le DNS ».

Ils ont trouvé des AVC : le répertoire bind‑monté était étiqueté default_t. Le domaine conteneur était
container_t. SELinux faisait son travail. L’hypothèse erronée était que les permissions de fichiers faisaient tout, donc ils
tournaient la mauvaise molette.

La correction fut simple : définir des règles fcontext persistantes pour les chemins hôtes de l’application et les appliquer avec
restorecon. L’amélioration réelle fut procédurale : l’équipe a ajouté une étape de validation hôte dans le provisioning
qui vérifie la justesse des labels pour les bind mounts connus.

L’action post‑incident fut franche : « Arrêtez d’utiliser chmod comme stratégie de debugging. » Ils l’ont imprimé sur un autocollant
et collé sur un portable. Ce fut seulement modérément efficace, mais le moral s’est amélioré.

Incident 2 : une optimisation qui s’est retournée contre eux (bind mounts partagés + :Z)

Une autre organisation faisait tourner un cluster de conteneurs partageant un répertoire hôte pour des assets générés. Pensez vignettes,
bundles front‑end compilés, ce genre. Ils voulaient des déploiements plus rapides et moins d’artefacts dupliqués, ils ont donc bind‑monté
le même chemin hôte dans plusieurs conteneurs sur un nœud et appelé ça « efficace ».

Un ingénieur soucieux de la sécurité remarqua des refus SELinux occasionnels et décida de « corriger proprement » en ajoutant
:Z au montage dans le fichier Compose. Ça fonctionnait en développement : un conteneur, un répertoire, pas de problème.
Ils ont déployé en production.

La production est devenue étrange. La moitié des conteneurs pouvait écrire, l’autre non, et quels conteneurs changeait après les redémarrages.
Parfois un déploiement « corrigeait » le problème quand un conteneur atterrissait sur un autre nœud. L’équipe a passé une journée à suspecter
le backend de stockage, puis Docker, puis l’application.

La cause racine : :Z applique un label privé destiné à un seul conteneur avec des catégories MCS. Quand plusieurs conteneurs
partagent le chemin, le label privé correspond aux catégories d’un conteneur et pas aux autres. Ce n’est pas déterministe après redémarrage
car les catégories peuvent varier.

Ils sont passés à :z pour le contenu réellement partagé, et pour un sous‑ensemble de cas, ont migré vers des volumes nommés pour éviter
le couplage au chemin hôte. L’« optimisation » ne s’est pas seulement retournée : elle a créé un mode d’échec qui ressemblait à un I/O flaky
et a fait perdre beaucoup de temps aux seniors. La leçon : en territoire SELinux, le partage est une décision de politique, pas une commodité de montage.

Story 3 : pratique ennuyeuse mais correcte qui a sauvé la mise (logs d’audit + runbooks)

Une grande entreprise avait un parc mixte : quelques hôtes avec SELinux enforcing, d’autres avec AppArmor, quelques machines durcies avec les deux,
et un flux constant d’images fournisseurs qui supposaient pouvoir tout faire.

Ils ont investi dans une pratique profondément non sexy : centraliser les logs d’audit et apprendre aux ingénieurs à les lire. Pas une case de conformité :
une capacité opérationnelle. Ils avaient un runbook : « permission denied dans un conteneur » correspond à « vérifier les montages Docker, puis SELinux/AppArmor,
puis UID/GID ». Tout le monde suivait les mêmes étapes.

Une nuit, un service lié au stockage a échoué après une vague de patching des hôtes. Les logs applicatifs n’indiquaient rien d’utile.
Les logs conteneur montraient « permission denied » sur un chemin de données. L’astreinte a suivi le runbook, trouvé des AVC,
et vu que le chemin cible avait été mal étiqueté après une migration de système de fichiers.

Comme ils avaient des règles fcontext persistantes vérifiées dans leur configuration d’hôte, la correction fut un restorecon
et un rollback d’un montage mal appliqué. Service restauré rapidement. Pas d’héroïsme. Pas de « mettez SELinux en permissive temporairement ».
Juste un système qui se comportait de façon prévisible.

Ce qui a sauvé la mise n’était pas le génie. C’était la répétabilité. L’équipe ne « se rappelait » pas SELinux ; leurs outils et runbooks s’en souvenaient pour eux.

Erreurs courantes : symptôme → cause racine → correction

1) Symptom: container can’t write to bind mount, but host permissions are correct

Cause racine : Le contexte SELinux du répertoire hôte n’est pas accessible aux conteneurs (souvent default_t).

Correction : Utilisez :z/:Z sur le montage, ou définissez des labels persistants avec
semanage fcontext -a -t container_file_t et restorecon.

2) Symptom: works on Ubuntu host, fails on RHEL/Fedora host

Cause racine : SELinux en enforcing sur une classe d’hôtes, AppArmor (ou rien) sur l’autre. Vos manifests supposent une baseline erronée.

Correction : Détectez et codifiez la configuration de sécurité des hôtes ; ajoutez l’étiquetage SELinux aux bind mounts sur les hôtes SELinux.

3) Symptom: only one of several containers can write to a shared directory

Cause racine : Utilisation de :Z (label privé) pour du contenu partagé ; les catégories MCS ne correspondent pas entre conteneurs.

Correction : Utilisez :z pour les montages partagés, ou cessez de partager et utilisez des chemins/volumes par conteneur.

4) Symptom: “permission denied” when accessing a UNIX socket (e.g., /var/run/…)

Cause racine : Le type SELinux sur la socket interdit le domaine conteneur, ou le profil AppArmor bloque le chemin.

Correction : Évitez de monter des sockets hôtes privilégiées quand possible ; sinon étiquetez/autorisiez la socket spécifiquement. Envisagez un service proxy plutôt que des mounts de socket directs.

5) Symptom: container fails when trying to mount or use FUSE

Cause racine : AppArmor refuse les opérations de montage ou les capabilities requises ; SELinux peut refuser l’accès aux périphériques.

Correction : Réévaluez la nécessité. Si c’est requis, créez un profil AppArmor adapté et autorisez explicitement les opérations nécessaires ; évitez le « unconfined » global.

6) Symptom: after reboot or relabel, the problem returns

Cause racine : Vous avez utilisé chcon ou le relabel Docker comme correctif ad‑hoc sans règles fcontext persistantes ; ou le provisioning recrée des répertoires avec des labels par défaut.

Correction : Utilisez semanage fcontext + restorecon, et assurez la création des répertoires via la gestion de configuration.

7) Symptom: you set SELinux to permissive and everything “works”

Cause racine : Vous avez prouvé que c’était SELinux, puis vous vous êtes arrêté à la solution la plus coûteuse.

Correction : Remettez SELinux en enforcing et corrigez labels/policy. Permissive sert pour des fenêtres de diagnostic, pas pour le confort en production.

8) Symptom: named volume works, bind mount fails, same path inside container

Cause racine : Le chemin géré par Docker pour le volume est déjà étiqueté/autorisée ; le bind mount arbitraire ne l’est pas.

Correction : Préférez les volumes nommés. Utilisez les bind mounts seulement quand l’intégration hôte est requise, et alors étiquetez/autorisiez correctement.

Listes de contrôle / plan pas à pas

Checklist A: when a container can’t write to a mount

  1. Identifiez le type de montage et le chemin hôte : docker inspect mounts.
  2. Reproduisez avec un simple touch dans le conteneur pour isoler la logique applicative des permissions.
  3. Vérifiez rapidement mode/propriétaire sur l’hôte (ls -ldn) pour écarter un mismatch d’UID évident.
  4. Vérifiez l’état SELinux (getenforce) et le label (ls -ldZ) si en enforcing.
  5. Cherchez des refus AVC (ausearch -m avc) correspondant au chemin et à l’opération.
  6. Si AppArmor est actif, cherchez dans les logs noyau apparmor="DENIED" et identifiez le profil.
  7. Appliquez la plus petite correction : ajustement de label SELinux ou changement de profil AppArmor.
  8. Re‑testez avec la sonde d’écriture minimale, puis avec l’application réelle.
  9. Rendez cela persistant : semanage fcontext/restorecon ou déploiement de profil via gestion de configuration.
  10. Notez l’attente dans le manifest du service (« ce bind mount nécessite container_file_t »).

Checklist B: hardening without breaking everything

  1. Privilégiez les volumes nommés pour l’état applicatif sauf si vous avez besoin d’un chemin hôte.
  2. Standardisez sur un petit ensemble de racines de bind mount (/srv/containers/<app>) et étiquetez‑les de façon cohérente.
  3. Centralisez les logs d’audit et noyau ; alertez sur des rafales de refus AVC/AppArmor.
  4. Gardez SELinux en enforcing et AppArmor en enforcing ; traitez les exceptions via contrôle de changement.
  5. N’abusez pas de --privileged. Si vous avez besoin d’une capability, ajoutez‑la seule.
  6. En CI, exécutez une « sonde de permission » contre les montages attendus pour détecter tôt la dérive d’étiquetage/profil.

Checklist C: migration day (when you move hosts or storage)

  1. Avant de déplacer des répertoires de données, enregistrez les contexts : ls -lZ sur l’ancien chemin.
  2. Après migration, appliquez les règles fcontext et restorecon sur le nouveau chemin.
  3. Vérifiez avec un conteneur minimal qui fait des opérations create/read/delete.
  4. Puis seulement basculez le trafic production. Ce n’est pas de la superstition ; c’est éviter une surprise à 2h du matin.

Une citation qui a bien vieilli en opérations : « L’espoir n’est pas une stratégie. » — General Gordon R. Sullivan

FAQ

1) Why does Docker show “permission denied” instead of “SELinux denied”?

Parce que le noyau renvoie une erreur d’accès générique au syscall. L’application et Docker voient EACCES. Les
détails réels se trouvent dans les logs SELinux/AppArmor, pas dans le message d’erreur de l’application.

2) Should I disable SELinux or AppArmor to make containers work?

Non, pas comme solution permanente. Utilisez permissive/complain brièvement pour confirmer la suspicion, puis corrigez les règles
d’étiquetage/profil. Désactiver le MAC transforme des contournements subtils en incidents bruyants plus tard.

3) What’s the difference between :z and :Z on Docker mounts?

Sur hôtes SELinux, elles déclenchent une relabelisation. :z est prévu pour du contenu partagé (plusieurs conteneurs).
:Z est prévu pour du contenu privé (un seul conteneur). Utiliser :Z sur des répertoires partagés provoque des
échecs cross‑conteneurs.

4) I used chcon and it worked. Why did it break later?

chcon change les labels mais ne les rend pas persistants face à des opérations de relabel ou certains flux de provisioning.
Utilisez semanage fcontext pour définir une règle, puis appliquez‑la avec restorecon.

5) Why does a named Docker volume work when a bind mount fails?

Les volumes nommés vivent sous les répertoires de stockage Docker et héritent de labels/chemins que les politiques attendent déjà pour
l’usage conteneur. Les bind mounts héritent du label/de la politique que le chemin hôte possède déjà.

6) Can I run containers unconfined under AppArmor?

Vous pouvez, mais traitez‑le comme exécution sans protections. Parfois c’est un geste diagnostic temporaire ou une exception contrôlée.
Ce ne doit pas être le comportement par défaut.

7) Why does this show up only after a host patch or OS upgrade?

Les paquets de politique, les profils par défaut et le comportement d’étiquetage peuvent changer avec les mises à jour. De plus, les migrations
de stockage et la recréation de répertoires peuvent réinitialiser les contexts aux valeurs par défaut. La solution est de codifier labels/profils,
pas de compter sur « ce que fait l’OS ».

8) Is this the same problem as user namespace remapping or rootless Docker?

C’est une couche différente. userns/rootless affecte le mapping UID/GID et les frontières de capabilities. SELinux/AppArmor sont des couches de politique LSM.
Vous pouvez avoir les deux problèmes en même temps, ce qui explique pourquoi il faut vérifier les preuves (logs d’audit) plutôt que deviner.

9) How do I decide between changing policy and changing the container design?

Si vous creusez sans cesse des trous dans la politique MAC par commodité, repensez le design. Préférez les volumes nommés, évitez les sockets hôtes,
réduisez les privilèges. Si vous avez un besoin d’intégration légitime (p.ex. répertoire hôte partagé pour sauvegardes), faites un changement de politique précis
et documentez‑le.

Conclusion : prochaines étapes pratiques

Les erreurs de permission que personne n’explique sont rarement mystérieuses. Elles sont juste mal rapportées. SELinux et AppArmor se placent
sous votre runtime conteneur et ne négocient pas avec votre chmod. Ils appliquent des politiques, et les détails sont dans les pistes
d’audit et les logs noyau.

Prochaines étapes que vous pouvez faire cette semaine, sans déclencher une guerre sainte :

  1. Enseignez à votre runbook d’astreinte de vérifier SELinux/AppArmor avant de toucher aux modes de fichiers.
  2. Standardisez les racines de bind mount et appliquez des règles fcontext SELinux persistantes quand pertinent.
  3. Préférez les volumes nommés sauf si les sémantiques d’un chemin hôte sont réellement nécessaires.
  4. Centralisez les logs d’audit et de refus noyau pour que « permission denied » devienne un diagnostic de deux minutes, pas une dispute de deux heures.
  5. Quand vous devez outrepasser le confinement, faites‑le intentionnellement, de façon minimale et en code — jamais comme un fossile d’urgence qui reste perpétuel.

Les conteneurs sont déjà compliqués. Ne les rendez pas effrayants. Rendez‑les observables, correctement étiquetés et ennuyeux.
L’ennui, c’est ce qui reste en production.

← Précédent
Formatage des enregistrements SPF : guillemets et espaces qui cassent discrètement les e-mails
Suivant →
Importer une VM ESXi dans Proxmox : Windows/Linux, pilotes VirtIO, mappage des NIC, corrections de démarrage

Laisser un commentaire