Durcissement de la sécurité Docker : mode rootless, Seccomp et AppArmor sur un VPS
Sept couches de durcissement pour Docker sur un VPS. Chaque section explique la menace, montre la correction en CLI et en Compose, puis vérifie le résultat.
La configuration par défaut de Docker privilégie la commodité au détriment de la sécurité. Les conteneurs tournent en tant que root sur l'hôte. Les 14 capabilities Linux restent actives. Seccomp ne bloque que 44 des 300+ appels système. Le trafic inter-conteneurs circule librement.
Sur un VPS, c'est plus problématique que sur une machine de développement locale. Vous partagez un hôte physique avec d'autres locataires. Une évasion de conteneur signifie qu'un attaquant atterrit en root sur le noyau exposé à l'hyperviseur. Chaque couche de durcissement que vous ajoutez réduit le rayon d'explosion.
Ce tutoriel couvre sept mesures de durcissement. Chaque section explique la menace qu'elle prévient, montre l'implémentation (flags docker run et syntaxe Compose), et inclut une étape de vérification. Chaque commande a été testée sur Ubuntu 24.04 avec Docker Engine 29.x.
Prérequis : Un VPS sous Debian 12 ou Ubuntu 24.04 avec Docker Engine installé. Accès SSH en tant qu'utilisateur non-root avec sudo. Si vous n'avez pas encore sécurisé l'hôte lui-même, commencez par notre guide de sécurité VPS Linux. Pour les problèmes de pare-feu Docker, voir Docker UFW Firewall Fix.
Cet article fait partie de la série .
Comment configurer Docker rootless sur Ubuntu 24.04 ou Debian 12 ?
Docker rootless exécute le daemon et tous les conteneurs sous un compte utilisateur standard au lieu de root. Si un attaquant s'échappe d'un conteneur, il atterrit en tant qu'utilisateur non privilégié sur l'hôte. Pas d'accès root. De toutes les mesures de ce guide, c'est celle qui a le plus grand impact.
Installer Docker rootless
Installez les paquets nécessaires. Le paquet uidmap fournit newuidmap et newgidmap, qui gèrent le mapping des UID/GID subordonnés :
sudo apt-get update && sudo apt-get install -y uidmap docker-ce-rootless-extras
Vérifiez que votre utilisateur dispose d'au moins 65 536 UID et GID subordonnés :
grep "^$(whoami):" /etc/subuid
grep "^$(whoami):" /etc/subgid
Vous devriez voir une sortie du type deploy:100000:65536. Si les entrées sont absentes, ajoutez-les :
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
Arrêtez le daemon Docker système. Vous n'en avez pas besoin pour le mode rootless :
sudo systemctl disable --now docker.service docker.socket
Lancez l'outil de configuration rootless en tant qu'utilisateur normal (pas root) :
dockerd-rootless-setuptool.sh install
Le script affiche les variables d'environnement à définir. Ajoutez-les à votre profil shell :
echo 'export PATH=/usr/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc
Activez le linger pour que le daemon rootless démarre au boot, pas seulement à la connexion :
sudo loginctl enable-linger $(whoami)
Vérifier le mode rootless
docker context use rootless
docker run --rm hello-world
Vérifiez que le processus daemon tourne sous votre utilisateur, pas root :
ps aux | grep dockerd
La sortie devrait afficher votre nom d'utilisateur, pas root. Confirmez aussi que Docker info signale le mode rootless :
docker info --format '{{.SecurityOptions}}'
Vous devriez voir rootless dans la liste.
Quand le mode rootless pose problème
Le mode rootless a de vraies limitations. Les connaître évite des heures de débogage.
| Limitation | Cause | Contournement |
|---|---|---|
| Impossible de binder les ports inférieurs à 1024 | Les utilisateurs non-root ne peuvent pas binder les ports privilégiés | Définir sysctl net.ipv4.ip_unprivileged_port_start=0 ou utiliser un reverse proxy sur l'hôte |
| Erreurs de permissions sur les bind mounts | Les fichiers de l'hôte appartenant à root sont inaccessibles pour l'UID remappé | Changer la propriété vers votre utilisateur, ou utiliser des volumes nommés |
| Système de fichiers overlay plus lent | Le mode rootless utilise fuse-overlayfs au lieu de overlay2 natif |
Accepter le surcoût (5-15 % pour les charges I/O intensives) ou utiliser overlay2 natif avec --privileged (ce qui annule l'intérêt) |
Pas de --net=host |
Le réseau rootless utilise slirp4netns ou pasta, pas la pile réseau de l'hôte | Utiliser le port mapping (-p) à la place. Pour de meilleures performances, installer pasta comme driver réseau |
ping échoue dans les conteneurs |
CAP_NET_RAW est restreint |
Installer slirp4netns >= 0.4.0 ou utiliser pasta |
Note sur les performances réseau : Par défaut, Docker rootless utilise slirp4netns pour le réseau, ce qui ajoute du NAT. Le driver pasta copie la config réseau de l'hôte dans le namespace du conteneur sans NAT et offre un meilleur débit. Sur Debian 12 et Ubuntu 24.04, installez-le avec :
sudo apt-get install -y passt
Docker détecte pasta automatiquement s'il est installé.
Quand utiliser le user namespace remapping au lieu de Docker rootless ?
Le user namespace remapping (userns-remap) mappe l'UID 0 dans les conteneurs vers un UID non privilégié sur l'hôte. Contrairement à Docker rootless, le daemon lui-même tourne toujours en root. Vous conservez donc toutes les fonctionnalités Docker (ports privilégiés, réseau hôte, overlay2 natif) tout en empêchant que root dans le conteneur soit root sur l'hôte.
Choisissez userns-remap quand le mode rootless casse votre workload mais que vous voulez quand même l'isolation UID. Choisissez rootless quand vous pouvez vivre avec ses limitations.
| Fonctionnalité | Docker rootless | userns-remap | Docker standard |
|---|---|---|---|
| Le daemon tourne en tant que | Utilisateur | Root | Root |
| Root conteneur = root hôte | Non | Non | Oui |
| Ports privilégiés | Contournement nécessaire | Fonctionne | Fonctionne |
--net=host |
Non | Oui | Oui |
| Driver de stockage | fuse-overlayfs | overlay2 | overlay2 |
| Complexité de mise en place | Moyenne | Faible | Aucune |
Configurer userns-remap
Créez l'utilisateur dockremap ou utilisez le raccourci default qui le crée automatiquement :
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"userns-remap": "default"
}
EOF
sudo systemctl restart docker
Docker crée l'utilisateur dockremap et ajoute les plages de UID/GID subordonnés dans /etc/subuid et /etc/subgid.
Vérifiez que ça fonctionne :
sudo ls -ld /var/lib/docker/
Vous devriez voir un nouveau sous-répertoire nommé d'après la plage UID remappée, par exemple /var/lib/docker/100000.100000/. Le nombre exact dépend de la plage de UID subordonnés assignée à l'utilisateur dockremap dans /etc/subuid.
Lancez un conteneur et vérifiez l'UID du processus sur l'hôte :
docker run -d --name test-userns nginx:alpine
ps aux | grep nginx
Le processus nginx devrait afficher un UID élevé (correspondant au premier nombre de /etc/subuid pour dockremap), pas 0.
Nettoyage :
docker rm -f test-userns
Piège sur la propriété des volumes : Les fichiers montés en bind appartenant à root (UID 0) sur l'hôte apparaissent comme nobody dans le conteneur parce que l'UID 0 est mappé vers une autre plage. Utilisez des volumes nommés ou faites un chown des fichiers vers l'UID remappé.
Comment supprimer les capabilities Linux d'un conteneur Docker ?
Docker accorde 14 capabilities Linux aux conteneurs par défaut. Chaque capability est une permission noyau qu'un attaquant peut exploiter après une évasion de conteneur ou dans un conteneur compromis. Supprimer toutes les capabilities et rajouter uniquement celles dont votre application a besoin réduit la surface d'attaque.
Capabilities Docker par défaut
| Capability | Ce qu'elle autorise | Garder ou supprimer ? |
|---|---|---|
CAP_CHOWN |
Changer la propriété des fichiers | Supprimer sauf si nécessaire |
CAP_DAC_OVERRIDE |
Contourner les vérifications de permissions lecture/écriture | Supprimer sauf si nécessaire |
CAP_FOWNER |
Contourner les vérifications de permissions sur le propriétaire | Supprimer sauf si nécessaire |
CAP_FSETID |
Définir les bits setuid/setgid | Supprimer |
CAP_KILL |
Envoyer des signaux à n'importe quel processus | Supprimer sauf si nécessaire |
CAP_SETGID |
Changer le GID du processus | Garder pour la plupart des applications |
CAP_SETUID |
Changer l'UID du processus | Garder pour la plupart des applications |
CAP_SETPCAP |
Modifier les capabilities du processus | Supprimer |
CAP_NET_BIND_SERVICE |
Binder les ports inférieurs à 1024 | Garder si vous bindez le port 80/443 |
CAP_NET_RAW |
Utiliser les sockets raw (forger des paquets) | Supprimer sauf besoin de ping/traceroute |
CAP_SYS_CHROOT |
Utiliser chroot | Supprimer |
CAP_MKNOD |
Créer des fichiers de périphérique | Supprimer |
CAP_AUDIT_WRITE |
Écrire dans le journal d'audit du noyau | Supprimer sauf si nécessaire |
CAP_SETFCAP |
Définir les capabilities de fichier | Supprimer |
Tout supprimer, rajouter le nécessaire
Syntaxe CLI :
docker run -d \
--cap-drop ALL \
--cap-add CHOWN \
--cap-add NET_BIND_SERVICE \
--cap-add SETUID \
--cap-add SETGID \
--name hardened-nginx \
nginx:alpine
Nginx a besoin de CHOWN parce que son script d'entrypoint change la propriété des répertoires de cache au démarrage. Sans cette capability, le conteneur s'arrête immédiatement avec une erreur chown: Operation not permitted.
Syntaxe Compose :
services:
web:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
Vérifier que les capabilities sont supprimées
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep Cap'
Comparez le bitmask CapEff (capabilities effectives). Avec toutes supprimées et quatre rajoutées, la valeur est bien inférieure au 00000000a80425fb par défaut.
Pour une sortie lisible, installez capsh sur l'hôte et décodez l'hexadécimal :
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep CapEff' | awk '{print $2}' | xargs -I{} capsh --decode=0x{}
Vous devriez voir uniquement cap_chown,cap_setgid,cap_setuid,cap_net_bind_service dans la sortie.
Nettoyage :
docker rm -f hardened-nginx
Que prévient le flag no-new-privileges ?
Le flag no-new-privileges empêche les processus à l'intérieur d'un conteneur d'acquérir des privilèges supplémentaires via des binaires setuid ou setgid. Sans ce flag, un processus compromis peut exécuter un binaire setuid (comme su ou sudo) et escalader vers root. Avec le flag activé, le noyau refuse l'escalade de privilèges.
Appliquer par conteneur
CLI :
docker run -d --security-opt no-new-privileges:true --name no-priv-test nginx:alpine
Compose :
services:
web:
image: nginx:alpine
security_opt:
- no-new-privileges:true
Appliquer comme défaut du daemon
Ajoutez-le dans daemon.json pour que chaque conteneur reçoive ce flag automatiquement :
{
"no-new-privileges": true
}
Redémarrez Docker après modification :
sudo systemctl restart docker
Vérifier que ça fonctionne
docker exec no-priv-test grep NoNewPrivs /proc/1/status
Sortie attendue :
NoNewPrivs: 1
La valeur 1 signifie qu'aucun nouveau privilège ne peut être acquis. La valeur 0 signifie que le flag n'est pas activé.
Nettoyage :
docker rm -f no-priv-test
Comment créer un profil seccomp personnalisé pour Docker ?
Le profil seccomp par défaut de Docker bloque environ 44 appels système sur 300+. Un profil personnalisé permet de restreindre les conteneurs aux seuls appels système que votre application utilise réellement. Si un attaquant compromet le conteneur, il ne peut pas exploiter de vulnérabilités noyau via les appels système bloqués.
Découvrir les appels système dont votre application a besoin
Utilisez strace sur un conteneur en cours d'exécution pour capturer les appels système effectués pendant le fonctionnement normal :
docker run -d --security-opt seccomp=unconfined --name trace-target nginx:alpine
# Install strace on the host
sudo apt-get install -y strace
# Get the container's PID
PID=$(docker inspect --format '{{.State.Pid}}' trace-target)
# Trace syscalls for 30 seconds during normal operation
sudo strace -f -p "$PID" -o /tmp/nginx-syscalls.log -e trace=all &
STRACE_PID=$!
sleep 30
# Send some test traffic to exercise the application
curl -s http://localhost:80 > /dev/null 2>&1 || true
kill "$STRACE_PID" 2>/dev/null
wait "$STRACE_PID" 2>/dev/null
Extrayez les noms d'appels système uniques :
grep -oP '^\[pid \d+\] \K\w+|^\w+' /tmp/nginx-syscalls.log | sort -u
Cela vous donne le jeu minimum d'appels système dont votre application a besoin.
Construire le profil personnalisé
Créez un fichier JSON. La defaultAction est SCMP_ACT_ERRNO (refuser tout ce qui n'est pas explicitement autorisé). La liste d'appels système doit inclure ce dont votre application a besoin ET ce dont le runtime conteneur (runc) a besoin pendant l'initialisation. Le profil ci-dessous a été testé avec nginx:alpine sur Docker Engine 29.x :
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept4",
"access",
"arch_prctl",
"bind",
"brk",
"capget",
"capset",
"chdir",
"chown",
"clone",
"clone3",
"close",
"close_range",
"connect",
"copy_file_range",
"dup",
"dup2",
"dup3",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"epoll_pwait2",
"epoll_wait",
"eventfd2",
"execve",
"exit",
"exit_group",
"faccessat",
"faccessat2",
"fchmod",
"fchmodat",
"fchown",
"fchownat",
"fcntl",
"fork",
"fstat",
"fstatfs",
"futex",
"getcwd",
"getdents",
"getdents64",
"getegid",
"geteuid",
"getgid",
"getpgrp",
"getpid",
"getppid",
"getrandom",
"getrlimit",
"getsockname",
"getsockopt",
"gettid",
"getuid",
"io_destroy",
"io_getevents",
"io_setup",
"io_submit",
"ioctl",
"kill",
"listen",
"lseek",
"lstat",
"madvise",
"memfd_create",
"mkdir",
"mkdirat",
"mmap",
"mount",
"mprotect",
"mremap",
"munmap",
"nanosleep",
"newfstatat",
"open",
"openat",
"pipe",
"pipe2",
"pivot_root",
"poll",
"ppoll",
"prctl",
"pread64",
"prlimit64",
"pwrite64",
"read",
"readlink",
"readlinkat",
"recvfrom",
"recvmsg",
"rename",
"renameat",
"rseq",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"sched_getaffinity",
"sched_yield",
"seccomp",
"sendfile",
"sendmsg",
"sendto",
"set_robust_list",
"set_tid_address",
"setgid",
"setgroups",
"sethostname",
"setitimer",
"setsockopt",
"setuid",
"sigaltstack",
"socket",
"socketpair",
"stat",
"statfs",
"statx",
"symlink",
"symlinkat",
"sysinfo",
"tgkill",
"umask",
"umount2",
"uname",
"unlink",
"unlinkat",
"unshare",
"utimensat",
"wait4",
"write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
Pourquoi autant d'appels système ? La liste inclut les appels système nécessaires à trois couches : le runtime conteneur (runc) pour la mise en place des namespaces (clone3, mount, pivot_root, unshare, seccomp, statx), le shell Alpine et les scripts d'entrypoint (fork, open, pipe, wait4), et nginx lui-même (accept4, bind, listen, io_setup). Une image basée sur glibc nécessiterait moins d'appels système liés au shell mais plus d'appels internes à la libc.
Sauvegardez ce fichier sous /etc/docker/seccomp-nginx.json.
Appliquer le profil personnalisé
CLI :
docker run -d \
--security-opt seccomp=/etc/docker/seccomp-nginx.json \
--name seccomp-test \
nginx:alpine
Compose :
services:
web:
image: nginx:alpine
security_opt:
- seccomp=/etc/docker/seccomp-nginx.json
Vérifier que le profil est actif
docker inspect --format '{{.HostConfig.SecurityOpt}}' seccomp-test
La sortie affiche le JSON complet du profil seccomp appliqué au conteneur. Docker lit le fichier à la création du conteneur et intègre le contenu du profil.
Testez qu'une opération restreinte échoue :
docker exec seccomp-test unshare --mount /bin/sh -c 'echo escaped'
Cela devrait échouer avec « Operation not permitted ». Le conteneur ne dispose pas de CAP_SYS_ADMIN (non accordé par défaut), et le profil seccomp fournit une seconde couche de défense en n'autorisant que les appels système nécessaires au fonctionnement normal.
Nettoyage :
docker rm -f seccomp-test
rm -f /tmp/nginx-syscalls.log
Conseil pour la production : Partez du profil par défaut de Docker et retirez des appels système plutôt que de construire de zéro. L'approche par strace donne le profil le plus restreint possible mais nécessite des tests approfondis. Exercez chaque chemin de code de votre application pendant la capture strace : démarrage, requêtes normales, gestion d'erreurs, arrêt propre (docker stop) et rotation des logs.
Approche itérative : Si construire à partir de strace vous semble risqué, utilisez ce workflow plus sûr :
- Copiez le profil seccomp par défaut de Docker comme point de départ.
- Lancez votre conteneur avec ce profil copié. Il se comporte de façon identique au profil par défaut.
- Retirez les appels système par groupes (par ex. tous les
key*, tous lesswap*). - Testez le conteneur après chaque suppression. Si ça casse, rajoutez le dernier appel système retiré.
- Répétez jusqu'à avoir retiré tout ce dont votre application n'a pas besoin.
C'est plus lent que la méthode strace mais plus sûr pour les conteneurs de production où un appel système manquant pourrait causer des défaillances intermittentes.
Comment écrire un profil AppArmor pour un conteneur Docker ?
AppArmor restreint les fichiers, ressources réseau et capabilities auxquels un processus conteneur peut accéder. Docker applique automatiquement un profil docker-default par défaut. Un profil personnalisé permet de restreindre davantage les conteneurs aux seuls chemins du système de fichiers et opérations réseau dont ils ont besoin.
Vérifier qu'AppArmor est actif
sudo aa-status
Vous devriez voir docker-default dans la liste des profils chargés. Si AppArmor n'est pas installé :
sudo apt-get install -y apparmor apparmor-utils
Écrire un profil personnalisé
Créez /etc/apparmor.d/containers/docker-nginx :
#include <tunables/global>
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/nameservice>
# Capabilities needed by nginx
capability chown,
capability setuid,
capability setgid,
capability net_bind_service,
capability dac_override,
# Network access
network inet tcp,
network inet udp,
network inet6 tcp,
network inet6 udp,
# Shell and entrypoint scripts
/bin/** rix,
/usr/bin/** rix,
/usr/sbin/** rix,
/lib/** mr,
/usr/lib/** mr,
/docker-entrypoint.sh rix,
/docker-entrypoint.d/ r,
/docker-entrypoint.d/** rix,
/dev/null rw,
/dev/stdout rw,
/dev/stderr rw,
# Nginx binary
/usr/sbin/nginx ix,
# Config files (read only)
/etc/ r,
/etc/nginx/ r,
/etc/nginx/** r,
# Web root (read only)
/usr/share/nginx/html/** r,
# Temp and cache directories
/var/cache/nginx/ rw,
/var/cache/nginx/** rw,
/var/log/nginx/ rw,
/var/log/nginx/** rw,
/run/ rw,
/run/** rw,
/tmp/ rw,
/tmp/** rw,
# Proc filesystem (needed for nginx worker management)
/proc/** r,
# Deny sensitive files
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/*/mem r,
deny /sys/** w,
}
Le profil a besoin de déclarations capability parce qu'AppArmor contrôle l'utilisation des capabilities indépendamment des flags --cap-add de Docker. Les chemins du script d'entrypoint (/docker-entrypoint.sh, /docker-entrypoint.d/) et les binaires shell (/bin/**) doivent être explicitement autorisés, sinon le conteneur ne démarre pas. La permission rix signifie lecture, héritage du contexte d'exécution et autorisation d'exécution.
Charger et appliquer le profil
sudo apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx
Vérifiez qu'il est chargé :
sudo aa-status | grep docker-nginx
Lancez un conteneur avec le profil personnalisé :
CLI :
docker run -d \
--security-opt apparmor=docker-nginx \
--name apparmor-test \
nginx:alpine
Compose :
services:
web:
image: nginx:alpine
security_opt:
- apparmor=docker-nginx
Vérifier l'application d'AppArmor
docker exec apparmor-test cat /etc/shadow
Cela devrait retourner « Permission denied » parce que le profil interdit explicitement la lecture de /etc/shadow.
Vérifiez le statut AppArmor du conteneur :
docker inspect --format '{{.AppArmorProfile}}' apparmor-test
Sortie attendue : docker-nginx.
Nettoyage :
docker rm -f apparmor-test
Comment exécuter un conteneur Docker avec un système de fichiers en lecture seule ?
Un système de fichiers root en lecture seule empêche les attaquants d'écrire des malwares, des backdoors ou des binaires modifiés dans un conteneur compromis. Le conteneur peut toujours écrire dans des volumes tmpfs explicitement montés pour les fichiers temporaires et les données d'exécution.
CLI :
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /run:rw,noexec,nosuid,size=16m \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=128m \
-p 8080:80 \
--name readonly-nginx \
nginx:alpine
Compose :
services:
web:
image: nginx:alpine
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
Notez le flag noexec sur les montages tmpfs. Il empêche l'exécution de binaires depuis les répertoires temporaires, une technique courante des attaquants après avoir obtenu un accès en écriture.
Vérifier que le système de fichiers est en lecture seule
docker exec readonly-nginx touch /testfile
Sortie attendue :
touch: /testfile: Read-only file system
Confirmez que les montages tmpfs fonctionnent :
docker exec readonly-nginx touch /tmp/testfile && echo "tmpfs works"
Vérifiez que le conteneur sert du trafic :
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080
Attendu : 200.
Nettoyage :
docker rm -f readonly-nginx
À quoi devrait ressembler un daemon.json Docker durci ?
Un daemon.json durci applique des paramètres de sécurité par défaut à chaque conteneur de l'hôte. Les conteneurs individuels peuvent toujours surcharger certains paramètres, mais la configuration du daemon définit la base.
Créez ou éditez /etc/docker/daemon.json :
{
"no-new-privileges": true,
"icc": false,
"live-restore": true,
"userland-proxy": false,
"log-driver": "journald",
"log-opts": {
"tag": "{{.Name}}"
},
"default-ulimits": {
"nproc": {
"Name": "nproc",
"Hard": 512,
"Soft": 256
},
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 32768
}
},
"storage-driver": "overlay2"
}
Ce que fait chaque paramètre :
no-new-privileges: Bloque l'escalade setuid/setgid dans tous les conteneurs par défaut.icc: false: Désactive la communication inter-conteneurs sur le réseau bridge par défaut. Les conteneurs ne peuvent se joindre qu'à travers des ports explicitement publiés ou des réseaux définis par l'utilisateur. Cela limite le mouvement latéral si un conteneur est compromis.live-restore: Maintient les conteneurs en exécution pendant les redémarrages du daemon. Évite les interruptions pendant les mises à jour Docker.userland-proxy: false: Utilise iptables pour le port mapping au lieu d'un processus proxy userland. Meilleures performances, moins de descripteurs de fichiers ouverts.log-driver: journald: Envoie les logs des conteneurs vers le journal système où ils sont gérés et rotés de manière centralisée.default-ulimits: Limite les processus et fichiers ouverts par conteneur. Prévient les fork bombs et l'épuisement des descripteurs de fichiers.
Appliquez les changements :
sudo systemctl restart docker
Vérifiez que le daemon a pris en compte la configuration :
docker info --format '{{.SecurityOptions}}'
Vous devriez voir no-new-privileges dans la liste des options de sécurité.
Vérifiez que l'ICC est désactivé :
docker network inspect bridge --format '{{index .Options "com.docker.network.bridge.enable_icc"}}'
Sortie attendue : false.
Note sur userns-remap : Si vous avez choisi le user namespace remapping au lieu du mode rootless, ajoutez "userns-remap": "default" à cette configuration. Ne combinez pas userns-remap avec Docker rootless.
Masquage de version : Pendant que vous éditez daemon.json, envisagez aussi de masquer les informations de version de l'API Docker. Docker n'expose pas d'en-têtes de version par défaut comme Nginx, mais si vous exposez l'API Docker sur un socket TCP (ne le faites pas, sauf nécessité absolue), protégez-le avec des certificats TLS client. Une API Docker exposée équivaut à un accès shell root.
Audit de votre configuration : Lancez Docker Bench for Security pour auditer votre configuration selon le CIS Docker Benchmark. Clonez le dépôt et exécutez le script directement, car l'image conteneur embarque un client Docker obsolète incompatible avec Docker Engine 29.x :
git clone https://github.com/docker/docker-bench-security.git /tmp/docker-bench
cd /tmp/docker-bench && sudo bash docker-bench-security.sh
Examinez la sortie pour les entrées WARN. Le daemon.json durci ci-dessus traite la plupart d'entre elles. Les éléments marqués INFO sont des recommandations, pas des échecs.
Combiner plusieurs couches de durcissement dans Docker Compose
Un fichier Compose de production devrait empiler plusieurs mesures de durcissement ensemble. Voici un exemple pour un conteneur Nginx avec les sept couches appliquées :
services:
web:
image: nginx:alpine
read_only: true
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
security_opt:
- no-new-privileges:true
- seccomp=/etc/docker/seccomp-nginx.json
- apparmor=docker-nginx
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
ports:
- "80:80"
pids_limit: 100
deploy:
resources:
limits:
memory: 256M
cpus: '1.0'
Notez que pids_limit prévient les fork bombs et deploy.resources.limits plafonne la mémoire et le CPU. Ce ne sont pas des flags security-opt mais ils empêchent le déni de service depuis l'intérieur du conteneur. Pour en savoir plus sur les limites de ressources, voir .
Vérifiez que toutes les options de sécurité sont actives sur le conteneur en cours d'exécution :
docker compose up -d
docker inspect web --format '{{json .HostConfig.SecurityOpt}}' | python3 -m json.tool
Vous devriez voir no-new-privileges, le chemin de votre profil seccomp et le nom du profil AppArmor listés.
Checklist de durcissement pour la production
| Mesure | Flag CLI | Clé Compose | daemon.json | Menace prévenue |
|---|---|---|---|---|
| Docker rootless | N/A (niveau daemon) | N/A | N/A (daemon séparé) | L'évasion de conteneur donne root sur l'hôte |
| userns-remap | N/A | N/A | "userns-remap": "default" |
Root conteneur = root hôte |
| Supprimer les capabilities | --cap-drop ALL --cap-add X |
cap_drop: [ALL] |
N/A | Surface d'attaque noyau |
| no-new-privileges | --security-opt no-new-privileges:true |
security_opt: [no-new-privileges:true] |
"no-new-privileges": true |
Escalade setuid/setgid |
| Seccomp personnalisé | --security-opt seccomp=profile.json |
security_opt: [seccomp:path] |
N/A | Exploit noyau via appels système bloqués |
| Profil AppArmor | --security-opt apparmor=name |
security_opt: [apparmor:name] |
N/A | Accès fichiers/réseau au-delà des besoins de l'app |
| Rootfs lecture seule | --read-only |
read_only: true |
N/A | Malware persistant, altération de binaires |
| Désactiver ICC | N/A | N/A | "icc": false |
Mouvement latéral entre conteneurs |
| Limiter les processus | --pids-limit 100 |
pids_limit: 100 |
"default-pids-limit": 100 |
Fork bombs |
| Désactiver le proxy userland | N/A | N/A | "userland-proxy": false |
Gaspillage de ressources, réduit la surface d'attaque |
Dépannage
Le conteneur ne démarre pas après l'ajout du profil seccomp :
Votre profil omet un appel système dont l'application a besoin. Lancez temporairement avec --security-opt seccomp=unconfined, tracez le processus avec strace, et ajoutez les appels système manquants à votre profil.
Vérifiez les logs d'audit du noyau pour les appels système bloqués :
sudo journalctl -k | grep -i seccomp
AppArmor bloque des opérations légitimes : Passez le profil en mode « complain » pour journaliser les refus sans les appliquer :
sudo aa-complain /etc/apparmor.d/containers/docker-nginx
Observez les logs :
sudo journalctl -k | grep apparmor | tail -20
Après avoir ajouté les permissions nécessaires, repassez en mode « enforce » :
sudo aa-enforce /etc/apparmor.d/containers/docker-nginx
Docker rootless ne peut pas tirer les images :
Vérifiez que DOCKER_HOST est correctement défini :
echo $DOCKER_HOST
Il devrait pointer vers /run/user/<UID>/docker.sock. Vérifiez aussi que le daemon rootless tourne :
systemctl --user status docker
Permission refusée sur un bind mount avec userns-remap : Les fichiers sur l'hôte appartenant à root (UID 0) sont inaccessibles parce que l'UID 0 du conteneur est mappé vers un UID élevé sur l'hôte. Corrigez en changeant la propriété :
# Find the remapped UID
grep dockremap /etc/subuid
# Then chown to that UID
sudo chown -R <remapped-uid>:<remapped-uid> /path/to/bind/mount
Remplacez <remapped-uid> par le premier nombre de /etc/subuid pour l'utilisateur dockremap (généralement 100000).
Logs des conteneurs : Pour tout problème lié à Docker, commencez par les logs du conteneur et le journal du daemon Docker :
docker logs <container-name>
sudo journalctl -u docker -f
Pour Docker rootless, vérifiez le journal au niveau utilisateur :
journalctl --user -u docker -f
Copyright 2026 Virtua.Cloud. Tous droits réservés. Ce contenu est une création originale de l'équipe Virtua.Cloud. Toute reproduction, republication ou redistribution sans autorisation écrite est interdite.
Prêt à essayer ?
Déployez votre serveur en quelques secondes. Linux, Windows ou FreeBSD.
Voir les offres VPS