Introduction
Utiliser Docker en production ne se limite pas à construire et expédier des images. La sécurité est une responsabilité opérationnelle continue qui englobe la configuration, la surveillance, la maintenance, les sauvegardes et les mises à niveau. Sans processus reproductible, les équipes s'exposent à des risques d'évasion de conteneur, d'élévation de privilèges, de fuite de secrets et d'attaques de la chaîne d'approvisionnement.
Cet article fournit une liste de contrôle opérationnelle pratique pour la sécurité Docker en production. Chaque section inclut des commandes concrètes, des extraits de configuration, des sorties attendues et des étapes de vérification. L'objectif est de donner aux développeurs, consultants DevOps et équipes techniques de startups une référence adaptable à leurs propres environnements.
Nous couvrons :
- L'inventaire des versions Docker et les prérequis d'environnement
- L'application d'un chemin de configuration sûr avec un durcissement ciblé
- La vérification de la posture de sécurité avec des contrôles observables
- La gestion des modes de défaillance et les procédures de récupération
- Une liste de contrôle opérationnelle reproductible pour une sécurité continue
À la fin, vous disposerez d'un ensemble clair d'actions pour durcir les hôtes Docker, appliquer des politiques d'exécution, détecter les anomalies et récupérer après incident.
Inventaire des versions et de l'environnement
Avant toute modification de sécurité, documentez l'environnement Docker actuel. Cela inclut les versions du moteur Docker et de la CLI, le runtime de conteneur, le système d'exploitation hôte, les pilotes de stockage et de réseau, ainsi que les paramètres actuels liés à la sécurité.
Vérifier les versions Docker
Exécutez la commande suivante sur chaque hôte Docker :
docker version
Exemple de sortie (tronquée) :
Client: Docker Engine - Community
Version: 24.0.5
API version: 1.43
Go version: go1.20.4
Git commit: ced0996
Built: Fri Jul 21 19:01:22 2023
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 24.0.5
API version: 1.43 (minimum version 1.12)
Go version: go1.20.4
Git commit: a61e2b4
Built: Fri Jul 21 19:01:22 2023
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.6.21
GitCommit: 3dce8eb055cbb6878b1e53c1b34f8f1b3e3f0c1a
runc:
Version: 1.1.7
GitCommit: v1.1.7-0-g860f061
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Notez la version du moteur et les composants (containerd, runc). Les correctifs de sécurité ciblent souvent des versions spécifiques, donc maintenir cet inventaire à jour est essentiel.
Inspecter la configuration de sécurité actuelle
Utilisez docker info pour voir les paramètres de sécurité généraux :
docker info --format '{{json .SecurityOptions}}'
Exemple de sortie :
["name=seccomp,profile=builtin","name=cgroupns"]
Si vous voyez name=userns dans la sortie, le remappage des espaces de noms utilisateur est activé. Sinon, il peut être candidat à un durcissement (discuté plus loin).
Vérifiez également le profil seccomp par défaut et le statut d'AppArmor/SELinux :
docker info | grep -E 'Security Options|Kernel Version|Operating System'
Identifier les conteneurs privilégiés
Listez les conteneurs en cours d'exécution et vérifiez le mode privilégié :
docker ps --quiet | xargs docker inspect --format '{{.Name}}: Privileged={{.HostConfig.Privileged}}'
Exemple de sortie :
/mysql: Privileged=false
/web: Privileged=true
/cache: Privileged=false
Le conteneur /web s'exécute en mode privilégié, ce qui est risqué. Vous traiterez cela dans le chemin de configuration sûr.
Prérequis
Assurez-vous d'avoir :
- Accès SSH aux hôtes Docker avec sudo (ou équivalent)
- Un environnement de test ou de préproduction pour valider les changements avant la production
- Sauvegarde de la configuration du démon Docker (
/etc/docker/daemon.json) - Sauvegarde des fichiers d'unité systemd ou scripts d'init pour Docker
- Capacité à redémarrer le démon Docker (nécessite une fenêtre de maintenance planifiée)
- Outils d'agrégation de journaux et de surveillance (par exemple, ELK, Prometheus, Datadog)
Documentez l'inventaire dans un tableur ou une base de données de gestion de configuration. Cette base sera utilisée pour la vérification ultérieure.
Chemin de configuration sûr
Appliquez le durcissement de sécurité de manière contrôlée et incrémentale. Commencez par les changements à plus faible risque et plus fort impact : configuration du démon, paramètres d'exécution des conteneurs et politiques réseau. Testez toujours sur un hôte hors production d'abord.
1. Durcir le démon Docker
Le démon Docker s'exécute avec les privilèges root et contrôle tous les conteneurs. Un démon mal configuré peut exposer l'hôte. Modifiez /etc/docker/daemon.json (créez-le s'il n'existe pas) et ajoutez ou modifiez ce qui suit :
{
"icc": false,
"log-level": "info",
"userland-proxy": false,
"no-new-privileges": true,
"live-restore": true,
"iptables": true,
"ip6tables": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
}
}
Paramètres clés :
"icc": false: Désactive la communication inter-conteneurs par défaut. Les conteneurs doivent être explicitement liés ou sur le même réseau défini par l'utilisateur pour communiquer."no-new-privileges": true: Empêche les conteneurs d'obtenir des privilèges supplémentaires via les binaires setuid/setgid ou d'autres mécanismes."live-restore": true: Maintient les conteneurs en cours d'exécution lorsque le démon Docker est arrêté, réduisant les temps d'arrêt lors des mises à niveau.
Après modification, redémarrez le démon Docker :
sudo systemctl restart docker
Vérifiez les paramètres :
docker info --format '{{json .SecurityOptions}}'
La sortie attendue devrait inclure no-new-privileges. En fait, no-new-privileges est un drapeau par conteneur ; le paramètre du démon n'y est pas affiché. Vérifiez plutôt docker info pour les paramètres au niveau du démon :
sudo docker info | grep -i 'Default Runtime\|Cgroup Driver\|Security Options'
Mais pour vérifier icc: false, exécutez :
docker network inspect bridge | grep -i 'com.docker.network.bridge.enable_icc'
Sortie attendue :
"com.docker.network.bridge.enable_icc": "false"
2. Activer le remappage des espaces de noms utilisateur
Les espaces de noms utilisateur mappent l'utilisateur root du conteneur vers un utilisateur non root sur l'hôte, limitant l'impact des évasions de conteneur. C'est un contrôle de sécurité puissant mais peut casser certaines charges de travail (par exemple, celles nécessitant des permissions de fichiers spécifiques). Testez minutieusement.
Pour l'activer, ajoutez à /etc/docker/daemon.json :
{
"userns-remap": "default"
}
Puis redémarrez Docker :
sudo systemctl restart docker
Vérifiez que l'utilisateur remappé existe :
id dockremap
Exemple de sortie :
uid=140000(dockremap) gid=140000(dockremap) groups=140000(dockremap)
Remarque : Si vous avez des conteneurs ou volumes existants, ils peuvent avoir des problèmes de permission après activation. Prévoyez de recréer les conteneurs et d'ajuster la propriété des volumes. Référez-vous à la documentation Docker pour les étapes de migration.
3. Restreindre les capacités des conteneurs
Par défaut, les conteneurs Docker reçoivent un sous-ensemble de capacités Linux. Vous pouvez réduire davantage les privilèges en supprimant toutes les capacités et en n'ajoutant que celles nécessaires. Utilisez les drapeaux --cap-drop et --cap-add.
Exemple : Exécuter un conteneur Nginx avec des capacités minimales :
docker run -d --name web \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
nginx:alpine
Ici, nous supprimons toutes les capacités, puis ajoutons uniquement NET_BIND_SERVICE (pour se lier au port 80). Nous définissons également no-new-privileges pour empêcher l'élévation de privilèges.
Vérifiez les capacités d'un conteneur en cours d'exécution :
docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}' web
Sortie attendue :
[NET_BIND_SERVICE] [ALL]
4. Appliquer des limites de ressources
Une utilisation illimitée des ressources peut conduire à un déni de service. Définissez des limites de mémoire et de CPU sur les conteneurs :
docker run -d --name api \
--memory 512m \
--cpus 1.5 \
--pids-limit 100 \
myapp:1.0
Ces drapeaux garantissent que le conteneur utilise au maximum 512 Mo de RAM, 1,5 cœur CPU et 100 processus/threads. --pids-limit empêche les bombes fork.
Vérifiez les limites :
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}} {{.HostConfig.PidsLimit}}' api
Sortie attendue :
536870912 1500000000 100
5. Utiliser un système de fichiers racine en lecture seule
De nombreuses applications peuvent fonctionner avec un système de fichiers racine en lecture seule, empêchant les attaquants d'écrire des fichiers malveillants. Utilisez --read-only et montez des volumes inscriptibles pour des chemins spécifiques (par exemple, /tmp, /var/run).
docker run -d --name web \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
nginx:alpine
Vérifiez :
docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' web
Sortie attendue :
true
6. Sécuriser la socket Docker
La socket Docker (/var/run/docker.sock) accorde un contrôle total sur le démon. Ne l'exposez jamais aux conteneurs sauf nécessité absolue. Si un conteneur doit gérer Docker (par exemple, Portainer), utilisez un proxy comme docker-socket-proxy avec des permissions restreintes.
Vérifiez si un conteneur monte la socket :
docker ps --quiet | xargs docker inspect --format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}YES{{end}}{{end}}'
Si un conteneur affiche YES, évaluez s'il a réellement besoin de la socket et restreignez avec un proxy.
7. Segmentation du réseau
Utilisez des réseaux bridge définis par l'utilisateur pour isoler les conteneurs. Évitez d'utiliser le réseau bridge par défaut en production.
Créez un réseau :
docker network create --driver bridge frontend
Exécutez des conteneurs attachés à ce réseau :
docker run -d --name web --network frontend nginx:alpine
La communication entre conteneurs sur différents réseaux est bloquée sauf autorisation explicite.
8. Configurer la journalisation et la surveillance
Assurez-vous que les journaux sont capturés et rotés. La configuration du démon ci-dessus définit la journalisation en fichier JSON avec rotation. Pour la production, envisagez d'envoyer les journaux vers un système central en utilisant un pilote de journalisation comme fluentd ou splunk.
Exemple d'ajout à daemon.json :
{
"log-driver": "fluentd",
"log-opts": {
"fluentd-address": "localhost:24224",
"tag": "docker.{{.Name}}"
}
}
Puis redémarrez Docker. Testez en exécutant un conteneur et en vérifiant la destination des journaux.
Vérification et diagnostic
Après avoir appliqué les changements, vérifiez que les contrôles de sécurité sont efficaces. Utilisez des outils automatisés et des vérifications manuelles.
1. Exécuter Docker Bench for Security
Docker Bench for Security est un script qui vérifie les meilleures pratiques courantes. Exécutez-le sur l'hôte :
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo ./docker-bench-security.sh
La sortie est un long rapport avec des éléments [WARN], [INFO] et [PASS]. Exemple d'extrait :
[INFO] 1 - Host Configuration
[WARN] 1.1 - Ensure a separate partition for containers has been created
[PASS] 1.2 - Ensure the container host has been Hardened
[PASS] 1.3 - Ensure Docker is up to date
[INFO] 2 - Docker daemon configuration
[WARN] 2.1 - Ensure network traffic is restricted between containers on the default bridge
[PASS] 2.2 - Ensure the logging level is set to 'info'
...
Traitez les avertissements selon votre environnement. Notez que certains avertissements peuvent être acceptables en raison de contraintes opérationnelles ; documentez les exceptions.
2. Scanner les images pour les vulnérabilités
Utilisez des outils de scan d'images comme Trivy, Clair ou Docker Scout. Exemple avec Trivy :
trivy image nginx:alpine
Exemple de sortie (tronquée) :
nginx:alpine (alpine 3.18.3)
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
Si des vulnérabilités sont trouvées, mettez à jour l'image de base ou appliquez des correctifs. Intégrez le scan dans votre pipeline CI/CD pour détecter les problèmes tôt.
3. Vérifier les profils Seccomp et AppArmor
Vérifiez que les conteneurs utilisent le profil seccomp par défaut (ou un profil personnalisé).
docker inspect --format '{{.HostConfig.SecurityOpt}}' web
La sortie attendue inclut seccomp=... et éventuellement apparmor=... si AppArmor est activé.
Si vous avez un profil seccomp personnalisé, testez-le en tentant un appel système interdit à l'intérieur du conteneur. Par exemple, avec un profil qui bloque mkdir, exécuter mkdir /tmp/test devrait échouer avec Operation not permitted.
4. Tester le remappage des espaces de noms utilisateur
Après activation de userns-remap, exécutez un conteneur et vérifiez la propriété des fichiers créés dans un volume du point de vue de l'hôte.
docker run -d --name test -v /tmp/testvol:/data alpine sleep 300
docker exec test touch /data/testfile
ls -l /tmp/testvol
La sortie attendue montre testfile appartenant à dockremap (par exemple, 140000) plutôt qu'à root.
5. Confirmer l'absence de conteneurs privilégiés
Relancez la vérification des conteneurs privilégiés :
docker ps --quiet | xargs docker inspect --format '{{.Name}}: Privileged={{.HostConfig.Privileged}}'
La sortie doit afficher Privileged=false pour tous les conteneurs. Si certains sont true, remédiez immédiatement.
6. Vérifier l'isolation réseau
Testez la communication inter-conteneurs. Avec icc: false, les conteneurs sur le bridge par défaut ne peuvent pas communiquer. Créez deux conteneurs de test sur le bridge par défaut :
docker run -d --name test1 alpine sleep 300
docker run -d --name test2 alpine sleep 300
Obtenez l'IP de test1 :
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' test1
Exemple de sortie : 172.17.0.2
Depuis test2, essayez de pinger test1 :
docker exec test2 ping -c 2 172.17.0.2
Résultat attendu : le ping échoue (100% de perte de paquets) car icc est désactivé. S'ils doivent communiquer, placez-les sur un réseau défini par l'utilisateur.
7. Surveiller les anomalies d'exécution
Utilisez des outils comme Falco ou Sysdig pour détecter les comportements suspects. Installez Falco sur l'hôte selon sa documentation. Ensuite, testez en exécutant un conteneur qui tente d'écrire dans /etc à l'intérieur du conteneur :
docker run --rm -it alpine sh -c "echo bad > /etc/test"
Falco devrait générer une alerte. Exemple de ligne de journal Falco :
12:34:56.789012345: Warning Container with writable /etc directory detected (user=root command=sh container_id=abc123)
Configurez les alertes pour qu'elles aillent vers votre canal de réponse aux incidents.
Modes de défaillance et récupération
Les changements de sécurité peuvent casser les applications. Planifiez les échecs et sachez comment revenir en arrière rapidement.
Modes de défaillance courants
- L'application nécessite des privilèges : Certaines applications ont légitimement besoin de certaines capacités ou même du mode privilégié (par exemple, démons système, outils réseau). Si vous supprimez des capacités et que l'application échoue, réajoutez uniquement les capacités nécessaires.
- Le remappage des espaces de noms utilisateur casse les permissions de fichiers : Les volumes existants peuvent devenir inaccessibles en raison du mappage UID/GID. Solutions : recréez les volumes avec la bonne propriété, ou désactivez
userns-remap. - Le système de fichiers racine en lecture seule cause des erreurs d'écriture : Les applications peuvent tenter d'écrire à des emplacements arbitraires. Utilisez
--tmpfsou des montages de volume pour les chemins inscriptibles requis. - L'isolation réseau bloque le trafic légitime : Les services internes peuvent avoir besoin de communiquer sur le bridge par défaut. Utilisez des réseaux définis par l'utilisateur avec une connectivité explicite.
- Les limites de ressources causent des kills OOM ou une limitation : Des limites trop basses peuvent faire planter les conteneurs. Ajustez en fonction de l'utilisation observée.
Procédures de retour en arrière
Pour tout changement de configuration du démon, gardez une sauvegarde de /etc/docker/daemon.json. Pour revenir en arrière :
sudo cp /etc/docker/daemon.json.backup /etc/docker/daemon.json
sudo systemctl restart docker
Pour les changements au niveau du conteneur, recréez simplement le conteneur avec les drapeaux précédents.
Documentez les étapes exactes pour annuler chaque changement dans votre runbook.
Vérifications de récupération
Après le retour en arrière, vérifiez que l'environnement fonctionne :
- Exécutez
docker pspour vous assurer que les conteneurs sont en cours d'exécution. - Vérifiez les journaux d'application pour les erreurs.
- Testez la connectivité.
- Relancez les commandes de vérification pertinentes de la section précédente.
Liste de contrôle des opérations
Utilisez cette liste de contrôle pour les revues de sécurité régulières et dans le cadre de votre routine opérationnelle.
Opérations quotidiennes
- [ ] Surveiller l'utilisation des ressources de l'hôte Docker (CPU, mémoire, disque) pour les anomalies. Responsable : ingénieur DevOps de garde. Vérifiez via les alertes Prometheus.
- [ ] Vérifier les journaux du démon Docker (
journalctl -u docker) pour les erreurs ou événements de sécurité. - [ ] Examiner les journaux des conteneurs pour les activités suspectes.
- [ ] Vérifier que la surveillance/alerte (par exemple, Falco) fonctionne.
Opérations hebdomadaires
- [ ] Exécuter Docker Bench for Security et examiner les nouveaux avertissements. Responsable : responsable sécurité.
- [ ] Scanner les images pour les nouvelles vulnérabilités (si non intégré au CI/CD).
- [ ] Examiner l'accès des utilisateurs aux hôtes Docker et les permissions de la socket. Responsable : responsable infrastructure.
- [ ] Vérifier les conteneurs, images, volumes et réseaux orphelins ; nettoyer :
docker system prune -f --volumes
Attention avec --volumes car cela supprime les volumes inutilisés.
Opérations mensuelles
- [ ] Mettre à jour le moteur Docker et la CLI vers la dernière version stable. Responsable : responsable infrastructure, planifié le premier mardi de chaque mois.
- [ ] Examiner et mettre à jour les profils seccomp/AppArmor si nécessaire.
- [ ] Faire tourner les secrets et identifiants utilisés par les conteneurs (par exemple, identifiants de registre, clés API).
- [ ] Tester les procédures de sauvegarde et de restauration.
- [ ] Examiner les limites de ressources et ajuster en fonction des tendances de performance.
- [ ] Évaluer les nouvelles fonctionnalités de sécurité dans les versions Docker et planifier leur adoption.
Réponse aux incidents
En cas d'incident de sécurité :
- Isolez l'hôte ou le conteneur affecté.
- Capturez les données forensiques : journaux de conteneur, métadonnées d'image, journaux de l'hôte.
- Identifiez la vulnérabilité (par exemple, port exposé, dépendance vulnérable).
- Corrigez et redéployez.
- Documentez l'incident et mettez à jour la liste de contrôle pour éviter la récurrence.
Pièges courants et comment les éviter
- Ignorer l'exposition de la socket Docker : De nombreux développeurs montent
/var/run/docker.sockdans des conteneurs par commodité, donnant à ces conteneurs un accès équivalent à root sur l'hôte. Utilisez un proxy commedocker-socket-proxyavec un accès API en lecture seule ou restreint, et uniquement pour les conteneurs qui en ont réellement besoin.
- Exécuter des conteneurs en tant que root : Par défaut, les conteneurs Docker s'exécutent en root. Si un attaquant s'échappe du conteneur, il obtient root sur l'hôte. Utilisez la directive
USERdans les Dockerfiles pour exécuter en tant qu'utilisateur non root, et évitez--privilegedsauf nécessité absolue.
- Ne pas définir de limites de ressources : Sans limites de mémoire ou CPU, un seul conteneur peut épuiser les ressources de l'hôte, causant un déni de service. Définissez toujours
--memoryet--cpusen production, et utilisez des orchestrateurs comme Kubernetes pour appliquer des limites.
- Utiliser des images de base obsolètes : Les images de base contiennent souvent des vulnérabilités connues. Mettez régulièrement à jour les images et scannez-les avec des outils comme Trivy dans le CI/CD. Épinglez les versions d'image pour éviter les changements inattendus.
- Désactiver les fonctionnalités de sécurité pour simplifier : Désactiver seccomp, AppArmor ou SELinux pour faire fonctionner une application expose l'hôte. Créez plutôt des profils personnalisés qui n'accordent que les appels système ou permissions requis.
- Stocker des secrets dans les images : Coder en dur des mots de passe ou clés API dans les Dockerfiles ou variables d'environnement est une erreur courante. Utilisez les secrets Docker, les secrets Kubernetes ou des gestionnaires de secrets externes comme Vault, et ne jamais intégrer de secrets dans les couches d'image.
- Négliger la segmentation réseau : S'appuyer sur le réseau bridge par défaut permet à tous les conteneurs de communiquer. Utilisez des réseaux définis par l'utilisateur avec une connectivité explicite et envisagez des politiques réseau (par exemple, Calico, Cilium) dans Kubernetes.
Conclusion
La sécurité Docker n'est pas une configuration unique ; elle exige un effort opérationnel continu. Cette liste de contrôle fournit une base pour durcir votre environnement Docker, vérifier les contrôles de sécurité et récupérer après échec. Commencez par l'inventaire, appliquez les changements de configuration sûrs de manière incrémentale et établissez des cycles de revue réguliers. Adaptez la liste de contrôle à vos charges de travail et profil de risque spécifiques. En suivant ces pratiques, vous pouvez réduire considérablement la surface d'attaque de vos applications conteneurisées et maintenir une posture de sécurité robuste en production.