Introduction
Voici un guide pratique, étape par étape, pour durcir Docker avec des exemples que vous pouvez exécuter dans un projet pilote contrôlé. Vous établirez une base de référence fiable, appliquerez des modifications de configuration sécurisées, vérifierez les résultats et pourrez revenir en arrière en cas de problème. L'accent est mis sur le moindre privilège, les secrets, les permissions et la limitation de l'exposition réseau, tout en gardant les changements mesurables et réversibles.
Note sur la portée : les exemples ci-dessous sont construits pour illustrer des patterns sûrs que vous pouvez adapter à votre environnement.
---
Inventaire des versions et de l'environnement
Avant toute modification, capturez l'état actuel afin de pouvoir comparer, vérifier et revenir en arrière si nécessaire. Limitez cette démarche à un petit conteneur ou service pilote que vous pouvez inspecter localement.
- Enregistrer les versions et réglages principaux
Exemples de commandes construits :
# Enregistrer les infos Docker et noyau
sudo docker version
sudo docker info
uname -a
# Sauvegarder dans un instantané daté
sudo docker version > baseline-$(date +%F).txt
sudo docker info >> baseline-$(date +%F).txt
- Identifier le pilote et la topologie
- Choisissez un service simple pour le pilote (par exemple, un petit serveur HTTP lié à localhost).
- Déterminez les chemins hôtes minimaux qui doivent être montés.
- Déterminez le plus petit ensemble de ports exposés et de réseaux requis.
- Confirmer les prérequis
- Hôte Linux avec un Docker Engine supporté.
- Capacité d'éditer
/etc/docker/daemon.jsonet de redémarrer Docker. - Privilèges sudo pour les étapes administratives.
Pourquoi commencer petit ? Un pilote étroit et mesurable est facile à inspecter, permet de quantifier les effets de chaque contrôle et réduit la reprise lorsque des problèmes surviennent plus tard.
---
Chemin de configuration sécurisée
Appliquez le durcissement par couches et vérifiez après chaque couche. L'objectif est le moindre privilège et l'exposition minimale sans casser les fonctionnalités requises.
1. Contrôle d'accès autour du démon Docker
- N'accordez pas largement l'appartenance au groupe
docker; cela fournit effectivement un accès root sur l'hôte. - Préférez l'exécution des commandes via
sudopour les administrateurs. Pour les développeurs, envisagez Docker rootless sur les machines de développement si c'est faisable.
Pilote optionnel : Docker rootless sur un poste de travail développeur (exemple construit)
# Prérequis : s'assurer que 'uidmap' est installé, puis :
sudo apt-get install -y uidmap
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export PATH=/usr/bin:$PATH
# Configurer rootless (exemple construit)
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
Vérifiez avec docker info qu'il rapporte un mode rootless si utilisé.
2. Valeurs par défaut du démon qui réduisent la surface d'attaque
Éditez /etc/docker/daemon.json et redémarrez Docker. Sauvegardez le fichier d'abord.
Exemple construit de daemon.json :
{
"icc": false,
"live-restore": true,
"userns-remap": "default",
"log-driver": "local",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
icc: falsebloque la communication inter-conteneurs sur le bridge par défaut à moins qu'ils ne soient explicitement mis en réseau ensemble.live-restore: trueaide les conteneurs à survivre à un redémarrage du démon.userns-remap: defaultmappe le root du conteneur vers un UID/GID non privilégié sur l'hôte.- La journalisation locale avec limites de taille aide à éviter le remplissage du disque.
Appliquer et redémarrer :
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.$(date +%F)
sudo systemctl restart docker
Note sur le remappage d'espace de noms utilisateur : après l'activation, la propriété des fichiers hôtes pour les montages bind peut devoir correspondre à la plage remappée. Voir Récupération pour les détails.
3. Pattern d'exécution de conteneur durci
Commencez avec un conteneur minimal et ajoutez progressivement seulement ce qui est requis. Les drapeaux ci-dessous démontrent une position par défaut solide.
Exemple construit : conteneur web durci
# Image et digest d'exemple construits ; remplacez par les vôtres
IMAGE="mycorp/web:1.2.3@sha256:deadbeef..."
sudo docker network create --driver bridge --subnet 172.30.0.0/24 --internal app_net || true
sudo docker run --rm -d \
--name web \
--network app_net \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
--pids-limit=200 \
--memory=256m --memory-swap=256m --cpus=1.0 \
--cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
--security-opt seccomp=default \
--user 10001:10001 \
-v /srv/web/static:/srv/web/static:ro \
-p 127.0.0.1:8080:8080 \
--health-cmd='curl -sSf http://127.0.0.1:8080/health || exit 1' \
--health-interval=30s --health-retries=3 \
"$IMAGE"
Pourquoi ces choix :
--read-onlyempêche les écritures inattendues sur le système de fichiers racine.--tmpfs /tmp:...offre une zone d'écriture sûre pour les fichiers temporaires.--cap-drop=ALLpuis--cap-add=NET_BIND_SERVICEapplique le moindre privilège.--security-opt no-new-privilegesbloque l'escalade de privilèges.--security-opt seccomp=defaultmaintient le filtre d'appels système par défaut en place.--user 10001:10001évite de s'exécuter en root à l'intérieur du conteneur.-v ...:rolie les données en lecture seule depuis l'hôte.--network app_net --internalcombiné avec la publication de port local sur127.0.0.1garde l'exposition réseau étroite.
Si une application a besoin de chemins d'écriture au-delà de /tmp, montez ces chemins explicitement avec des options appropriées au lieu de désactiver --read-only globalement.
4. Gestion des secrets
Objectifs : éviter les secrets dans les images et les variables d'environnement ; privilégier les fichiers avec permissions restreintes.
- Secrets Docker Swarm (exemple construit) :
echo "constructed-db-password" | docker secret create db_password -
docker service create \
--name app \
--secret source=db_password,target=/run/secrets/db_password,mode=0400 \
"$IMAGE"
- Conteneurs autonomes utilisant un fichier hôte (exemple construit) :
# Créer un fichier lisible seulement par l'utilisateur du conteneur (UID 10001 ici)
sudo install -o 10001 -g 10001 -m 0400 /root/db_password /srv/app/db_password
sudo docker run --rm \
--user 10001:10001 \
-v /srv/app/db_password:/run/secrets/db_password:ro \
"$IMAGE"
Privilégiez les montages de fichiers aux variables d'environnement pour les secrets. Assurez-vous du bon UID/GID et du mode (0400/0440) sur le fichier hôte.
5. Éviter les drapeaux et modes dangereux
- N'utilisez pas
--privilegedsauf si absolument nécessaire. - Évitez
--pid=host,--net=hostet--ipc=hostdans le pilote ; ils élargissent la surface d'attaque. - Publiez les ports explicitement et liez à loopback si le service est destiné à être local seulement, par exemple
-p 127.0.0.1:8080:8080.
6. Exposition de l'API distante
- N'exposez pas l'API Docker non authentifiée sur
tcp://0.0.0.0:2375. - Si un accès distant est requis, activez TLS et des règles de pare-feu strictes.
Extrait daemon.json construit pour TLS :
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tls": true,
"tlscert":
"tlskey":
"tlscacert":
"tlsverify": true
}
7. Hygiène des images et verrouillage
- Verrouillez les images par digest :
repo:tag@sha256:pour assurer la reproductibilité. - Gardez les images de base minimales et supprimez les paquets inutiles dans vos propres images.
- Optionnellement, définissez
DOCKER_CONTENT_TRUST=1lors du pull et de l'exécution d'images dans les environnements où la confiance de contenu est configurée.
8. Limites de ressources
- Limites mémoire et CPU :
--memory,--memory-swap,--cpus. - Limite de nombre de processus :
--pids-limit. Ces mesures empêchent un seul conteneur d'épuiser les ressources de l'hôte.
---
Carte rapide des contrôles de durcissement
Le tableau montre où chaque contrôle est défini et le résultat attendu.
| Domaine | Contrôle | Où définir | Exemple | Résultat attendu |
|---|---|---|---|---|
| Accès | Restreindre groupe docker | Hôte | Gérer l'appartenance avec sudo | Moins d'utilisateurs avec accès équivalent root |
| Démon | userns-remap | /etc/docker/daemon.json | "userns-remap": "default" | Root conteneur mappé vers UID hôte non privilégié |
| Exécution | Utilisateur non-root | Dockerfile/drapeaux run | --user 10001:10001 | Application s'exécute sans root dans le conteneur |
| Exécution | Capacités | Drapeaux run | --cap-drop=ALL --cap-add=NET_BIND_SERVICE | Capacités au moindre privilège |
| Exécution | Pas de nouveaux privilèges | Drapeaux run | --security-opt no-new-privileges | Blocage de l'escalade de privilèges |
| Exécution | Profil seccomp | Drapeaux run | --security-opt seccomp=default | Filtrage d'appels système actif |
| Système de fichiers | Racine en lecture seule | Drapeaux run | --read-only; --tmpfs /tmp | Empêche les écritures inattendues sur rootfs |
| Secrets | Montages fichiers ou Swarm | Drapeaux run/service | -v secret:...:ro; --secret ... | Secrets pas dans env ni couches d'image |
| Réseau | Minimiser exposition | Drapeaux run/réseaux | -p 127.0.0.1:8080:8080; --internal | Surface d'attaque externe réduite |
| Limites | Mémoire/CPU/PIDs | Drapeaux run | --memory 256m --cpus 1 --pids-limit 200 | Utilisation des ressources contenue |
---
Vérification et diagnostics
Vérifiez chaque contrôle avec des contrôles ciblés et enregistrez les résultats.
- Réglages du démon
sudo docker info | grep -E "userns|Live Restore|Security Options"
cat /etc/docker/daemon.json
Attendu : userns activé, Live Restore vrai, options de sécurité incluent apparmor/selinux le cas échéant.
- Options d'exécution du conteneur (nom du conteneur construit : web)
sudo docker inspect web \
--format 'User={{.Config.User}} ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}} CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}} NoNewPrivs={{.HostConfig.SecurityOpt}} PidsLimit={{.HostConfig.PidsLimit}} Memory={{.HostConfig.Memory}} CPUs={{.HostConfig.NanoCpus}}'
Attendu : ID utilisateur non-root, read-only true, CapDrop inclut ALL, NoNewPrivs présent, limites non nulles.
- Vérifications dans le conteneur
sudo docker exec web id -u
sudo docker exec web sh -c 'test -w / || echo "rootfs est en lecture seule"'
sudo docker exec web sh -c 'grep NoNewPrivs /proc/1/status || true'
sudo docker exec web sh -c 'grep CapEff /proc/1/status'
Attendu : ID utilisateur pas 0, rootfs non inscriptible, ligne NoNewPrivs : 1 présente (selon noyau), masque de capacités effectives petit.
- Secrets
sudo docker exec web sh -c 'stat -c "%U %G %a %n" /run/secrets/* 2>/dev/null || true'
Attendu : fichiers secrets existent seulement si vous les avez montés ; modes devraient être 400/440 et appartenir à l'utilisateur de l'application.
- Exposition réseau
sudo ss -ltnp | grep 8080
sudo docker network inspect app_net | grep -E '"Internal":|"Subnet"'
Attendu : service en écoute sur 127.0.0.1:8080, réseau montre "Internal": true.
- Santé et journaux
sudo docker ps --format 'table {{.Names}} {{.Status}} {{.Ports}}'
sudo docker logs --tail 50 web
Attendu : conteneur sain, pas d'erreurs de permission dues au read-only ou aux capacités manquantes.
---
Modes de défaillance et récupération
Lors du durcissement, les problèmes les plus courants concernent les permissions, les capacités et les chemins d'écriture. Gardez un plan de retour en arrière simple.
Problèmes courants
| Symptôme | Cause probable | Correction rapide |
|---|---|---|
| L'application ne peut pas écrire au chemin attendu | --read-only bloque les écritures | Monter un chemin inscriptible : -v /srv/app/data:/app/data:rw ou ajouter --tmpfs /var/cache/app |
| Permission refusée sur montage bind | userns-remap a changé le mapping UID/GID hôte | chown -R <uid_remappe>:<gid_remappe> /srv/app/data (exemple construit : 165536:165536) |
| Impossible de lier le port 80 en non-root | Capacité NET_BIND_SERVICE manquante | Ajouter --cap-add=NET_BIND_SERVICE ou utiliser port >= 1024 |
| Appels réseau échouent depuis réseau interne | --internal bloque la sortie | Utiliser un réseau non-interne pour les services nécessitant la sortie |
| Fichier secret illisible | Mauvais UID/GID ou mode sur l'hôte | Définir -o <uid> -g <gid> -m 0400 sur le fichier secret et monter :ro |
| Processus tué par OOM (Out of Memory) ou limite pids | Limites trop serrées | Augmenter --memory ou --pids-limit avec marge mesurée |
Spécificités du remappage d'espace de noms utilisateur
Après userns-remap, le root du conteneur mappe vers une plage non privilégiée sur l'hôte, typiquement à partir d'un UID élevé comme 165536 (exemple construit). Les fichiers montés depuis l'hôte doivent appartenir à cet UID/GID pour être inscriptibles à l'intérieur du conteneur.
Exemple de correction construit pour un montage inscriptible :
# Déterminer l'UID/GID remappé (exemple construit suppose 165536)
grep dockremap /etc/subuid /etc/subgid || true
# Ajuster la propriété pour un répertoire de données
sudo chown -R 165536:165536 /srv/app/data
Retour en arrière des changements du démon
- Restaurer le
daemon.jsonprécédent
sudo cp /etc/docker/daemon.json.bak.$(date +%F -d 'yesterday' 2>/dev/null || echo backup) /etc/docker/daemon.json
- Redémarrer le démon
sudo systemctl restart docker
Note : redémarrer Docker peut arrêter les conteneurs sauf si live-restore est activé. Prévoyez une fenêtre de maintenance pour les changements en production.
Retour en arrière d'une exécution de conteneur
- Arrêter le conteneur durci et le redémarrer avec les drapeaux qui fonctionnaient auparavant.
sudo docker stop web
# Relancer sans read-only et avec capacités plus larges comme retour temporaire (exemple construit)
sudo docker run -d --name web --cap-drop=NET_RAW -p 127.0.0.1:8080:8080 "$IMAGE"
Documentez les différences et réappliquez le durcissement étape par étape pour isoler le drapeau qui casse.
---
Liste de contrôle opérationnelle
Utilisez cette liste de contrôle concise pour chaque service que vous durcissez. Marquez N/A là où non applicable.
Accès et démon
- [ ] Réviser l'appartenance au groupe docker ; retirer les utilisateurs inutiles
- [ ] Confirmer userns-remap activé et fonctionnel
- [ ] Définir live-restore et options de journalisation bornées
Image et exécution
- [ ] Verrouiller l'image par digest
- [ ] Exécuter en utilisateur non-root
- [ ] Supprimer toutes les capacités ; rajouter seulement ce qui est nécessaire
- [ ] Activer no-new-privileges
- [ ] Utiliser le profil seccomp par défaut ou un plus strict personnalisé
Système de fichiers et secrets
- [ ] Activer
--read-only - [ ] Fournir les chemins inscriptibles requis via
--tmpfsou montages dédiés - [ ] Monter les secrets comme fichiers en lecture seule avec bon UID/GID et mode
Réseau et exposition
- [ ] Utiliser des réseaux définis par l'utilisateur ; internes quand possible
- [ ] Lier les ports publiés à loopback sauf si accès externe requis
- [ ] Garder l'API Docker distante désactivée ; si activée, exiger TLS et pare-feu
Limites et santé
- [ ] Définir limites mémoire, CPU et PIDs
- [ ] Configurer un healthcheck le cas échéant
Vérification
- [ ] Capturer les champs clés de docker inspect pour le service
- [ ] Valider dans le conteneur : non-root, rootfs lecture seule, capacités limitées
- [ ] Confirmer propriété et permissions des secrets
- [ ] Confirmer que seuls les ports requis sont en écoute
Récupération
- [ ] Sauvegarder daemon.json avant les changements
- [ ] Documenter les drapeaux d'exécution précédents pour permettre un retour rapide
---
Conclusion
Vous disposez maintenant d'un chemin mesuré et reproductible pour durcir Docker dans des conditions proches de la production sans deviner : établir la base de référence de l'environnement, appliquer des contrôles en couches pour l'accès, l'exécution, le système de fichiers, les secrets, le réseau et les limites, puis vérifier chaque résultat avec des contrôles ciblés. Commencez avec un pilote étroit que vous pouvez inspecter localement, enregistrez ce qui fonctionne, et étendez la même approche aux services supplémentaires. Gardez les instructions de retour en arrière à portée de main pour avancer vite sans rester bloqué quand quelque chose casse.