Chapitre 14.3 — Isolation & moindre privilège
⏱️ TL;DR — Une compromission est inévitable un jour ; ce qui compte, c’est jusqu’où elle va. Le moindre privilège et l’isolation au niveau système bornent les dégâts. Concrètement : faire tourner l’appli sous un compte système dédié non-root (jamais root), avec juste les droits nécessaires ; fermer tous les ports non indispensables (pare-feu deny-by-default, base non exposée) ; segmenter le réseau (l’appli ne joint que ce dont elle a besoin — pas les métadonnées cloud, cf. SSRF Partie 8) ; isoler avec des conteneurs (utilisateur non-root dans le conteneur, image minimale, pas de capabilities superflues, filesystem en lecture seule si possible). L’objectif : qu’une faille applicative reste confinée, sans devenir une prise de contrôle totale.
🎯 Objectifs
- Appliquer le moindre privilège au niveau système (comptes non-root, droits minimaux).
- Réduire la surface réseau : fermer les ports, ne pas exposer la base, segmenter.
- Utiliser les conteneurs pour isoler, sans reproduire les mauvaises pratiques (root, image obèse).
- Comprendre comment ces mesures bornent l’impact d’une compromission.
Le principe : borner l’impact
On a construit beaucoup de défenses (Parties 4-13). Mais aucune n’est parfaite : un jour, une faille passe (une dépendance vérolée, un unserialize oublié, un 0-day). La question défensive devient alors : « quand l’attaquant obtient l’exécution sur mon serveur, que peut-il atteindre ? ». Le moindre privilège et l’isolation répondent à ça — non pas en empêchant la compromission, mais en la confinant. C’est la Partie 1 (fail secure, moindre privilège, defense in depth) appliquée au système.
Ne pas tourner en root
L’erreur fondatrice : exécuter l’appli (ou le conteneur) en root. Si le processus est compromis, l’attaquant hérite des droits root → contrôle total de la machine. Le principe (Partie 1, Serveur Linux) :
- Un compte système dédié, non-root, par service (ex.
www-data, un utilisateurapp), avec juste les permissions nécessaires (lire son code, écrire dans ses dossiers de données, rien d’autre). - Pas d’accès superflu : le compte de l’appli n’a pas besoin de lire
/etc/shadow, d’écrire hors de ses dossiers, ni de droitssudo. - Permissions de fichiers strictes (Partie 9, Serveur Linux) :
config.php/.envlisibles seulement par ce compte.
Ainsi, un processus compromis ne dispose que des maigres droits de son compte — il ne peut pas prendre la machine.
Réduire la surface réseau
Chaque port ouvert, chaque service joignable est une porte. On ferme tout ce qui n’est pas indispensable (rappel Serveur Linux) :
- Pare-feu deny-by-default (UFW) : n’ouvrir que 80/443 (web) et 22 (SSH, idéalement restreint). Tout le reste fermé.
- Base de données non exposée : Postgres/MySQL écoute en local (ou sur un réseau privé), jamais sur
0.0.0.0accessible d’Internet (rappel Partie 6 : moindre privilège DB et non-exposition). Le port 5432/3306 ne doit pas être ouvert au monde. - Segmenter le réseau : l’appli ne peut joindre que ce dont elle a besoin (sa base, ses services). En particulier, l’empêcher de joindre les métadonnées cloud (
169.254.169.254) et les services internes sensibles borne le SSRF (Partie 8) : même un SSRF réussi ne trouve rien à atteindre.
Internet --443--> [Reverse proxy] --local--> [App (compte non-root)] --reseau prive--> [DB non exposee]
(ne peut PAS joindre 169.254.169.254 ni les autres services)Isoler avec des conteneurs (correctement)
Les conteneurs (Docker — Serveur Linux) apportent une isolation : chaque service tourne dans son propre environnement, avec ses dépendances, séparé des autres. Bien utilisés, ils réduisent la surface et confinent une compromission. Mal utilisés, ils reproduisent (voire aggravent) les mauvaises pratiques.
Les bonnes pratiques de sécurité des conteneurs :
- Utilisateur non-root dans le conteneur : par défaut, beaucoup d’images tournent en root (à l’intérieur) — définir un
USERnon-root dans le Dockerfile. Un conteneur root compromis + une évasion = root sur l’hôte. - Image minimale : partir d’une base légère (distroless, alpine, slim) → moins de binaires = moins de surface (pas de shell, pas d’outils qu’un attaquant réutiliserait). Réduit aussi les CVE (Partie 12).
- Pas de capabilities/priviléges superflus : ne pas lancer en
--privileged, retirer les capabilities inutiles, ne pas monter le socket Docker dans un conteneur applicatif (évasion facile). - Filesystem en lecture seule quand possible (
read-only), volumes explicites pour les données → un attaquant ne peut pas écrire partout. - Secrets injectés proprement (pas dans l’image, pas en
ENVen clair dans le Dockerfile — Partie 13) : via des mécanismes de secrets du runtime. - Images à jour et scannées (chapitre 14.4).
⚠️ Piège — Un conteneur n’est pas une isolation magique. Les erreurs courantes l’annulent : tourner en root dans le conteneur (une évasion → root hôte), lancer en
--privileged, monter le socket Docker (/var/run/docker.sock) dans un conteneur applicatif (= contrôle du démon Docker = de l’hôte), utiliser une image obèse pleine de CVE et d’outils. « C’est dans un conteneur » ne veut pas dire « c’est isolé » si le conteneur est root et sur-privilégié. Applique le moindre privilège dans le conteneur comme sur l’hôte.
💡 Réflexe — Pour chaque service, pose-toi : « si ce processus était compromis maintenant, que pourrait faire l’attaquant ? ». La réponse doit être la plus petite possible : compte non-root aux droits minimaux, ne peut joindre que sa base (pas les métadonnées, pas les autres services), filesystem limité, pas de secrets superflus à portée. Chaque « oui, il pourrait aussi… » est un privilège à retirer. L’isolation ne prévient pas la faille — elle décide de l’ampleur du désastre.
🎯 Côté attaquant — Une fois l’exécution obtenue (via une faille applicative, une dépendance vérolée), l’attaquant fait du post-exploitation : il cherche à élever ses privilèges (root ?), à se déplacer latéralement (quels autres services/hôtes joint-il ?), à atteindre les métadonnées cloud (credentials → tout le cloud) et la base. Un environnement bien isolé et à moindre privilège le bloque à chaque étape : compte non-root sans échappatoire, réseau segmenté qui ne mène nulle part, secrets hors de portée. Un serveur où l’appli tourne en root, la base est joignable et les métadonnées accessibles lui offre au contraire une prise de contrôle totale à partir d’une seule faille.
🧭 Sur FormaCampus — FormaCampus applique le moindre privilège partout : chaque service (front Next.js, API Symfony, Moodle) tourne sous un compte dédié non-root, dans des conteneurs avec
USERnon-root, image minimale (slim/distroless), sans--privilegedni socket Docker monté, filesystem largement en lecture seule. Le pare-feu est deny-by-default (80/443 + SSH restreint), la base Postgres n’est pas exposée (réseau privé uniquement), et le réseau est segmenté pour que les services applicatifs ne puissent pas joindre les métadonnées cloud (borne le SSRF, Partie 8) ni les services non nécessaires. Résultat : une faille dans un service reste confinée à ce service et à ses maigres droits — pas une prise de contrôle de l’infra.
✏️ Exercices
Exercice 1 — Borne l’impact. Une appli tourne en root, la base MySQL écoute sur 0.0.0.0, et le serveur peut joindre 169.254.169.254. Une faille (SSRF + RCE) est exploitée. Décris ce que l’attaquant peut atteindre, puis les corrections.
✅ Solution
Impact maximal avec cette config : (1) l’appli en root → l’attaquant obtient root sur la machine (contrôle total) ; (2) le SSRF peut atteindre 169.254.169.254 → credentials cloud exfiltrés → compromission de toute l’infrastructure cloud ; (3) MySQL sur 0.0.0.0 → la base est joignable (et si un identifiant est trouvé, dumpable) ; l’attaquant peut se déplacer latéralement. Une seule faille devient une catastrophe totale. Corrections (moindre privilège + isolation) : faire tourner l’appli sous un compte non-root dédié (une RCE n’obtient que ses maigres droits) ; fermer l’exposition de MySQL (écoute locale/réseau privé, pare-feu) ; segmenter le réseau pour que le serveur ne puisse pas joindre les métadonnées cloud ni les services non nécessaires (le SSRF ne trouve alors rien) ; conteneurs non-root, image minimale, secrets hors de portée. La faille resterait confinée au lieu de tout emporter.
Exercice 2 — Conteneur mal isolé. Cite trois erreurs qui font qu’un conteneur n’isole quasiment rien, et leur correctif.
✅ Solution
(1) Tourner en root dans le conteneur → une évasion donne root sur l’hôte ; correctif : USER non-root dans le Dockerfile. (2) Lancer en --privileged (ou monter /var/run/docker.sock) → le conteneur contrôle le démon Docker, donc l’hôte ; correctif : ne jamais utiliser --privileged, ne pas monter le socket Docker dans un conteneur applicatif, retirer les capabilities inutiles. (3) Image obèse (base complète avec shell, outils, nombreuses CVE) → grande surface + outils réutilisables par l’attaquant ; correctif : image minimale (distroless/alpine/slim), scannée et à jour (chapitre 14.4). Bonus : filesystem en lecture seule, secrets injectés proprement (pas dans l’image). Un conteneur root, privilégié et obèse « isole » très peu.
🧠 Quiz de révision
1. Quel est l’objectif du moindre privilège / de l’isolation au niveau système ?
Borner l’impact d’une compromission (inévitable un jour) : non pas empêcher la faille, mais la confiner pour qu’un processus compromis ne puisse atteindre que le minimum. C’est fail secure + moindre privilège + defense in depth appliqués au système.
2. Pourquoi ne jamais faire tourner l’appli en root ?
Parce que si le processus est compromis, l’attaquant hérite des droits root → contrôle total de la machine. Sous un compte dédié non-root aux droits minimaux, une compromission ne donne que ces maigres droits — elle reste confinée.
3. Comment la segmentation réseau borne-t-elle le SSRF ?
En empêchant l’appli de joindre les cibles sensibles (métadonnées cloud 169.254.169.254, services internes) : même un SSRF réussi (Partie 8) ne trouve rien à atteindre. On ouvre au serveur uniquement ce dont il a besoin (sa base), rien de plus.
4. Cite trois bonnes pratiques de sécurité des conteneurs.
Utilisateur non-root dans le conteneur (USER), image minimale (distroless/slim, moins de surface/CVE), pas de privilèges superflus (pas de --privileged, pas de socket Docker monté, capabilities réduites). Aussi : filesystem en lecture seule, secrets injectés proprement, images à jour et scannées.
5. « C’est dans un conteneur, donc c’est isolé » — vrai ?
Non, pas automatiquement : un conteneur root, --privileged, avec le socket Docker monté ou une image obèse n’isole quasiment rien (une évasion = l’hôte). L’isolation exige d’appliquer le moindre privilège dans le conteneur (non-root, minimal, sans privilèges) comme sur l’hôte.
Chapitre suivant : CI/CD sécurisé & scan d’images — le pipeline qui a accès à tout : une cible de choix à protéger.