Durcissement de la sécurité Docker : mode rootless, Seccomp et AppArmor sur un VPS

17 min de lecture·Matthieu·vpscontainersapparmorseccomprootless-dockersecuritydocker|

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 :

  1. Copiez le profil seccomp par défaut de Docker comme point de départ.
  2. Lancez votre conteneur avec ce profil copié. Il se comporte de façon identique au profil par défaut.
  3. Retirez les appels système par groupes (par ex. tous les key*, tous les swap*).
  4. Testez le conteneur après chaque suppression. Si ça casse, rajoutez le dernier appel système retiré.
  5. 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