Intro
Cette version française explique Docker container security hardening for safer runtime defaults avec le même objectif pratique que l article source : aider le lecteur à comprendre le contexte, les décisions à prendre et les points à vérifier avant de passer à l action.
Les conteneurs démarrent souvent en root, peuvent écrire presque partout et héritent de plus de pouvoirs noyau que la plupart des applications n'en requièrent. En cas de compromission, ces libertés aggravent l'impact. Ce guide propose des valeurs par défaut d'exécution plus sûres et immédiatement actionnables : non-root containers, Docker capabilities minimales, read-only filesystem avec tmpfs pour les chemins réellement nécessaires en écriture, et conservation des profils seccomp Docker et AppArmor Docker par défaut. Vous y trouverez des Dockerfile examples, des docker commands, du Docker Compose, un petit plan de test local et les principaux pièges de production avec leur troubleshooting.
Aperçu du flux de travail
- Construire des images qui s'exécutent en non-root par défaut et corriger la propriété des fichiers au build.
- Privilégier des ports élevés dans le conteneur et mapper depuis l'hôte.
- Supprimer toutes les Linux capabilities puis réajouter uniquement celles requises.
- Rendre le système de fichiers racine en lecture seule et fournir des tmpfs pour les besoins d'écriture à l'exécution comme /tmp.
- Conserver le profil seccomp par défaut de Docker et, si disponible, le profil AppArmor docker-default.
- Ajouter des limites d'exécution comme no-new-privileges, pids-limit et des limites de ressources.
- Tester localement via un pilote restreint, observable et mesurable avant d'élargir.
Dockerfile : non-root et propriété des fichiers
Créez un utilisateur et un groupe dédiés, préparez les répertoires et fixez la propriété au moment du build. Exemple pour un service web générique qui écoute sur 8080 :
# syntax=docker/dockerfile:1
FROM debian:12-slim
# Installer uniquement le nécessaire (ex. dépendances runtime)
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Créer l'utilisateur applicatif et les répertoires
RUN groupadd --system app && useradd --system --create-home --gid app app
WORKDIR /app
# Copier les fichiers applicatifs et fixer la propriété
COPY --chown=app: app . /app
# Exposer un port élevé pour éviter des capacités réseau supplémentaires
EXPOSE 8080
# Abandonner root au build
USER app: app
# Point d'entrée simple
CMD ["./server"]
Notes :
- Utilisez --chown lors du COPY pour que les fichiers soient accessibles en écriture par l'utilisateur applicatif si nécessaire.
- Préférez des ports élevés (ex. 8080) à l'intérieur du conteneur. Mappez le port 80 de l'hôte vers 8080, ce qui évite d'ajouter NET_BIND_SERVICE.
- Les builds multi-étages gardent les images petites et réduisent la surface d'attaque.
Capabilities et réseau
Les conteneurs héritent d'un ensemble par défaut de Linux capabilities. La voie la plus sûre consiste à tout supprimer et à réintroduire seulement l'indispensable.
Modèle plus sûr quand vous n'avez pas besoin de privilèges réseau particuliers :
docker run --rm \
--cap-drop ALL \
--security-opt no-new-privileges: true \
-p 80:8080 myapp: latest
Si vous devez absolument lier un port bas (ex. 80) dans le conteneur, alors :
- Préférez le mapping hôte :
-p 80:8080et évitez CAP_NET_BIND_SERVICE. - Ou n'ajoutez que la capacité spécifique nécessaire :
docker run --rm \
--cap-drop ALL --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges: true \
-p 80:80 myapp: latest
Évitez les conteneurs privilégiés et l'ajout de capabilities larges comme SYS_ADMIN sans raison minimale, claire et auditée.
Read-only filesystem et tmpfs
Un read-only filesystem (système de fichiers en lecture seule) bloque de nombreuses attaques basées sur l'écriture et réduit les dégâts accidentels. Combinez-le avec des montages tmpfs pour les chemins devant rester écrits à l'exécution.
Exemple d'exécution :
docker run --rm \
--read-only \
--tmpfs /tmp: rw, noexec, nosuid, nodev, size=64m \
--tmpfs /var/run: rw, noexec, nosuid, nodev, size=16m \
-p 80:8080 myapp: latest
Conseils :
- Redirigez les logs applicatifs vers stdout/stderr plutôt que des fichiers. Docker collecte ces logs.
- Si l'application doit persister des données, montez un volume explicite ou un bind mount sur un chemin unique, en lecture/écriture, et laissez le reste en lecture seule.
- Utilisez noexec, nosuid, nodev sur les tmpfs quand c'est possible.
Profils seccomp et AppArmor
Docker applique par défaut un profil seccomp qui bloque de nombreux appels système risqués. Conservez ce profil par défaut à moins d'un besoin identifié de l'assouplir. Évitez l'option unconfined.
- Seccomp : appliqué automatiquement. N'utilisez pas
--security-opt seccomp=unconfined. Fournissez un profil seccomp personnalisé uniquement pour un cas spécifique. - AppArmor : sur les hôtes avec AppArmor activé, le profil docker-default est appliqué. Conservez-le, ou liez un profil plus strict avec
--security-opt apparmor=docker-default(ou un nom de profil personnalisé) quand disponible.
Exemple Compose qui garde les valeurs par défaut et ajoute no-new-privileges :
services:
web:
image: myapp: latest
ports:
- "80:8080"
security_opt:
- "no-new-privileges: true"
- "apparmor: docker-default" # si AppArmor est activé sur l'hôte
Si vous fournissez un fichier de profil seccomp personnalisé, référencez-le explicitement :
docker run --rm \
--security-opt seccomp=./seccomp-profile.json \
myapp: latest
Exemples d'exécution et Compose
Ensemble d'options plus sûres, réunies :
Exemple docker run :
docker run --rm \
--user 1000:1000 \
--cap-drop ALL \
--security-opt no-new-privileges: true \
--read-only \
--tmpfs /tmp: rw, noexec, nosuid, nodev, size=64m \
--pids-limit 200 \
--cpus 1 --memory 256m \
-p 80:8080 myapp: latest
Équivalent Docker Compose :
services:
web:
image: myapp: latest
user: "1000:1000"
ports:
- "80:8080"
read_only: true
tmpfs:
- "/tmp: rw, noexec, nosuid, nodev, size=64m"
cap_drop:
- ALL
security_opt:
- "no-new-privileges: true"
- "apparmor: docker-default"
deploy:
resources:
limits:
cpus: "1"
memory: 256M
Remarque : sur les hôtes sans AppArmor, retirez la ligne AppArmor. Les limites de ressources sous deploy nécessitent un orchestrateur pris en charge ; en local avec Compose, vous pouvez aussi utiliser mem_limit et cpus (compose v2).
Plan pilote local
Démarrez petit et mesurable pour inspecter localement les effets avant un déploiement plus large.
Objectif : exécuter un service en non-root, avec un read-only filesystem, un tmpfs pour /tmp, des capabilities supprimées et no-new-privileges. Vérifier que l'app sert toujours du trafic et échoue proprement lors d'écritures non autorisées.
Plan :
- Choisissez un service simple (site statique ou API basique).
- Mettez à jour son Dockerfile pour créer un utilisateur dédié et définir USER.
- Exécutez avec
--cap-drop ALL,--read-only,--tmpfs /tmpet--security-opt no-new-privileges: true. - Mappez le port 80 de l'hôte vers 8080 du conteneur pour éviter des capacités supplémentaires.
- Vérifiez :
- Non-root :
docker exec -it <cid> idet confirmez que uid != 0. - Système de fichiers :
docker exec <cid> sh -c 'touch /etc/blocked'doit échouer. - /tmp en écriture :
docker exec <cid> sh -c 'touch /tmp/ok'doit réussir. - Trafic :
curl localhost:80doit renvoyer un 200.
- Documentez tout chemin d'écriture manquant et fournissez un volume ou un tmpfs spécifique si nécessaire.
Critères de succès :
- L'application sert le trafic.
- Les écritures hors chemins autorisés échouent comme prévu.
- Aucune capability ajoutée au-delà du strict nécessaire.
Dépannage et pièges en production
Problèmes courants et correctifs :
- Permission denied au démarrage : des fichiers copiés en root ne sont pas accessibles en écriture par l'utilisateur applicatif. Corrigez avec
COPY --chown=app: appet vérifiez la propriété des répertoires. - L'app ne peut pas écrire ses logs : envoyez-les vers stdout/stderr ou montez un répertoire dédié en lecture/écriture pour les logs.
- Les gestionnaires de paquets échouent au runtime : avec
--read-only, pas d'écriture possible pour apt ou apk. Installez tout au build et gardez un runtime minimal. - Liaison au port 80 impossible : utilisez
-p 80:8080et laissez le conteneur sur 8080. Si vous devez absolument lier 80 dans le conteneur, ajoutez uniquementNET_BIND_SERVICE. - Problèmes de fichiers temporaires : fournissez un tmpfs pour
/tmpet les répertoires de sockets runtime comme/var/run. - Besoin d'un seccomp ou AppArmor personnalisé : ne relâchez que le nécessaire. Partez des valeurs par défaut et ajoutez des autorisations minimales.
- Comportements divergents selon les hôtes : AppArmor peut être absent sur certains hôtes. Gardez la configuration conditionnelle et testez sur l'OS cible.
Limites du scanning d'images (image scanning boundaries) :
- Le scanning détecte des CVE connus dans les images, mais n'assure pas des réglages runtime sûrs. Combinez le scanning d'images avec les gestes de durcissement ci-dessus.
- Les mauvaises configurations d'exécution (root, capabilities larges) échappent au périmètre des scanners d'images.
Conseils opérationnels :
- Epinglez les digests des images de base pour réduire les changements inattendus.
- Utilisez des builds multi-étages pour réduire la taille et la surface d'attaque.
- Ajoutez des healthchecks et des limites de ressources pour éviter les processus incontrôlés.
Conclusion
Des valeurs par défaut plus sûres sont simples à mettre en œuvre : non-root containers, Docker capabilities réduites au strict nécessaire, read-only filesystem avec tmpfs pour les écritures indispensables, et conservation des profils seccomp Docker et AppArmor Docker par défaut. Ajoutez no-new-privileges et des limites de ressources, testez via un petit pilote local, puis déployez progressivement. Ces gestes réduisent le risque tout en restant simples, observables et compatibles avec des opérations quotidiennes efficaces.