Introduction
Ce guide pratique et progressif explique comment durcir Docker avec des exemples que vous pouvez exécuter dans un pilote contrôlé. L'objectif est d'établir une base saine, d'appliquer des changements de configuration sûrs, de vérifier les résultats après chaque étape et de pouvoir revenir en arrière si quelque chose se casse. Le fil conducteur : moindre privilège, secrets, permissions et exposition réseau minimale, avec des changements mesurables et réversibles.
Note de périmètre : les exemples ci-dessous sont des modèles sûrs et adaptables à votre contexte.
---
Inventaire des versions et de l'environnement
Avant tout changement, capturez l'état actuel pour comparer, valider et revenir en arrière si besoin. Limitez ce travail à un petit conteneur ou service pilote que vous pouvez inspecter localement.
- Enregistrer les versions et paramètres clés
- Commandes d'exemple (exemples construits) :
# Enregistrer la version de Docker et les infos du 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
- Choisir un service simple pour le pilote (par exemple, un petit serveur HTTP lié à localhost).
- Déterminer les chemins hôte minimaux à monter.
- Définir l'ensemble le plus restreint de ports exposés et de réseaux nécessaires.
- Confirmer les prérequis
- Hôte Linux avec Docker Engine supporté.
- Possibilité d'éditer /etc/docker/daemon.json et de redémarrer Docker.
- Droits sudo pour les étapes administratives.
Pourquoi commencer petit ? Un pilote étroit et mesurable est facile à inspecter, permet de quantifier l'effet de chaque contrôle et réduit la reprise quand des problèmes surviennent plus tard.
---
Chemin de configuration sûre
Appliquez le durcissement par couches et vérifiez après chaque couche. Le but est d'atteindre le moindre privilège et une exposition minimale sans casser la fonctionnalité requise.
1. Contrôle d'accès autour du démon Docker
- N'accordez pas largement l'appartenance au groupe
docker; cela revient pratiquement à donner les droits root sur l'hôte. - Préférez l'exécution via
sudopour les administrateurs. Pour les développeurs, envisagez Docker rootless sur les machines de dev si c'est pertinent.
Pilote optionnel : Docker rootless sur un poste développeur (exemple construit)
# Prérequis : s'assurer que 'uidmap' est installé
sudo apt-get install -y uidmap
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export PATH=/usr/bin:$PATH
# Configuration 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 que le mode rootless est signalé si utilisé.
2. Défauts du démon qui réduisent le rayon d'impact
Éditez /etc/docker/daemon.json puis redémarrez Docker. Sauvegardez le fichier au préalable.
Exemple de daemon.json (exemple construit) :
{
"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 sauf mise en réseau explicite.live-restore: trueaide les conteneurs à survivre à un redémarrage du démon.userns-remap: defaultmappe root du conteneur vers un UID/GID non privilégié sur l'hôte.- Le logging local borné évite de saturer le 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 remap d'espace de noms utilisateur : après activation, la propriété des fichiers montés en bind peut devoir correspondre à la plage remappée. Voir la section Reprise.
3. Modèle d'exécution de conteneur durci
Démarrez avec un conteneur minimal et n'ajoutez que ce qui est nécessaire. Les options ci-dessous illustrent une posture par défaut robuste.
Exemple construit : conteneur web durci
# Image et digest d'exemple construit ; 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 dans le rootfs.--tmpfs /tmp:...fournit une zone d'écriture sûre pour les fichiers temporaires.--cap-drop=ALLpuis--cap-add=NET_BIND_SERVICEappliquent le moindre privilège.--security-opt no-new-privilegesbloque l'escalade de privilèges.--security-opt seccomp=defaultconserve le filtrage d'appels système par défaut.--user 10001:10001évite d'exécuter en root dans le conteneur.-v ...:romonte des données en lecture seule depuis l'hôte.--network app_net --internalcombiné à une publication locale sur127.0.0.1resserre l'exposition réseau.
Si l'application a besoin d'autres chemins en écriture que /tmp, montez-les explicitement avec les options adaptées au lieu de désactiver globalement --read-only.
4. Gestion des secrets
Objectifs : éviter d'inclure des secrets dans les images et les variables d'environnement ; préférer des fichiers avec des 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 avec un fichier hôte (exemple construit) :
# Créer un fichier lisible uniquement par l'utilisateur in‑container (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"
Préférez le montage de fichiers aux variables d'environnement pour les secrets. Assurez l'UID/GID et le mode (0400/0440) corrects côté hôte.
5. Éviter les options et modes dangereux
- N'utilisez
--privilegedqu'en ultime recours. - Évitez
--pid=host,--net=hostet--ipc=hostdans le pilote ; ils élargissent le rayon d'impact. - Publiez les ports explicitement et liez à loopback si le service est local uniquement, par ex.
-p 127.0.0.1:8080:8080.
6. Exposition distante de l'API
- N'exposez jamais l'API Docker non authentifiée sur
tcp://0.0.0.0:2375. - Si un accès distant est requis, activez TLS et appliquez des règles de pare-feu strictes.
Extrait daemon.json avec TLS (exemple construit) :
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tls": true,
"tlscert": "/etc/docker/certs.d/server-cert.pem",
"tlskey": "/etc/docker/certs.d/server-key.pem",
"tlscacert": "/etc/docker/certs.d/ca.pem",
"tlsverify": true
}
7. Hygiène d'images et épinglage
- Épinglez les images par digest :
repo: tag@sha256:<digest>pour garantir la reproductibilité. - Gardez les images de base minimales et supprimez les paquets inutiles dans vos images.
- Optionnellement, définissez
DOCKER_CONTENT_TRUST=1lors des pulls et exécutions si 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. Elles empêchent un conteneur d'épuiser les ressources de l'hôte.
---
Carte rapide des contrôles
Le tableau indique où définir chaque contrôle et le résultat attendu.
| Domaine | Contrôle | Où le définir | Exemple | Résultat attendu |
|---|---|---|---|---|
| Accès | Restreindre le groupe docker | Hôte | Gérer l'appartenance via sudo | Moins d'accès équivalents root |
| Démon | userns-remap | /etc/docker/daemon.json | "userns-remap": "default" | Root conteneur mappé vers UID non privilégié |
| Exécution | Utilisateur non-root | Dockerfile/options run | --user 10001:10001 | Application sans droits root |
| Exécution | Capacités | Options run | --cap-drop=ALL --cap-add=NET_BIND_SERVICE | Moindre privilège |
| Exécution | No new privileges | Options run | --security-opt no-new-privileges | Bloque l'escalade |
| Exécution | Profil seccomp | Options run | --security-opt seccomp=default | Filtrage d'appels système |
| FS | Root en lecture seule | Options run | --read-only; --tmpfs /tmp | Écritures inattendues bloquées |
| Secrets | Fichiers montés ou Swarm | Options run/service | -v secret:...:ro; --secret ... | Pas de secrets en env ou image |
| Réseau | Exposition minimale | Options run/réseaux | -p 127.0.0.1:8080:8080; --internal | Surface d'attaque réduite |
| Limites | Mémoire/CPU/PIDs | Options run | --memory 256m --cpus 1 --pids-limit 200 | Usage contenu |
---
Vérification et diagnostics
Vérifiez chaque contrôle avec des tests ciblés et enregistrez les résultats.
- Paramètres du démon
sudo docker info | grep -E "userns|Live Restore|Security Options"
cat /etc/docker/daemon.json
Attendu : userns activé, Live Restore à true, et des options de sécurité (apparmor/selinux si applicable).
- Options d'exécution du conteneur (nom d'exemple : 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 : un UID non-root, rootfs en lecture seule, CapDrop avec ALL, NoNewPrivs présent, limites non nulles.
- Contrôles dans le conteneur
sudo docker exec web id -u
sudo docker exec web sh -c 'test -w / || echo "rootfs is read-only"'
sudo docker exec web sh -c 'grep NoNewPrivs /proc/1/status || true'
sudo docker exec web sh -c 'grep CapEff /proc/1/status'
Attendu : UID différent de 0, rootfs non inscriptible, ligne NoNewPrivs: 1 (selon noyau), masque de capacités réduit.
- Secrets
sudo docker exec web sh -c 'stat -c "%U %G %a %n" /run/secrets/* 2>/dev/null || true'
Attendu : des fichiers de secret existent seulement si montés ; modes 400/440 et possession par l'utilisateur applicatif.
- Exposition réseau
sudo ss -ltnp | grep 8080
sudo docker network inspect app_net | grep -E '"Internal":|"Subnet"'
Attendu : service à l'écoute sur 127.0.0.1:8080, réseau avec "Internal": true.
- Santé et journaux
sudo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
sudo docker logs --tail 50 web
Attendu : conteneur healthy, pas d'erreurs de permission dues au rootfs read-only ou aux capacités manquantes.
---
Modes d'échec et reprise
Lors du durcissement, les problèmes les plus fréquents concernent les permissions, les capacités et les chemins en écriture. Prévoyez un plan de rollback simple.
Problèmes courants
| Symptôme | Cause probable | Correctif rapide |
|---|---|---|
| L'application ne peut pas écrire | --read-only bloque les écritures | Monter un chemin en écriture : -v /srv/app/data:/app/data: rw ou --tmpfs /var/cache/app |
| Permission refusée sur un bind mount | userns-remap change le mapping UID/GID | chown -R <uid_remappé>:<gid_remappé> /srv/app/data (exemple construit : 165536:165536) |
| Impossible d'écouter sur le port 80 en non-root | Capacité NET_BIND_SERVICE manquante | Ajouter --cap-add=NET_BIND_SERVICE ou utiliser un port >= 1024 |
| Appels réseau échouent depuis un réseau interne | --internal bloque l'egress | Utiliser un réseau non‑interne pour les services ayant besoin d'egress |
| Fichier secret illisible | UID/GID ou mode incorrect côté hôte | Définir -o <uid> -g <gid> -m 0400 et monter :ro |
| Processus tué (OOM ou PIDs) | Limites trop strictes | Augmenter --memory ou --pids-limit avec marge mesurée |
Spécificités du remap d'espace de noms utilisateur
Après userns-remap, root du conteneur correspond à une plage non privilégiée sur l'hôte, souvent à partir d'un UID élevé comme 165536 (exemple construit). Les fichiers montés depuis l'hôte doivent être possédés par cet UID/GID pour être inscriptibles dans le conteneur.
Correctif d'exemple pour un montage inscriptible :
# Déterminer l'UID/GID remappés (exemple construit suppose 165536)
grep dockremap /etc/subuid /etc/subgid || true
# Ajuster la propriété d'un répertoire de données
sudo chown -R 165536:165536 /srv/app/data
Revenir en arrière sur les changements du démon
- Restaurer le précédent daemon.json
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 : le redémarrage de Docker peut arrêter des conteneurs sauf si live-restore est activé. Prévoyez un créneau de maintenance pour la production.
Revenir en arrière sur l'exécution d'un conteneur
- Arrêter le conteneur durci et le relancer avec les options antérieures connues.
sudo docker stop web
# Relance temporaire sans rootfs read-only et avec des capacités plus larges (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 l'option fautive.
---
Liste de contrôle d'exploitation
Utilisez cette checklist concise pour chaque service durci. Cochez N/A si non applicable.
Accès et démon
- [ ] Revoir l'appartenance au groupe docker ; retirer les comptes inutiles
- [ ] Confirmer
userns-remapactivé et opérationnel - [ ] Activer
live-restoreet des options de logs bornées
Image et exécution
- [ ] Épingler l'image par digest
- [ ] Exécuter sous un utilisateur non-root
- [ ] Supprimer toutes les capacités ; ne rajouter que le nécessaire
- [ ] Activer
no-new-privileges - [ ] Utiliser le profil seccomp par défaut ou plus strict
Système de fichiers et secrets
- [ ] Activer
--read-only - [ ] Fournir les zones d'écriture via
--tmpfsou montages dédiés - [ ] Monter les secrets en lecture seule avec UID/GID et mode corrects
Réseau et exposition
- [ ] Utiliser des réseaux définis par l'utilisateur ; internes si possible
- [ ] Lier les ports publiés à loopback sauf besoin externe
- [ ] Garder l'API Docker distante désactivée ; sinon TLS et pare-feu
Limites et santé
- [ ] Définir des limites mémoire, CPU et PIDs
- [ ] Configurer un healthcheck si pertinent
Vérification
- [ ] Capturer les champs clés de
docker inspectdu service - [ ] Valider dans le conteneur : non-root, rootfs read-only, capacités limitées
- [ ] Confirmer la possession et les permissions des secrets
- [ ] Confirmer que seuls les ports requis sont à l'écoute
Reprise
- [ ] Sauvegarder daemon.json avant les changements
- [ ] Documenter les anciennes options run pour un rollback rapide
---
Conclusion
Vous disposez désormais d'un chemin mesuré et reproductible pour durcir Docker dans des conditions proches de la production : établir une base de référence, appliquer des contrôles par couches (accès, exécution, système de fichiers, secrets, réseau, limites), puis vérifier chaque résultat avec des tests ciblés. Commencez par un pilote étroit que vous pouvez inspecter localement, consignez ce qui fonctionne et étendez progressivement la même approche aux autres services. Gardez les instructions de rollback à portée de main pour aller vite sans rester bloqué si quelque chose se casse.