Isolation de Conteneurs et Exécution Non-Root dans Docker
Sécuriser les Dockerfiles multi-stage, supprimer les capabilities Linux et éviter les évasions de conteneur.
1. Ne jamais exécuter un conteneur en mode root
La plupart des images de base (node, python, debian) s’exécutent par défaut en UID 0 (root) sauf mention explicite du contraire, et la plupart des Dockerfiles ne disent jamais le contraire - donc la majorité des conteneurs en circulation font tourner leur processus applicatif en root à l’intérieur du conteneur. Les conteneurs ne sont pas des VM : ils partagent le noyau de l’hôte, et root dans un conteneur correspond, en configuration par défaut, à un UID disposant de privilèges réels sur ce noyau partagé. L’isolation conteneur est un ensemble de namespaces et de cgroups, pas une frontière de sécurité dure équivalente à un hyperviseur.
La conséquence pratique est qu’un conteneur tournant en root relève considérablement le plafond de ce qu’une compromission au niveau applicatif réussie (bug de désérialisation, dépendance vulnérable, écriture de fichier arbitraire) peut devenir. En root dans le conteneur, un attaquant obtenant une exécution de code peut écrire dans n’importe quel fichier visible par le conteneur, installer des paquets, modifier des binaires, et surtout dispose d’un chemin bien plus court pour exploiter la moindre vulnérabilité du noyau ou du runtime conteneur lui permettant de s’échapper complètement du namespace du conteneur et de toucher l’hôte.
La correction ne coûte presque rien au moment du build : créer un utilisateur dédié et non privilégié dans le Dockerfile et y basculer avant que l’application ne démarre. Le seul vrai coût est d’avoir parfois à corriger les permissions des répertoires dans lesquels l’app écrit (logs, uploads, fichiers temporaires), qui doivent appartenir au nouvel utilisateur non-root ou lui être accessibles en écriture - un chown en une ligne qu’on ajoute une fois et qu’on oublie ensuite.
2. Builds multi-stage pour réduire la surface d’attaque
La surface d’attaque d’un conteneur n’est pas seulement son processus en cours d’exécution - c’est chaque binaire, chaque bibliothèque, chaque outil présent dans l’image finale, parce que n’importe lequel peut être détourné par un attaquant qui obtient un shell (ou une injection de commande) dans le conteneur. Une étape de build a typiquement besoin d’une chaîne de compilation, d’en-têtes de développement et de l’arbre complet de dépendances npm/pip, devDependencies incluses ; rien de tout cela n’a sa place dans l’image qui tourne réellement en production.
Les builds multi-stage résolvent ce problème de manière structurelle : une étape fait la compilation/le build avec toute la chaîne d’outils lourde nécessaire, et une étape finale distincte part d’une image de base minimale et ne copie que la sortie compilée et les dépendances d’exécution. La chaîne de build, les source maps, les fichiers de tests et les dépendances de dev n’existent tout simplement pas dans l’image finale - pas masqués, pas cachés, réellement absents, car ils appartiennent à une couche de système de fichiers dont l’étape finale n’hérite jamais.

