Docker - Dockerfile et sécurité
Les conteneurs Docker sont devenus un standard de l'industrie pour le déploiement d'applications modernes, mais ils introduisent également des défis de sécurité spécifiques.
Cette quête explore les risques de sécurité liés aux conteneurs Docker et présente les principes fondamentaux pour sécuriser les déploiements conteneurisés.

⚙️ Pré-requis
Avant de commencer cette quête, il est nécessaire de :
- Comprendre les bases de Docker (images, conteneurs, volumes)
- Savoir créer et exécuter des conteneurs Docker simples
- Maîtriser les commandes de base Linux
- Disposer d'un environnement Linux avec Docker installé (de préférence une version récente)
Et avoir réalisé la quête suivante :
🎯 Objectifs :
✅ Comprendre les risques de sécurité associés aux conteneurs Docker ✅ Maîtriser les principes fondamentaux de la sécurité des conteneurs ✅ Identifier les vulnérabilités courantes dans les déploiements Docker ✅ Appliquer les premières mesures de sécurisation des conteneurs
Les conteneurs Docker offrent des avantages considérables en termes de vitesse de déploiement et d'efficacité des ressources, mais ils présentent également des défis de sécurité uniques qu'il est essentiel de comprendre.
Isolation limitée par rapport aux machines virtuelles
Contrairement aux machines virtuelles qui possèdent leur propre système d'exploitation complet, les conteneurs partagent le noyau du système hôte.
Cette architecture offre des avantages en termes de performances et d'efficacité, mais elle présente aussi des limites importantes en matière d'isolation.
# Exemple montrant le partage du noyau entre conteneur et hôte
docker run --rm alpine uname -r # Affiche la version du noyau
uname -r # Même version sur l'hôte
Lorsqu'on exécute cette commande, on constate que la version du noyau dans le conteneur est identique à celle de l'hôte.
Cela démontre que les conteneurs ne virtualisent pas le noyau mais l'utilisent directement depuis l'hôte.
C'est cette caractéristique fondamentale qui explique la légèreté des conteneurs, mais aussi leur vulnérabilité : si un attaquant parvient à exploiter une faille du noyau depuis un conteneur, il peut potentiellement s'échapper et compromettre l'hôte ainsi que tous les autres conteneurs qui s'y exécutent.
Risques liés aux images de base
Les images Docker proviennent souvent de sources tierces (Docker Hub, par exemple) et peuvent contenir :
- Des vulnérabilités non corrigées
- Des bibliothèques obsolètes
- Des backdoors ou du code malveillant
- Des dépendances avec des vulnérabilités connues (CVE)
Exemple de commande pour vérifier le contenu d'une image :
docker image history nginx:latest
Cette commande affiche l'historique complet des couches de l'image, révélant chaque instruction du Dockerfile qui a été utilisée pour construire l'image. Cet examen est essentiel pour comprendre quels composants ont été intégrés, quelles versions de packages ont été installées, et quelles potentielles vulnérabilités pourraient exister. L'analyse de cet historique permet de détecter des comportements suspects, comme l'installation de packages non documentés ou l'exécution de scripts arbitraires lors de la construction.
Principe du "moindre privilège" souvent ignoré
De nombreux conteneurs s'exécutent par défaut en tant qu'utilisateur root, ce qui pose un risque considérable si le conteneur est compromis.
# Vérifier l'utilisateur dans un conteneur standard
docker run --rm alpine id
# Affiche généralement : uid=0(root) gid=0(root)
Cette commande lance un conteneur Alpine Linux et exécute la commande id, qui affiche l'identité de l'utilisateur actuel. Le résultat typique montre que les processus dans le conteneur s'exécutent avec l'UID 0 et le GID 0, correspondant à l'utilisateur root. C'est extrêmement problématique du point de vue de la sécurité : si un attaquant exploite une vulnérabilité dans l'application conteneurisée, il obtient immédiatement les privilèges root dans le conteneur, lui permettant d'installer des outils malveillants, de modifier la configuration système, ou de tenter une évasion du conteneur.
Risques liés aux configurations réseau inappropriées
Les configurations réseau par défaut de Docker peuvent exposer inutilement des services ou permettre des communications non désirées entre conteneurs.
# Exemple de conteneur exposant un service sur toutes les interfaces
docker run -d -p 8080:80 nginx
# Ce conteneur est accessible depuis n'importe quelle interface sur le port 8080
Cette commande lance un conteneur Nginx et publie le port 80 du conteneur sur le port 8080 de toutes les interfaces réseau de l'hôte. L'option -p 8080:80 crée un mappage entre le port 8080 de l'hôte et le port 80 du conteneur. Bien que pratique pour le développement, cette configuration expose le service à toutes les interfaces, y compris celles accessibles depuis l'extérieur. Dans un environnement de production, une approche plus sécurisée consisterait à limiter l'exposition à certaines interfaces (par exemple, -p 127.0.0.1:8080:80) pour restreindre l'accès au service uniquement depuis l'hôte local.
Risques liés au stockage et aux volumes
Le montage de volumes peut créer des chemins d'accès non intentionnels entre les conteneurs et le système hôte, compromettant l'isolation.
# Exemple de montage potentiellement risqué
docker run -v /:/host_root alpine ls -la /host_root
# Ce conteneur peut lire et potentiellement modifier le système de fichiers de l'hôte
Cet exemple illustre un problème de sécurité majeur : le montage du répertoire racine de l'hôte (/) dans un conteneur. L'option -v /:/host_root crée un pont direct entre l'ensemble du système de fichiers de l'hôte et le répertoire /host_root du conteneur. Cela signifie que le conteneur a un accès complet en lecture et écriture à tous les fichiers de l'hôte, y compris les fichiers système critiques, les configurations et les données sensibles. Un attaquant qui compromettrait ce conteneur pourrait facilement accéder à des fichiers sensibles comme /etc/shadow ou injecter des backdoors dans des scripts système de l'hôte.
Vulnérabilités dans le démon Docker
Le démon Docker lui-même fonctionne avec des privilèges élevés. Si un attaquant obtient l'accès au socket Docker, il peut effectivement obtenir un accès root sur l'hôte.
# Test montrant les risques liés à l'accès au socket Docker
docker run -v /var/run/docker.sock:/var/run/docker.sock docker:latest docker images
# Ce conteneur peut contrôler le démon Docker
Cette commande expose le danger lié au montage du socket Docker dans un conteneur. En utilisant -v /var/run/docker.sock:/var/run/docker.sock, on donne au conteneur un accès complet à l'API du démon Docker. Le conteneur peut alors exécuter n'importe quelle commande Docker, y compris lancer de nouveaux conteneurs, accéder à des volumes sensibles, ou même exécuter des conteneurs privilégiés. Dans l'exemple, le conteneur exécute simplement docker images pour lister les images disponibles, mais il pourrait tout aussi bien lancer un conteneur privilégié qui monte le système de fichiers de l'hôte, permettant ainsi une compromission totale du système.
OWASP Docker Security Cheat Sheet
Pour sécuriser efficacement les déploiements Docker, il est essentiel de comprendre et d'appliquer plusieurs principes fondamentaux :
Le principe du moindre privilège
Ce principe fondamental en sécurité informatique stipule qu'une entité (utilisateur, processus, conteneur) ne devrait disposer que des privilèges minimaux nécessaires pour accomplir sa tâche.
Application dans Docker :
- Éviter d'exécuter des conteneurs en tant que
root - Limiter les capacités du noyau (Linux capabilities)
- Utiliser des contextes de sécurité restreints
# Exemple : exécuter un conteneur avec des privilèges limités
docker run --user 1000:1000 --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
Cette commande exécute un conteneur Nginx en appliquant le principe du moindre privilège de trois façons importantes :
--user 1000:1000: Force l'exécution sous l'identité d'un utilisateur et groupe non-root spécifique (UID 1000, GID 1000)--cap-drop ALL: Supprime toutes les capacités Linux par défaut, qui seraient normalement accordées au conteneur--cap-add NET_BIND_SERVICE: Réajoute uniquement la capacité nécessaire pour lier des ports privilégiés (< 1024)
Ainsi, même si l'application dans le conteneur est compromise, l'attaquant aurait des privilèges très limités, réduisant considérablement l'impact potentiel.
Defense in Depth (Défense en profondeur)
Ce principe consiste à mettre en place plusieurs couches de protection, de sorte que si une couche est compromise, d'autres mécanismes de défense restent en place.
Application dans Docker :
- Mise à jour régulière des images de base
- Analyse des vulnérabilités des images
- Utilisation d'AppArmor/SELinux
- Surveillance des conteneurs en temps réel
La défense en profondeur implique de ne pas compter sur une seule mesure de sécurité, mais de combiner plusieurs approches complémentaires. Par exemple, même avec un utilisateur non-root dans le conteneur, il reste important d'analyser les vulnérabilités dans les packages installés, de limiter les capacités système, et de surveiller activement le comportement du conteneur en production.
Principe de séparation des préoccupations
Chaque conteneur doit idéalement avoir une responsabilité unique, ce qui limite la surface d'attaque potentielle.
Application dans Docker :
- Conteneur unique = service unique
- Ségrégation des réseaux
- Isolation entre les environnements (dev, staging, prod)
# Création d'un réseau isolé pour une application spécifique
docker network create --driver bridge app-network
docker run --network app-network --name api api-service
docker run --network app-network --name db database-service
Cette série de commandes illustre la séparation des préoccupations en créant un réseau isolé pour une application, puis en y connectant uniquement les services nécessaires.
Les conteneurs dans ce réseau peuvent communiquer entre eux par leurs noms, mais sont isolés des autres réseaux Docker. Cela limite l'exposition et réduit les risques en cas de compromission d'un conteneur.
Principe du "zero trust"
Ne jamais faire confiance implicitement et vérifier continuellement la sécurité à chaque niveau.
Application dans Docker :
- Signature et vérification des images
- Authentification mutuelle TLS
- Politiques d'admission strictes
Le modèle zero trust considère que les menaces peuvent provenir aussi bien de l'extérieur que de l'intérieur du réseau.
Il nécessite une vérification constante de l'identité et des privilèges, même pour les composants internes d'un système.
Docker security documentation
Voici les premières actions à mettre en place pour améliorer immédiatement la sécurité des déploiements Docker :
Utiliser des images officielles et minimales
Les images officielles maintenues par Docker ou par les éditeurs de logiciels sont généralement plus sûres car elles suivent de bonnes pratiques et reçoivent des mises à jour de sécurité régulières.
# À éviter
FROM ubuntu:latest
# Préférer
FROM alpine:3.21
# ou
FROM debian:12-slim
Les images minimales comme Alpine Linux réduisent considérablement la surface d'attaque en n'incluant que les composants essentiels.
Avec une taille d'environ 5 MB (contre plus de 100 MB pour une image Ubuntu standard), Alpine offre moins de packages potentiellement vulnérables et moins d'outils qui pourraient être utilisés par un attaquant en cas de compromission.
Toujours spécifier des versions précises (tag pinning)
Ne jamais utiliser de tags flottants comme latest qui peuvent changer sans préavis.
# À éviter
FROM node:latest
# Préférer
FROM node:24.0.1-alpine3.21
Cette pratique, appelée "tag pinning" ou "version pinning", garantit la reproductibilité des builds et empêche l'introduction accidentelle de nouvelles vulnérabilités lors de reconstructions ultérieures.
Sans version fixe, l'instruction FROM node:latest pourrait télécharger une image différente chaque semaine, avec potentiellement de nouveaux packages ou configurations qui pourraient introduire des vulnérabilités ou des incompatibilités.
Créer un utilisateur non-root dans le Dockerfile
FROM alpine:3.21
# Créer un utilisateur dédié
RUN addgroup -g 1000 appgroup && \
adduser -u 1000 -G appgroup -D appuser
# ... Installer les dépendances et configurer l'application ...
# Basculer vers l'utilisateur non privilégié
USER appuser
CMD ["./application"]
Ce Dockerfile crée un utilisateur dédié (appuser) avec un ID numérique spécifique (1000) puis configure le conteneur pour exécuter l'application sous cette identité via la directive USER appuser.
Cette approche simple mais efficace limite considérablement les privilèges des processus à l'intérieur du conteneur, réduisant l'impact potentiel d'une compromission.
Limiter les capacités du noyau Linux
Même avec un utilisateur non-root, il est recommandé de limiter les capacités Linux accordées au conteneur :
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE --rm myimage
Cette commande applique une stratégie de sécurité par défaut restrictive en supprimant toutes les capacités Linux (--cap-drop ALL) puis en rajoutant uniquement celles spécifiquement nécessaires au fonctionnement de l'application.
Cette approche du "moindre privilège" garantit que même si le conteneur est compromis, l'attaquant sera très limité dans ce qu'il peut faire.
Configurer correctement les réseaux Docker
Docker crée par défaut un réseau bridge partagé par tous les conteneurs. Pour une meilleure isolation :
# Créer des réseaux dédiés par application
docker network create frontend-net
docker network create backend-net
# Connecter les conteneurs aux réseaux appropriés
docker run --network frontend-net --name webapp webapp-image
docker run --network backend-net --name api api-image
# Connecter des conteneurs à plusieurs réseaux si nécessaire
docker network connect frontend-net api
Cette configuration permet de segmenter les communications réseau, de sorte que seuls les conteneurs qui ont réellement besoin de communiquer entre eux peuvent le faire.
Cette isolation réduit les risques de mouvements latéraux en cas de compromission d'un conteneur.
Utiliser des montages en lecture seule quand possible
# Monter une configuration en lecture seule
docker run -v /host/config:/app/config:ro myapp
L'option :ro (read-only) garantit que le conteneur peut lire les fichiers du volume monté mais ne peut pas les modifier.
Cette restriction est particulièrement importante pour les fichiers de configuration, les certificats et autres données sensibles qui ne devraient pas être modifiables par l'application en cours d'exécution.
Limiter les ressources des conteneurs
docker run -d --name limited-app \
--memory="512m" \
--memory-swap="512m" \
--cpus="1.0" \
--pids-limit=100 \
myapp
Ces limites de ressources sont cruciales pour :
- Éviter les attaques par déni de service où un conteneur compromis tenterait de consommer toutes les ressources de l'hôte
- Garantir que chaque conteneur ne peut utiliser que sa part équitable des ressources
- Détecter des comportements anormaux (une application qui consomme soudainement beaucoup plus de ressources que prévu peut indiquer une compromission)
L'option --pids-limit est particulièrement importante car elle limite le nombre de processus que le conteneur peut créer, empêchant les attaques de type "fork bomb" ou l'exécution de nombreux processus malveillants.
Options de sécurité pour docker run
- 1
Étape 1 : Évaluer un conteneur standard
Examinons d'abord les risques de sécurité dans un conteneur standard.
Créer un répertoire de travail !
bashmkdir -p ~/docker-security-demo cd ~/docker-security-demoExécuter un conteneur standard et vérifier l'utilisateur
bashdocker run --rm alpine idRésultat attendu : uid=0(root) gid=0(root)
Vérifier les capacités du conteneur :
bashdocker run --rm alpine capsh --printRésultat : liste complète des capacités Linux
Ces commandes révèlent deux problèmes de sécurité courants : l'exécution en tant que root et l'attribution de nombreuses capacités système qui ne sont généralement pas nécessaires.
- 2
Étape 2 : Créer un conteneur plus sécurisé
Créons maintenant un simple Dockerfile avec des améliorations de sécurité :
bashcat > Dockerfile.secure << 'EOF' FROM alpine:3.21 RUN apk update && apk upgrade RUN addgroup -g 1000 appgroup && \ adduser -u 1000 -G appgroup -D appuser RUN mkdir -p /app && \ chown -R appuser:appgroup /app USER appuser WORKDIR /app CMD ["sh", "-c", "echo 'Conteneur sécurisé en cours d'exécution' && sleep 3600"] EOFCe Dockerfile inclut plusieurs bonnes pratiques :
- Utilisation d'une image de base Alpine avec une version spécifique
- Mise à jour des packages pour inclure les derniers correctifs de sécurité
- Création d'un utilisateur dédié non-root
- Attribution des bonnes permissions sur les répertoires
- Exécution des processus sous l'identité de l'utilisateur non-root
- 3
Étape 3 : Construire et tester l'image sécurisée
Construction de l'image :
bashdocker build -t secure-alpine -f Dockerfile.secure .Test de l'image :
bashdocker run --rm secure-alpine idRésultat attendu : uid=1000(appuser) gid=1000(appgroup)
Comparaison avec une exécution avec des capacités limitées :
bashdocker run --rm --cap-drop ALL secure-alpine capsh --printRésultat : liste beaucoup plus restreinte, mais peut générer une erreur car l'utilisateur non-root n'a pas les permissions pour exécuter certaines commandes
Cette étape montre comment le conteneur s'exécute désormais sous une identité non privilégiée. L'option
--cap-drop ALLmontre également comment limiter encore davantage les privilèges en supprimant toutes les capacités Linux. - 4
Étape 4 : Configurer un réseau isolé
Création d'un réseau dédié :
bashdocker network create --driver bridge secure-networkExécution d'un conteneur dans ce réseau isolé :
bashdocker run -d --network secure-network --name secure-app secure-alpineVérification de l'isolation :
bashdocker run --rm alpine ping -c 2 secure-appCela devrait échouer car le conteneur Alpine standard n'est pas dans le même réseau
Cette étape illustre comment créer une isolation réseau entre différents groupes de conteneurs, empêchant les communications non autorisées.
- 5
Étape 5 : Utiliser des montages en lecture seule
Création d'un fichier de configuration sur l'hôte :
bashecho "CONFIG_VALUE=test" > ~/docker-security-demo/config.txtMontage en lecture seule dans un conteneur :
bashdocker run --rm -v ~/docker-security-demo/config.txt:/app/config.txt:ro secure-alpine cat /app/config.txtTentative d'écriture qui doit échouer :
bashdocker run --rm -v ~/docker-security-demo/config.txt:/app/config.txt:ro secure-alpine sh -c "echo 'MODIFIED' > /app/config.txt || echo 'Échec de la modification comme prévu'"Cette étape démontre comment protéger les données sensibles en empêchant leur modification par le conteneur, même en cas de compromission de l'application.
- 6
Étape 6 : Limiter les ressources du conteneur
Exécution avec des limites de ressources :
bashdocker run -d --name limited-app \ --memory="128m" \ --cpus="0.5" \ --pids-limit=20 \ secure-alpineVérification des limites appliquées :
bashdocker inspect limited-app | grep -A 10 "HostConfig"Cette commande lance un conteneur avec des restrictions strictes sur sa consommation de mémoire (128 MB), d'utilisation CPU (50% d'un cœur) et du nombre maximum de processus (20). Ces limites aident à prévenir les attaques par déni de service et à isoler les ressources entre conteneurs.
true||true|||true
# Par défaut, sous quel utilisateur s'exécutent les processus dans un conteneur Docker ?
[] Un utilisateur aléatoire
[x] root (UID 0)
[] docker
[] L'utilisateur qui a lancé le conteneur
# Quelle commande permet de limiter les capacités Linux d'un conteneur ?
[] docker limit --capabilities
[x] docker run --cap-drop ALL
[] docker secure --no-privileges
[] docker container --restrict
# Pourquoi est-il risqué de monter le socket Docker dans un conteneur ?
[] Cela ralentit les performances du conteneur
[] Cela empêche la communication entre conteneurs
[x] Cela peut permettre d'obtenir un accès root sur l'hôte
[] Cela désactive les mises à jour automatiques
# Quelle instruction dans un Dockerfile permet de changer l'utilisateur d'exécution ?
[x] USER
[] EXEC_AS
[] PRIVILEGES
[] RUN_AS
# Quelle est la principale différence de sécurité entre conteneurs et machines virtuelles ?
[] Les conteneurs sont toujours plus sécurisés
[] Les machines virtuelles n'ont pas de mécanismes d'isolation
[x] Les conteneurs partagent le noyau de l'hôte, contrairement aux machines virtuelles
[] Les conteneurs ne peuvent pas accéder au réseau externe
💪 Challenge
L'objectif de ce challenge est de sécuriser une application web simple déployée dans un conteneur Docker en appliquant les principes fondamentaux de sécurité vus dans cette quête.
🧩 Objectifs
- Créer un dépôt de code, gitlab.com ou github, à fournir en réponse à cette quête
- Créer un Dockerfile sécurisé pour une application web simple
- Mettre en place un utilisateur non-root
- Configurer correctement les permissions et les montages
- Limiter les capacités du conteneur
- Mettre en place une isolation réseau appropriée
⚙️ Fonctionnalités attendues
Le projet doit inclure :
- Un Dockerfile sécurisé pour une application web simple (une page HTML statique ou une API basique en Python/Node.js/Go)
- Un script de déploiement qui lance le conteneur avec les options de sécurité appropriées
- Une documentation des mesures de sécurité implémentées
📋 Spécifications techniques
- Application : Une application web simple qui expose un endpoint sur le port 8080
- Image de base : Alpine Linux ou une image "slim"
- Sécurité utilisateur : Exécution sous un utilisateur non-root
- Optimisation : L'image finale doit être aussi petite que possible
- Réseau : Utilisation d'un réseau dédié
- Ressources : Limitation appropriée des ressources (mémoire, CPU)
🛠️ Indices
- Commencer par créer une application web minimaliste (un simple serveur HTTP suffit)
- S'assurer que l'application fonctionne correctement sous un utilisateur non-root
- Utiliser
--cap-drop ALLpuis n'ajouter que les capacités strictement nécessaires - Pour les montages de volumes, toujours utiliser l'option
:ro(read-only) quand possible - Limiter les ressources avec
--memory,--cpuset--pids-limit
🔎 Critères d'acceptation
-
Sécurité de base :
- L'application ne s'exécute pas en tant que root
- Les capacités Linux sont restreintes au minimum nécessaire
- Aucun montage de volume sensible (comme /var/run/docker.sock)
-
Fonctionnalité :
- L'application reste accessible sur le port configuré
- Toutes les fonctionnalités de base fonctionnent correctement
-
Documentation :
- Un fichier README expliquant les mesures de sécurité implémentées
- Des commentaires dans le Dockerfile expliquant les choix de sécurité
-
Déploiement :
- Un script de déploiement qui configure correctement toutes les options de sécurité