Voici un Dockerfile qu'on retrouve dans la majorité des dépôts publics :
FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]
Six lignes, ça compile, ça tourne, le CI passe.
Maintenant, supposons qu'une vulnérabilité dans une dépendance npm donne à un attaquant un RCE dans ce conteneur. Voici ce qu'il a effectivement entre les mains :
- Un shell qui tourne en root (UID 0)
- L'ensemble des Linux capabilities que Docker conserve par défaut, dont CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_NET_RAW et CAP_KILL
- Un système de fichiers entièrement inscriptible
- L'intégralité des ressources CPU et mémoire de l'hôte (aucune limite n'est posée par défaut)
- Une image dont le contenu peut changer entre deux pulls (
:latest) - Une surface d'attaque qui inclut un OS Debian complet, package manager compris
Aucun de ces points n'est un bug. Ce sont les valeurs par défaut.
Docker est conçu pour être ergonomique, pas pour être sûr, et ces deux objectifs ne sont pas alignés.
C'est l'idée centrale de cet article : la sécurité conteneur n'est pas une couche qu'on ajoute, c'est un ensemble de défauts qu'on retire. Le travail consiste à partir d'une configuration permissive et à la fermer méthodiquement, en sachant à chaque fois ce qu'on protège et contre quoi.
On va parcourir le sujet à trois niveaux :
- Le runtime (la commande
docker runou son équivalent compose) - L'image (le Dockerfile lui-même)
- Le démon (la configuration du daemon Docker côté hôte)
À la fin, tu auras une config de référence applicable telle quelle, et surtout les justifications pour chaque ligne. C'est ce qui permet de l'adapter intelligemment à un workload qui sort du cas standard.
Le modèle mental : isolation vs hardening
Avant les commandes, deux distinctions qui changent tout.
VM vs conteneur
Une machine virtuelle s'appuie sur un hyperviseur qui émule du matériel et fournit à chaque OS invité son propre noyau. La frontière entre invité et hôte est une frontière matérielle simulée, et la franchir demande de casser l'hyperviseur.
Un conteneur, lui, partage le noyau de l'hôte.
L'isolation se fait au niveau processus, via les namespaces et cgroups du kernel Linux. La frontière est logicielle, et elle s'effondre dès qu'on trouve une vulnérabilité dans le runtime (runC, containerd) ou dans le kernel lui-même.
CVE-2019-5736 sur runC en est l'exemple le plus connu : un conteneur non-privilégié pouvait écraser le binaire runC sur l'hôte et obtenir l'exécution de code en tant que root au prochain docker exec. CVE-2024-21626 ("Leaky Vessels", début 2024) a démontré que le sujet reste d'actualité.
La conséquence est importante. La surface d'attaque réelle d'un conteneur n'est pas le conteneur lui-même, c'est l'ensemble noyau hôte plus runtime. Tout ce qu'on appelle "container hardening" consiste à réduire cette surface, soit en limitant ce que le processus peut atteindre, soit en limitant ce qu'il peut faire avec ce qu'il atteint.
Isolation vs hardening
Ces deux mots sont souvent confondus alors qu'ils renvoient à deux mécanismes distincts.
L'isolation détermine ce que le conteneur voit. Elle est assurée par les namespaces Linux :
- PID (arbre des processus)
- NET (pile réseau)
- MNT (système de fichiers)
- IPC (System V / POSIX)
- UTS (hostname)
- USER (mapping UID/GID)
Casser un namespace, c'est élargir la vue du conteneur jusqu'à inclure des choses qui appartiennent à l'hôte ou à un autre conteneur.
Le hardening détermine ce que le conteneur peut faire, même s'il voit quelque chose. Il s'appuie sur les capabilities (sous-ensembles du privilège root), les profils seccomp (filtre de syscalls), les politiques MAC (AppArmor/SELinux) et les cgroups (limites de ressources).
Les deux sont nécessaires.
Une isolation parfaite avec aucun hardening permet à un conteneur compromis de saturer le CPU de l'hôte. Un hardening agressif avec une isolation cassée (par exemple --pid=host) laisse le conteneur lire les processus de l'hôte. La défense en profondeur consiste à empiler les deux, et à empiler plusieurs couches dans chaque catégorie.
Trois scénarios pour rendre ça concret
Avant de plonger dans la config, trois scénarios qui justifient chaque ligne qu'on va écrire.
Container escape via vulnérabilité runtime
CVE-2019-5736 (runC) en est le cas d'école. Un conteneur non-privilégié peut, dans certaines conditions, écraser le binaire /usr/bin/runc sur l'hôte au moment où un docker exec est lancé.
Au prochain démarrage de conteneur, c'est le code de l'attaquant qui s'exécute en tant que root sur la machine.
Mitigation : user namespaces (root du conteneur != root de l'hôte), patches kernel à jour, profils MAC actifs.
Abus de capabilities
Un conteneur démarré avec --privileged désactive tous les mécanismes de sécurité Docker en bloc : seccomp désactivé, AppArmor désactivé, toutes les capabilities ajoutées, accès aux périphériques de l'hôte.
À partir de là, un attaquant peut monter le filesystem de l'hôte (mount /dev/sda1 /mnt), écrire dans /etc/shadow, ou abuser des cgroups v1 release_agent pour faire exécuter un script en tant que root sur l'hôte.
Même sans --privileged, certaines capabilities suffisent. CAP_SYS_ADMIN à elle seule ouvre la voie à des dizaines d'évasions documentées.
Mitigation : --cap-drop=ALL par défaut, ajouts ciblés et justifiés.
Exposition du socket Docker
Le démon Docker écoute sur /var/run/docker.sock. Toute personne qui peut écrire dans ce socket peut piloter le démon, donc lancer n'importe quel conteneur, y compris un conteneur --privileged qui monte / de l'hôte.
Monter ce socket dans un conteneur (-v /var/run/docker.sock:/var/run/docker.sock) est une pratique courante chez les devs qui veulent faire du Docker-in-Docker pour leur CI ou leur Portainer. C'est l'équivalent d'un accès root effectif à l'hôte pour le processus du conteneur.
Mitigation : ne jamais monter le socket en production. Si vraiment nécessaire (CI builders), utiliser un proxy comme docker-socket-proxy qui filtre les API exposées.
Ces trois scénarios ne sont pas exhaustifs, mais ils couvrent l'essentiel : kernel/runtime, privilèges trop larges, exposition d'API. Chaque ligne de la config qui suit répond à au moins l'un des trois.
Anatomie d'une config durcie, ligne par ligne
Voici la commande de référence. On va la décortiquer en six blocs thématiques.
docker run -d \
--name myapp \
--user 1000:1000 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop=ALL \
--security-opt no-new-privileges \
--security-opt seccomp=/etc/docker/seccomp/default.json \
--security-opt apparmor=docker-default \
--pids-limit 100 \
--memory 512m \
--memory-swap 512m \
--cpus 1.0 \
--network myapp-net \
-p 127.0.0.1:8080:8080 \
-v myapp-data:/data \
-v /etc/myapp/config.yml:/app/config.yml:ro \
myapp@sha256:a1b2c3...
Tu remarqueras que --cap-add=NET_BIND_SERVICE, qu'on voit partout dans les configs partagées en ligne, n'est plus là. On y revient.
Bloc 1 : identité
--user 1000:1000
Par défaut, le processus à l'intérieur du conteneur tourne en root (UID 0). Si l'image n'a pas d'instruction USER et que les user namespaces ne sont pas actifs, ce root du conteneur est aussi root sur l'hôte.
Une évasion donne alors directement les droits root de la machine.
--user 1000:1000 force le processus à tourner avec un UID/GID arbitraire non-privilégié.
Cette protection est complétée au niveau du démon par les user namespaces (userns-remap dans /etc/docker/daemon.json), qui remappent l'intégralité des UIDs du conteneur vers une plage non privilégiée sur l'hôte. C'est la protection la plus efficace contre les évasions, et elle est désactivée par défaut. On verra la config dans la section dédiée au démon.
Attention : --user ne suffit pas si l'image elle-même a besoin d'écrire dans des chemins appartenant à root. C'est pour ça que l'instruction USER doit aussi figurer dans le Dockerfile, pour que les permissions de l'image soient cohérentes avec l'UID runtime.
Bloc 2 : capabilities
--cap-drop=ALL
Linux fragmente le privilège root en une quarantaine de capabilities (CAP_CHOWN, CAP_NET_RAW, CAP_SYS_ADMIN, etc.).
Docker conserve par défaut un sous-ensemble qui inclut encore CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_NET_RAW, CAP_KILL, CAP_SYS_CHROOT et quelques autres. Ce n'est pas anodin. CAP_NET_RAW permet par exemple de forger des paquets ARP et de lancer du spoofing sur le réseau du conteneur. CAP_DAC_OVERRIDE permet de contourner les permissions DAC sur les fichiers.
La règle est simple : --cap-drop=ALL au démarrage, puis --cap-add pour rajouter ce qui est strictement nécessaire et seulement ça.
Le piège classique : NET_BIND_SERVICE
On voit souvent --cap-add=NET_BIND_SERVICE dans les configs partagées en ligne. Cette capability autorise le processus à se binder sur un port < 1024.
Elle n'est utile que si ton processus écoute directement sur le port 80 ou 443 à l'intérieur du conteneur.
Or dans une architecture moderne, ton app écoute sur 8080 ou 3000. C'est Docker qui fait le mapping -p 80:8080 côté hôte. Le bind sur le port 80 est fait par le démon, pas par ton processus.
Si en plus tu as un reverse proxy (nginx, Traefik, Caddy) devant, ce dernier gère les ports privilégiés, et ton app n'a pas du tout besoin de cette capability.
Conclusion pragmatique : démarre avec --cap-drop=ALL sans rien ajouter. Si ton app crashe, le message d'erreur te dira ce qui manque, et tu ajouteras la capability ciblée. Ne pré-ajoute rien "au cas où".
Bloc 3 : syscalls
--security-opt seccomp=/etc/docker/seccomp/default.json
--security-opt no-new-privileges
Seccomp filtre les appels système que le processus peut faire au noyau.
Le profil par défaut de Docker bloque environ 44 syscalls jugés dangereux ou inutiles pour la majorité des workloads applicatifs : keyctl (manipulation du keyring kernel), unshare (création de nouveaux namespaces), mount, pivot_root, kexec_load, bpf, et d'autres. En pratique, ce profil est suffisant pour 95 % des applications.
Préciser explicitement --security-opt seccomp=... plutôt que de se reposer sur le défaut implicite a deux utilités. D'abord, ça rend la configuration auditable (un grep sur ton infra te dit immédiatement quels conteneurs ont seccomp activé). Ensuite, ça évite que quelqu'un retire silencieusement la protection.
Voir --security-opt seccomp=unconfined quelque part en production est un drapeau rouge immédiat. La seule raison légitime de désactiver seccomp est le debug ponctuel d'un syscall bloqué, et ça doit revenir à default aussitôt après.
--security-opt no-new-privileges applique le flag PR_SET_NO_NEW_PRIVS au processus. Cela empêche le processus et tous ses descendants d'élever leurs privilèges via un binaire SUID/SGID.
Sans cette option, un binaire SUID root présent dans l'image (ping, mount dans certains cas) peut être exploité pour regagner des privilèges malgré --user.
Le coût opérationnel est nul, le bénéfice est réel, donc on l'active toujours.
Bloc 4 : MAC (Mandatory Access Control)
--security-opt apparmor=docker-default
AppArmor (Ubuntu, Debian) et SELinux (RHEL, Fedora, CentOS) sont des modules kernel qui appliquent des politiques d'accès au niveau de l'hôte, en plus des permissions DAC standards.
Docker installe un profil par défaut (docker-default pour AppArmor, label container_t pour SELinux) qui restreint les conteneurs au-delà de ce que les capabilities et seccomp couvrent : interdiction d'écrire dans /proc/sysrq-trigger, dans /sys/firmware, dans certains chemins sensibles du sysfs.
Préciser apparmor=docker-default explicitement a la même logique que pour seccomp : auditabilité et défense contre une dérive de configuration. Sur un hôte SELinux, l'équivalent est --security-opt label=type:container_t (généralement appliqué automatiquement par le runtime).
Important : ces protections supposent que le module est activé sur l'hôte. Sur une distribution sans AppArmor ni SELinux, l'option est ignorée silencieusement. C'est pourquoi le hardening conteneur n'a de sens qu'avec un hôte lui-même durci.
Bloc 5 : système de fichiers
--read-only
--tmpfs /tmp:rw,noexec,nosuid,size=64m
-v myapp-data:/data
-v /etc/myapp/config.yml:/app/config.yml:ro
--read-only rend le rootfs du conteneur immuable. Un attaquant ne peut plus modifier les binaires du conteneur ni y déposer un implant persistant.
La majorité des applications web n'écrivent jamais sur leur rootfs, donc cette option passe sans douleur.
Pour le cas où l'app a besoin d'un répertoire temporaire, on remonte /tmp en tmpfs (donc en mémoire, pas sur disque), avec deux options de durcissement :
-
noexec: pas d'exécution de binaire depuis ce mount -
nosuid: les bits SUID/SGID y sont ignorés
La taille est bornée pour éviter qu'un attaquant ne sature la RAM via ce mount.
-v myapp-data:/data utilise un named volume géré par Docker. C'est nettement préférable à un bind mount d'un chemin de l'hôte. Les named volumes vivent dans /var/lib/docker/volumes/, sont gérés par le démon, et n'exposent pas de structure du filesystem hôte au conteneur.
Les bind mounts (-v /home/user/data:/data) sont l'un des principaux vecteurs d'évasion. Un bind mount sur /, sur /etc, ou sur /proc casse l'isolation du namespace MNT.
-v /etc/myapp/config.yml:/app/config.yml:ro est le seul bind mount toléré ici, et il est en lecture seule (:ro). C'est un pattern courant : la configuration est sur l'hôte (gérée par Ansible, Terraform, ou montée depuis un secret manager), le conteneur la lit, point.
Le :ro est non négociable.
Et le grand absent : jamais de -v /var/run/docker.sock:... en production. Comme vu dans la section menaces, ça équivaut à donner les clés du démon, donc un accès root effectif à l'hôte.
Bloc 6 : ressources
--pids-limit 100
--memory 512m
--memory-swap 512m
--cpus 1.0
Ces options reposent sur les cgroups Linux et limitent ce que le conteneur peut consommer. Par défaut, Docker n'impose aucune limite : un conteneur peut allouer toute la RAM de l'hôte, faire un fork bomb, saturer le CPU.
--pids-limit 100 borne le nombre de processus/threads que le conteneur peut créer. C'est la protection la plus simple contre les fork bombs, qu'elles soient malveillantes ou accidentelles (un bug dans ton code qui boucle sur fork()).
--memory 512m plafonne la RAM. Au-delà, le conteneur se fait OOM-killer.
--memory-swap 512m réglé à la même valeur que --memory désactive le swap pour ce conteneur. Cela évite des comportements de performance imprévisibles et coupe une voie d'épuisement du swap de l'hôte.
--cpus 1.0 fixe un quota CPU équivalent à un cœur. Sur une machine multi-cœurs, ça laisse les autres conteneurs et l'hôte respirer même si le conteneur boucle.
L'argument sécurité est moins évident que pour les capabilities, mais il est réel. Un conteneur compromis ne devient pas un vecteur de DoS sur les voisins ou sur l'hôte. C'est de la résilience, et la résilience fait partie de la sécurité.
Et le réseau
--network myapp-net
-p 127.0.0.1:8080:8080
--network myapp-net place le conteneur sur un bridge user-defined plutôt que sur le bridge par défaut (docker0).
Le bridge par défaut autorise tous les conteneurs à se parler. Un bridge dédié couplé à icc: false dans le démon (voir plus loin) coupe cette communication non sollicitée.
-p 127.0.0.1:8080:8080 est un détail qui change tout. Sans préfixe d'IP, -p 8080:8080 bind sur 0.0.0.0, donc sur toutes les interfaces de l'hôte, y compris celles exposées sur Internet.
Sur un VPS bien configuré côté firewall, ce n'est pas catastrophique. Sur un poste de dev ou un serveur sans firewall strict, ça expose ton conteneur au monde.
127.0.0.1:8080:8080 ne bind que sur loopback, ce qui est ce que tu veux dans 90 % des cas (typiquement quand un reverse proxy en frontal route vers le conteneur).
Côté image : le Dockerfile
Le runtime n'est que la moitié du sujet. L'autre moitié est ce qui est dans l'image elle-même, et ça se construit dans le Dockerfile.
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/myapp ./cmd/myapp
FROM gcr.io/distroless/static-debian12:nonroot
LABEL org.opencontainers.image.source="https://github.com/org/myapp"
COPY --from=builder /out/myapp /usr/local/bin/myapp
USER nonroot:nonroot
ENTRYPOINT ["/usr/local/bin/myapp"]
Quatre points méritent qu'on s'y arrête.
Multi-stage
L'étape builder contient toute la toolchain : compilateur, dépendances, code source. L'image finale n'embarque que le binaire compilé.
Conséquence directe : un attaquant qui obtient un RCE dans le conteneur n'a pas de gcc, de git, de curl, ni de npm à sa disposition pour construire son outillage post-exploitation.
La surface est fortement réduite.
Distroless
L'image finale dérive de gcr.io/distroless/static-debian12. Pas de shell, pas de package manager, pas d'utilitaires Unix. Pas de sh, pas de bash, pas de apt, pas de wget.
Un attaquant qui aurait quand même un RCE doit travailler uniquement avec les capacités de ton binaire et celles du kernel.
La variante :nonroot fournit en plus un utilisateur UID 65532, qu'on active avec USER nonroot:nonroot.
Pour un binaire statique compilé en Go, Rust ou C, c'est l'image la plus minimale possible. Pour Node.js ou Python, regarder du côté de gcr.io/distroless/nodejs ou gcr.io/distroless/python3.
Épinglage par digest, pas par tag
Dans l'exemple ci-dessus, gcr.io/distroless/static-debian12:nonroot est un tag mutable. Son contenu change quand Google republie l'image (ce qui est fréquent, pour de bonnes raisons de patches sécurité).
Pour une chaîne de build reproductible et auditable, on remplace le tag par un digest immuable :
FROM gcr.io/distroless/static-debian12@sha256:6b3b06...
Le digest est obtenu via docker pull <image>:tag puis docker inspect.
Au prix d'un peu de discipline (mettre à jour le digest à chaque audit), on garantit qu'aucune supply chain attack ne peut substituer une image silencieusement entre deux builds.
.dockerignore
Souvent oublié, parfois critique.
Sans .dockerignore, COPY . . copie tout le contexte de build dans l'image, y compris .git/, .env, node_modules/ (si tu builds en local avant), et tout fichier sensible que tu aurais laissé traîner.
Un .dockerignore minimal contient au moins :
.git
.env*
*.log
node_modules
.DS_Store
.idea
.vscode
Ce n'est pas du hardening au sens strict, c'est de l'hygiène. L'oubli est un classique des audits.
Le démon Docker : /etc/docker/daemon.json
Tout ce qu'on a vu jusqu'ici se configure par conteneur. Certains réglages se font une seule fois, au niveau du démon, et s'appliquent à tous les conteneurs.
C'est la couche que les développeurs ignorent parce qu'elle relève de l'admin de l'hôte. Et c'est précisément pour ça qu'elle mérite d'être traitée.
{
"userns-remap": "default",
"no-new-privileges": true,
"live-restore": true,
"icc": false,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
userns-remap: default
Active les user namespaces sur l'ensemble des conteneurs.
Root (UID 0) à l'intérieur d'un conteneur est mappé vers un UID non privilégié (par exemple 100000) sur l'hôte. Une évasion qui aboutit avec UID 0 dans le conteneur se retrouve avec UID 100000 sur l'hôte. Elle ne peut donc pas écrire dans /etc/shadow, ni monter de filesystem, ni faire quoi que ce soit qui demande root réel.
C'est la protection la plus efficace contre les container escapes.
Elle est désactivée par défaut parce qu'elle introduit quelques contraintes sur les volumes (les UIDs des fichiers doivent être adaptés). Le coût en vaut largement la peine.
no-new-privileges: true
Au niveau démon, applique le flag PR_SET_NO_NEW_PRIVS à tous les conteneurs sans qu'on ait besoin de le préciser à chaque docker run.
C'est de la défense en profondeur : si un dev oublie l'option dans une commande, le démon la rajoute.
icc: false
Désactive l'inter-container communication sur le bridge par défaut.
Sans cette option, deux conteneurs sur docker0 peuvent se parler librement. Avec icc: false, ils sont isolés sauf si on les place explicitement sur le même réseau user-defined.
C'est de la segmentation réseau gratuite.
live-restore: true
Permet aux conteneurs de continuer à tourner si le démon redémarre (pour une mise à jour, par exemple).
Ce n'est pas du hardening au sens strict, mais ça réduit la fenêtre où on serait tenté de désactiver des protections "le temps d'un fix urgent". Moins de pression opérationnelle = moins de raccourcis sécurité.
Rotation des logs
max-size et max-file préviennent qu'un conteneur bavard remplisse /var/lib/docker/containers/... et finisse par saturer le disque de l'hôte.
Saturer le disque hôte est un vecteur de DoS comme un autre.
La checklist, en synthèse
Voici la grille d'évaluation qui résume tout. Elle peut servir de checklist d'audit, ou de gabarit pour un linter d'images comme hadolint, trivy config ou checkov.
Drop privileges
- Exécution non-root (
--user,USERdans le Dockerfile) -
--cap-drop=ALLpuis ajouts ciblés - User namespaces actifs au niveau démon
Restrict syscalls
- Profil seccomp explicite (le default suffit dans la majorité des cas)
--no-new-privileges
Apply MAC
- AppArmor
docker-defaultou SELinuxcontainer_t - Vérifier que le module est actif sur l'hôte
Limit volume risk
- Named volumes plutôt que bind mounts
-
:roquand l'écriture n'est pas nécessaire - Jamais de
/var/run/docker.sock,/proc,/sysmontés dans un conteneur applicatif
Runtime options
-
--read-only+ tmpfs pour les chemins inscriptibles -
--memory,--cpus,--pids-limit - Bind sur
127.0.0.1plutôt que0.0.0.0quand un reverse proxy est devant
Build minimal images
- Multi-stage
- Distroless ou scratch pour les binaires statiques
- Épinglage par digest, pas par tag
-
.dockerignoreà jour
La formule à retenir : chaque case décochée doit être un choix conscient, pas un défaut hérité.
En entretien comme en code review, la mauvaise réponse est "je ne savais pas". La bonne réponse est "j'ai évalué que pour ce workload, le coût opérationnel dépasse le bénéfice, et voici la mitigation alternative en place".
Cas concret : Mini Secrets Manager
J'ai appliqué cette checklist à un projet personnel, un mini gestionnaire de secrets en NestJS/PostgreSQL. Le pipeline CI/CD intégrait Trivy pour le scan d'image, Snyk pour les dépendances, et Trufflehog pour la détection de secrets en commit.
Au premier passage, Trivy a remonté une CVE de niveau HIGH sur l'image de base que j'avais épinglée trois semaines plus tôt. Le correctif a consisté à bumper le digest, valider que les tests passent, republier.
Sans ce pipeline, la vulnérabilité serait restée en prod jusqu'au prochain audit manuel, c'est-à-dire potentiellement jamais.
La leçon n'est pas que ma config était mauvaise (elle suivait la checklist ci-dessus), c'est qu'une config durcie sans détection continue est une photographie qui périme. Les deux vont ensemble.
Ce que cet article ne couvre pas
Le sujet "sécuriser un conteneur" déborde largement de Docker en mode standalone. Quatre prolongements importants qu'il faut au moins savoir nommer.
Kubernetes
En production sérieuse, Docker seul est rare.
Kubernetes ajoute des primitives spécifiques qui transposent les concepts vus ici :
-
securityContext.capabilitiespour les capabilities -
securityContext.runAsNonRootpour l'identité -
seccompProfilepour les syscalls -
NetworkPoliciespour la segmentation réseau
Et surtout les Pod Security Standards (Restricted, Baseline, Privileged) qui codifient des baselines applicables au niveau namespace. La philosophie du moindre privilège transpose directement, les commandes changent.
Rootless Docker
Docker peut tourner entièrement sans privilèges (le démon lui-même tourne en utilisateur non-root).
Ça change le modèle de menace : l'évasion de conteneur ne donne plus root sur l'hôte par construction. Le coût est en compatibilité (certaines fonctionnalités réseau et de stockage sont limitées).
Podman, l'alternative de Red Hat, fonctionne en rootless par défaut.
Sandboxing renforcé
Pour les workloads multi-tenant ou les environnements à très haute sensibilité, le partage du noyau hôte reste un risque.
gVisor (Google) interpose un "noyau utilisateur" entre le conteneur et le kernel hôte, qui filtre les syscalls.
Kata Containers va plus loin et lance chaque conteneur dans une micro-VM légère (KVM).
Le coût en performance n'est plus négligeable, mais l'isolation se rapproche du niveau VM.
Supply chain
Le hardening d'un conteneur est inutile si l'image qu'on déploie a été substituée en amont.
Les outils à connaître :
-
cosignpour signer cryptographiquement les images - Les attestations SLSA pour prouver la provenance d'un build
- Les SBOM (Software Bill of Materials, format SPDX ou CycloneDX) pour documenter les composants
Sigstore et Notary v2 sont les écosystèmes de référence.
Chacun de ces sujets mérite son propre article. Mais le socle reste celui qu'on a parcouru ici : sans une config conteneur correctement durcie, ajouter Kubernetes, gVisor ou cosign ne sert pas à grand-chose. C'est l'ordre qui compte.
Le réflexe à garder : avant de copier une config trouvée en ligne (y compris celle-ci), ouvre docker inspect sur tes conteneurs en prod et compare. Tu seras surpris de ce qui tourne avec des défauts que personne n'a jamais remis en question.
C'est par là qu'on commence.
Top comments (0)