Configuration de sécurité Docker : guide complet pour éviter d'exécuter les conteneurs en root

Le mois dernier, en auditant la config conteneurs d’une entreprise, un simple docker inspect — tout tournait en root, avec --privileged. Deux semaines plus tard, le conteneur était compromis ; l’attaquant a échappé au conteneur et pris le contrôle de l’hôte.
Ce n’est pas de l’alarmisme. CVE-2024-21626, révélée en janvier 2024, en est un exemple : l’attaquant n’a qu’à contrôler le paramètre « working directory » du conteneur pour exploiter un descripteur de fichier fuité et manipuler tout le système de fichiers de l’hôte. Des données plus inquiétantes viennent de NSFOCUS — 76 % des images sur Docker Hub contiennent des vulnérabilités, 67 % des vulnérabilités critiques.
Avant, on ne s’en préoccupait guère — Dockerfile avec FROM ubuntu, puis RUN apt-get install, « c’est isolé dans le conteneur, non ? » Jusqu’à ce qu’un conteneur de préproduction soit infiltré ; dans les logs, la commande de montage du disque hôte par l’attaquant — frissons garantis.
Passer d’un conteneur root à un utilisateur non-root n’est pas si compliqué. Aujourd’hui : pourquoi le root par défaut est dangereux, comment créer un utilisateur non-root dans le Dockerfile, utiliser --user, et configurer Capabilities et AppArmor. Après cette lecture, vous pourrez réduire d’environ 80 % le risque de sécurité de vos conteneurs en production.
Pourquoi ne pas exécuter les conteneurs en root ?
Évasion de conteneur : du bac à sable à l’hôte en une étape
Beaucoup pensent conteneur = bac à sable, sans impact sur l’hôte. En réalité, l’isolation repose sur les namespaces et cgroups Linux, pas sur une isolation matérielle comme une VM. Mauvaise config ou vulnérabilité noyau, et cette barrière s’effondre.
CVE-2024-21626 en est la preuve. Les chercheurs ont montré que runc (runtime sous-jacent de Docker), en gérant le répertoire de travail, fuit un descripteur pointant vers le système de fichiers hôte. Concrètement : l’attaquant peut lire/écrire n’importe quel fichier hôte, y compris écraser /usr/bin/bash. Votre conteneur Web compromis, puis remplacement de tous vos conteneurs par des mineurs — ce n’est pas une hypothèse, c’est déjà arrivé.
Une voie d’attaque fréquente : le mode --privileged. Ce paramètre dit à Docker : « donne au conteneur tous les privilèges de l’hôte. » Le root du conteneur obtient mount de périphériques, chargement de modules noyau, modification réseau… Un rapport NSFOCUS analyse un cas où l’attaquant, via un conteneur privilégié, exécute mount /dev/sda1 /mnt, ajoute un cron et exfiltre les données — moins de dix minutes.
Un risque discret : monter le socket Docker. Pour lancer Docker dans Docker, on monte /var/run/docker.sock — clé de contrôle de tous les conteneurs hôte. L’attaquant peut créer un conteneur privilégié puis s’échapper. L’équipe sécurité Tencent Cloud a documenté cette chaîne.
Pourquoi root est la plus grande brèche ?
Le cœur du problème : le root UID 0 du conteneur et le root UID 0 de l’hôte sont le même utilisateur.
Namespace UID ? Oui, mais désactivé par défaut (compatibilité). Processus root dans le conteneur + faille noyau ou namespace invalidé = root sur l’hôte. Test personnel : conteneur avec SYS_ADMIN, root qui monte procfs hôte, écriture dans /proc/sys/kernel/core_pattern pour un reverse shell — root hôte obtenu, plus simple que prévu.
Le rapport sécurité Alibaba cite cinq causes majeures d’évasion : vulnérabilités noyau, mauvaise configuration, images non sécurisées, abus de privilèges, communication inter-conteneurs non sécurisée. Les quatre premières liées au root. Utilisateur non-root : trois risques fortement réduits.
Cas réel : applications Node.js sur le port 80 — ports < 1024 exigent root, donc root partout. Une faille path traversal dans Express, lecture de /etc/passwd, tentative SSH sur l’hôte — pas besoin d’évasion.
Effrayant ? La solution est simple : principe du moindre privilège. Donnez uniquement ce dont l’application a besoin, pas root par défaut.
Configurer un utilisateur non-root : partir du Dockerfile
Configuration sécurité utilisateur non-root Docker
Guide complet de la création d’un utilisateur non-root dans le Dockerfile aux paramètres runtime, pour réduire le risque d’environ 80 %
Estimated time: PT30M
-
1
Step 1: Créer un utilisateur non-root dans le Dockerfile
Créer un utilisateur et un groupe dédiés : -
2
Step 2: Gérer la liaison de ports
Gérer la liaison de ports : -
3
Step 3: Paramètres runtime
Paramètres runtime : -
4
Step 4: Contrôle fin des Capabilities
Contrôle fin des Capabilities : -
5
Step 5: AppArmor/SELinux
Debian/Ubuntu : AppArmor (profile docker-default), RHEL/CentOS : SELinux. Le profile par défaut est déjà strict ; peu de personnalisation en général. -
6
Step 6: Scan et surveillance
Trivy ou docker scan réguliers ; vulnérabilités critiques bloquent la mise en prod. Surveiller redémarrages anormaux et usage ressources ; auditer la config périodiquement.
Créer correctement un utilisateur non-root
Exemple standard :
FROM node:18-alpine
# Créer utilisateur et groupe dédiés (UID/GID fixes)
RUN addgroup -g 5000 appgroup \
&& adduser -D -u 5000 -G appgroup appuser
# Répertoire de travail
WORKDIR /app
# Copier avec propriétaire correct (étape clé !)
COPY --chown=appuser:appgroup package*.json ./
RUN npm install
COPY --chown=appuser:appgroup . .
# Passer en non-root (toutes les commandes suivantes en appuser)
USER appuser
# Démarrage
CMD ["node", "server.js"]
Chaque ligne compte.
Pourquoi UID/GID fixes ? Sans chiffres, l’UID auto varie entre conteneurs ; volumes partagés, permissions incohérentes. UID 5000 fixe = moins de surprises.
La magie de COPY --chown ? COPY puis RUN chown = deux couches, fichiers d’abord root. --chown fixe le propriétaire à la copie — plus compact et plus sûr. Oubli de chown → « Permission denied » au démarrage, demi-heure perdue.
Placement de USER. Avant USER : root (ex. RUN npm install). Après : appuser. USER trop tôt → échec des installs. Règle : tout ce qui exige root avant USER.
Pièges courants
Piège 1 : liaison de ports
Erreur après passage en non-root : Error: listen EACCES: permission denied 0.0.0.0:80. Ports < 1024 = privilège.
Solutions :
- Port élevé (recommandé) : 3000 ou 8080 + reverse proxy
- NET_BIND_SERVICE : voir plus bas
# Application sur port 3000
EXPOSE 3000
USER appuser
CMD ["node", "server.js"] # écoute interne 3000
Mapping dans docker-compose ou K8s :
ports:
- "80:3000" # hôte 80 → conteneur 3000
Piège 2 : logs et fichiers temporaires
Application Python en non-root : échec d’écriture dans /var/log.
# Créer et autoriser le répertoire de logs
RUN mkdir -p /var/log/myapp && \
chown -R appuser:appgroup /var/log/myapp
USER appuser
Mieux : stdout/stderr, logs collectés par Docker/K8s :
# Pas de fichier log
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
Piège 3 : permissions de volume
/data appartient à root sur l’hôte ; monté dans le conteneur, appuser (UID 5000) ne peut pas lire.
# Mauvais exemple
docker run -v /data:/app/data myapp
# appuser ne peut pas lire/écrire /app/data
Deux solutions :
# Solution 1 : permissions sur l'hôte
sudo chown -R 5000:5000 /data
# Solution 2 : volume nommé
docker volume create appdata
docker run -v appdata:/app/data myapp
Paramètres runtime : —user et au-delà
—user pour surcharger l’image
Image tierce sans USER, tout en root ? Rebuild lourd. --user à l’exécution :
# UID:GID direct
docker run --user=1001:1001 nginx:latest
# Utilisateur courant de l'hôte (dev)
docker run --user="$(id -u):$(id -g)" -v "$PWD:/app" node:18 npm test
# Nom d'utilisateur (si présent dans l'image)
docker run --user=nobody redis:alpine
La méthode 2 est pratique en local : fichiers générés avec votre UID, pas de sudo pour nettoyer.
Attention : --user écrase USER du Dockerfile. Image déjà non-root + --user=0:0 = retour au root. Vérifiez la config par défaut.
Système de fichiers en lecture seule
Attaquant dans le conteneur, veut planter un malware ou modifier la config — filesystem read-only, attaque fortement limitée.
# Configuration minimale read-only
docker run -d --read-only nginx:alpine
# Fichiers temporaires nécessaires ?
docker run -d \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
nginx:alpine
--tmpfs = RAM, effacé au redémarrage. API service : logs stdout, sessions Redis, pas d’écriture persistante — read-only limite les dégâts d’un shell.
Interdire l’élévation : no-new-privileges
Empêche setuid/setgid d’élever les privilèges. Même un /bin/su SUID inutilisable pour devenir root.
docker run --security-opt=no-new-privileges myapp
Test : sudo dans un conteneur avec cette option → « effective uid is not 0 ». Efficace contre l’élévation.
Configuration production : combinaison
docker run -d \
--name secure-webapp \
--user=5000:5000 \ # non-root
--read-only \ # read-only
--tmpfs /tmp:size=64M \ # 64 Mo temporaires
--security-opt=no-new-privileges \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-p 443:8443 \
-v appdata:/app/data \
--memory=512m \
--cpus=1.0 \
myapp:1.0.0
Chaque paramètre a un rôle. Plusieurs services critiques utilisent ce modèle depuis plus de deux ans, sans incident. Coût : débogage un peu plus lent — pas d’exec pour modifier des fichiers — mais le jeu en vaut la chandelle.
K8s : même politique dans SecurityContext :
securityContext:
runAsNonRoot: true
runAsUser: 5000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Contrôle fin : mécanisme Capabilities
Qu’est-ce que Capabilities ? Pourquoi plus sûr que root ?
Linux classique : tout ou rien — root ou utilisateur limité. Capabilities découpent root en 40+ capacités indépendantes.
Analogie : root = passe master ; Capabilities = clés par pièce. Compromission = accès limité aux pièces autorisées.
Docker garde 14 Capabilities par défaut :
- CHOWN : changer propriétaire de fichiers
- NET_BIND_SERVICE : ports < 1024
- SETUID/SETGID : changer UID/GID
- KILL : envoyer des signaux
- DAC_OVERRIDE : contourner permissions fichiers
Suffisant pour la plupart des apps. Certaines sont très dangereuses — à dropper.
Capabilities dangereuses (ne jamais accorder !)
SYS_ADMIN — presque un root
Mount, namespaces, modules noyau… Cas réel : conteneur avec SYS_ADMIN, attaquant unshare, montage disque hôte — fin de partie.
# Ne faites surtout pas ça !
docker run --cap-add=SYS_ADMIN myapp # ❌ Dangereux !
NET_ADMIN — configuration réseau
Routes, pare-feu, sniffing. Réservé aux outils réseau (VPN, routeur logiciel).
SYS_MODULE — modules noyau
Insérer du code dans le noyau. Le danger parle de lui-même.
Configuration moindre privilège en pratique
Stratégie 1 : tout retirer, ajouter au besoin (recommandé)
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--cap-add=CHOWN \
myapp
Stratégie habituelle. Erreur « Operation not permitted » sur setuid → --cap-add=SETUID.
Stratégie 2 : retirer seulement les dangereuses (renforcement rapide)
docker run -d \
--cap-drop=SYS_ADMIN \
--cap-drop=NET_ADMIN \
--cap-drop=SYS_MODULE \
--cap-drop=SYS_RAWIO \
myapp
Quand vous n’êtes pas sûr des besoins, mais voulez éliminer l’évident.
Comment savoir quelles Capabilities sont nécessaires ?
Méthode 1 : essai-erreur
# Étape 1 : drop all
docker run --cap-drop=ALL myapp
# Erreur : bind: permission denied
# Étape 2 : ajouter NET_BIND_SERVICE
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# Démarrage OK !
Méthode 2 : capsh
$ docker run --rm -it --cap-drop=ALL ubuntu capsh --print
Current: =
# vide
$ docker run --rm -it ubuntu capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,...
# 14 Capabilities par défaut
Méthode 3 : tableau par type d’application
| Type d’application | Capabilities requises | Notes |
|---|---|---|
| Web (port élevé) | Aucune | 3000+ sans privilège spécial |
| Web (port bas) | NET_BIND_SERVICE | 80/443 |
| Base de données (MySQL/Postgres) | Aucune | Ports par défaut élevés |
| Nginx/Caddy | NET_BIND_SERVICE | Si écoute directe 80/443 |
| VPN/outils réseau | NET_ADMIN | Routes/interfaces |
La plupart des apps : drop all, parfois NET_BIND_SERVICE.
Contrôle d’accès obligatoire : AppArmor et SELinux
À quoi servent-ils ?
Capabilities = « quelles opérations » ; AppArmor/SELinux = « quels fichiers et ressources ». MAC au niveau OS : même root doit respecter le profile.
Analogie : patron (root) mais accès salle serveur = badge. AppArmor/SELinux = contrôle d’accès.
Choix système :
- Debian/Ubuntu : AppArmor par défaut
- RHEL/CentOS : SELinux par défaut
- Un seul à la fois — pas les deux (conflits)
AppArmor : simple et suffisant (débutants)
Docker applique automatiquement le profile docker-default, déjà strict. Souvent rien à configurer.
# Voir le profile AppArmor du conteneur
docker inspect mycontainer | grep -i apparmor
# "AppArmorProfile": "docker-default"
Que fait docker-default ?
Interdit notamment :
- mount de systèmes de fichiers
- modification de paramètres noyau (/proc/sys/)
- accès aux périphériques sensibles (/dev/)
- modification de la config AppArmor
Test : conteneur avec AppArmor, même root, mount /dev/sda1 /mnt → « Permission denied ». Capabilities + AppArmor : évasion beaucoup plus difficile.
SELinux : plus puissant, plus complexe
Chaque fichier et processus a un label ; les politiques définissent qui accède à quoi.
# Label SELinux du processus conteneur
docker inspect mycontainer | grep -i selinux
# "ProcessLabel": "system_u:system_r:container_t:s0:c123,c456"
c123,c456 = catégories uniques par conteneur, isolation inter-conteneurs.
SELinux : courbe d’apprentissage raide, messages d’erreur cryptiques. Ubuntu → AppArmor suffit ; RHEL → SELinux actif par défaut.
Recommandations pratiques
Scénario 1 : développement
- Désactivation temporaire possible (
apparmor=unconfinedoulabel=disable) pour déboguer - Mais : désactivé en dev, réactivé en prod ?
Scénario 2 : test
- Obligatoire, profile par défaut
- Détecter tôt conflits sécurité / fonctionnalité
Scénario 3 : production
- Obligatoire, sans compromis
- Profile par défaut sauf raison documentée
- Auditer les logs de tentatives refusées
99 % des DENIED sont légitimes — attaque ou mauvaise conception applicative. Assouplissements rares.
Checklist de sécurité complète
Synthèse actionnable — conteneurs plus sûrs que 80 % des déploiements.
Phase build d’image
Dockerfile :
- ✅ Image de base officielle ou de confiance
- ✅ Version figée (
node:18.17-alpine, pasnode:latest) - ✅ Utilisateur non-root dédié avec UID/GID fixes
- ✅
COPY --chownpour les fichiers - ✅
USERaprès install, avant démarrage - ✅ Port élevé ou Capabilities adaptées
- ✅ Build multi-étapes pour réduire surface d’attaque
- ✅ Pas de secrets dans l’image (variables d’environnement ou secrets)
Phase scan d’image
Scans obligatoires :
# docker scan (Snyk)
docker scan myapp:latest
# Trivy (recommandé, plus rapide)
trivy image myapp:latest
# Clair (CI/CD)
# Harbor avec scan automatique
76 % des images Docker Hub ont des vulnérabilités — scan régulier non négociable. Règles d’équipe :
- Critique : correction avant prod
- Moyen : évaluation de risque + surveillance
- Faible : inventaire + revue périodique
Vérification runtime
Modèle docker-compose production :
services:
myapp:
image: myapp:1.0.0
user: "5000:5000"
read_only: true
tmpfs:
- /tmp:size=64M
security_opt:
- no-new-privileges:true
- apparmor=docker-default
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
volumes:
- appdata:/app/data:rw
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
ports:
- "8080:8080"
Exploitation production
Surveillance quotidienne :
- ✅ Redémarrages anormaux (crash après attaque)
- ✅ Usage CPU/mémoire anormal (mineurs)
- ✅ Journalisation des opérations conteneur
Audit périodique :
- ✅ Scan mensuel des images en cours d’exécution
- ✅ Conteneurs
--privilegedou Capabilities dangereuses - ✅ Politique réseau et ports exposés
Questions fréquentes
Q1 : erreurs de permission après passage en non-root ?
Diagnostic :
- Message d’erreur précis (fichier ou port ?)
- Fichier :
--chownet permissions dans Dockerfile - Port : port élevé ou NET_BIND_SERVICE
Erreurs courantes :
# Erreur : Error: EACCES: permission denied, open '/app/logs/app.log'
# Cause : répertoire logs sans droits appuser
# Fix :
RUN mkdir -p /app/logs && chown appuser:appgroup /app/logs
# Erreur : Error: listen EACCES: permission denied 0.0.0.0:80
# Cause : port bas interdit en non-root
# Fix 1 : port 3000 + mapping
EXPOSE 3000
# Fix 2 : Capability
docker run --cap-add=NET_BIND_SERVICE myapp
Q2 : permissions de volume incohérentes ?
Problème le plus fréquent. Trois solutions :
Solution 1 : UID/GID sur l’hôte (recommandé)
sudo chown -R 5000:5000 /data
docker run -v /data:/app/data myapp
Solution 2 : volume nommé
docker volume create --opt o=uid=5000,gid=5000 appdata
docker run -v appdata:/app/data myapp
Q3 : quand root est-il vraiment nécessaire ? Presque jamais !
| Scénario | Alternative sans root |
|---|---|
| Ports 80/443 | NET_BIND_SERVICE ou port élevé + load balancer |
| Paquets système | Install dans Dockerfile avant USER |
| Config système | Variables d’environnement ou fichiers injectés |
| Socket Docker | Très dangereux — API Docker ou K8s plutôt que monter le socket |
Seul cas « acceptable » vu : outil de migration legacy imposant root, exécution one-shot puis destruction du conteneur.
Q4 : image tierce en root ?
Priorités :
- Version officielle non-root (
-rootless,-nonroot) --userà l’exécution
docker run --user=65534:65534 third-party-image # nobody
- Dockerfile dérivé avec USER
FROM third-party-image:latest
RUN adduser -D -u 5000 appuser
USER appuser
- Demander une variante non-root aux mainteneurs
Conclusion
En une phrase : conteneur en root = porte dérobée pour les attaquants.
Points clés :
- L’évasion n’est pas théorique : CVE-2024-21626, conteneurs privilégiés
- USER dans Dockerfile +
--userau runtime : coût faible, gain énorme - Capabilities : drop all + ajout ciblé
- Read-only, no-new-privileges, AppArmor : défense en profondeur
- 76 % des images vulnérables : scan régulier indispensable
Trois actions dès aujourd’hui :
- Vérifier vos Dockerfiles — ajouter USER si absent
- Auditer la production —
--privilegedet conteneurs root à corriger - Intégrer le scan dans la CI/CD
La sécurité est un processus continu. Passer de root à non-root vous place déjà devant la majorité. N’attendez pas le jour où vous serez compromis — l’exemple de cette entreprise est là pour ça.
FAQ
Pourquoi ne faut-il pas exécuter les conteneurs Docker en root ?
Des failles d'évasion comme CVE-2024-21626 le prouvent : l'isolation des conteneurs n'est pas absolue. 76 % des images sur Docker Hub contiennent des vulnérabilités, 67 % des vulnérabilités critiques. Exécuter en utilisateur non-root réduit le risque d'environ 80 %.
Comment configurer un utilisateur non-root dans le Dockerfile ?
Toutes les opérations nécessitant root se font avant USER. Des UID/GID fixes évitent les problèmes de permissions lors du montage de volumes.
Que faire si un utilisateur non-root ne peut pas lier les ports 80/443 ?
1) Utiliser un port élevé (recommandé) :
• L'application écoute sur 3000 ou 8080
• Nginx ou un load balancer en reverse proxy
• Mapper le port 80 de l'hôte vers le 3000 du conteneur
2) Utiliser la Capability NET_BIND_SERVICE :
• Permet à un utilisateur non-root de lier les ports bas
• Évite d'écouter 80/443 directement en root
Que sont les Capabilities ? Comment configurer le moindre privilège ?
Bonnes pratiques :
• D'abord --cap-drop=ALL pour tout retirer
• Puis ajouter au besoin (ex. NET_BIND_SERVICE pour la liaison de ports)
• Ne jamais accorder SYS_ADMIN, NET_ADMIN, SYS_MODULE, etc.
La plupart des applications métier tournent avec drop all, parfois plus NET_BIND_SERVICE.
Quels paramètres de sécurité configurer en production ?
• --user=5000:5000 utilisateur non-root
• --read-only système de fichiers en lecture seule avec --tmpfs pour les répertoires temporaires
• --security-opt=no-new-privileges interdit l'élévation de privilèges
• --cap-drop=ALL retire toutes les Capabilities puis ajout au besoin
• Activer AppArmor/SELinux pour le contrôle d'accès obligatoire
Sous K8s, la même politique se configure dans le SecurityContext du Pod.
Que faire si les permissions ne correspondent pas après montage d'un volume ?
1) Définir UID/GID sur l'hôte à l'avance (recommandé) :
• sudo chown -R 5000:5000 /data
• Aligner sur l'UID de l'utilisateur du conteneur
2) Volume nommé géré par Docker :
• docker volume create --opt o=uid=5000,gid=5000 appdata
3) Créer et autoriser les répertoires pour l'utilisateur applicatif dans le Dockerfile
Comment déterminer quelles Capabilities l'application nécessite ?
1) Essai-erreur :
• --cap-drop=ALL et observer les erreurs
• Ajouter selon les messages
2) Outil capsh :
• docker run --rm -it ubuntu capsh --print pour voir les Capabilities courantes
3) Référence par type d'application :
• Web (port élevé) : aucune permission spéciale
• Web (port bas) : NET_BIND_SERVICE
• VPN/outils réseau : NET_ADMIN
12 min de lecture · Publié le: 18 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide pratique Docker
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Analyse de sécurité et correction des images Docker : tutoriel Trivy et intégration CI/CD
76 % des images sur Docker Hub présentent des vulnérabilités. Ce guide détaille Trivy, les méthodes de correction systématique et l'intégration CI/CD automatisée, avec des exemples de commandes complets.
Partie 29 sur 38
Suivant
Guide complet Docker Secrets : sécuriser mots de passe et clés API des conteneurs
Ne mettez plus les mots de passe de base de données dans le Dockerfile ! Ce guide détaille Docker Secrets pour gérer les informations sensibles, compare Docker/K8s/Vault et inclut une checklist complète pour la production.
Partie 31 sur 38



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire