Les conteneurs ne créent aucun nouveau mécanisme d’isolation : ils exploitent, de manière coordonnée, des namespaces Linux qui existent depuis longtemps dans le noyau. Ce TP construit cette conviction pas à pas, en assemblant à la main — via la commande unshare — exactement ce qu’un runtime de conteneur assemble automatiquement.
Ce que révèle lsns
Lister les namespaces actifs sur une session utilisateur ordinaire (lsns --output=NS,TYPE,NPROCS,PID,USER,COMMAND) révèle huit types distincts, conformes à man 7 namespaces : mnt, pid, net, ipc, uts, user, cgroup, time. Démarrer un conteneur Docker n’en fait apparaître aucun de nouveau — il crée simplement de nouvelles instances de ces mêmes types, dédiées au processus init du conteneur, avec NPROCS = 1 signalant leur caractère privé. Le conteneur coexiste avec les namespaces globaux de l’hôte plutôt que de les remplacer.
Un navigateur moderne comme Firefox illustre le même principe à une échelle différente : chaque processus -contentproc dispose fréquemment de son propre user namespace, de son propre IPC namespace, parfois de son propre net namespace. Ce n’est pas de la conteneurisation au sens strict, mais une isolation fine par processus, visant le même objectif — réduire la surface d’attaque — avec un outillage identique.
Construire l’isolation avec unshare
Le user namespace, et le piège du mapping
Créer un user namespace sans mapping explicite (unshare -U) place le processus dans un état fortement restreint : whoami y répond nobody, et /proc/$$/uid_map reste vide — aucun privilège effectif n’est accessible.
NOTE
Certains shells (zsh notamment) peuvent afficher des messages d’erreur lors de ce changement de contexte UID/GID atypique — un comportement lié à une gestion interne du mode privilégié du shell, sans lien avec une faille active du mécanisme de namespace lui-même (cf. CVE-2019-20044, qui concernait précisément un défaut de signalement de cette nature, corrigé depuis).
Ajouter -r (unshare -Ur) change radicalement la donne : le processus obtient l’UID 0 à l’intérieur du namespace — whoami répond désormais root. Le fichier /proc/$$/uid_map révèle le mécanisme sous-jacent : une ligne 0 1000 1 signifie que l’UID 0 du namespace correspond, côté hôte, à l’UID réel de l’utilisateur (1000). C’est le principe même du user namespace : offrir des privilèges complets à l’intérieur d’un périmètre isolé, sans jamais accorder de privilège root réel sur le système hôte.
Le mount namespace : changer de racine sans chroot
En combinant --mount avec -R "$PWD" sur un mini-système de fichiers Alpine préalablement extrait, unshare redéfinit la racine visible du système de fichiers pour le processus lancé. pwd y répond /, mais cette racine ne pointe plus vers celle de l’hôte — elle pointe vers le contenu Alpine extrait. L’identité root acquise dans le user namespace persiste sans interférence : les deux namespaces se combinent proprement.
Le PID namespace : pourquoi --fork et --mount-proc sont indispensables
Lancer un PID namespace sans --fork produit un environnement instable : le shell interactif tente de forker pour ses opérations normales (complétion, scripts de démarrage), et ces appels échouent avec une erreur de type manque de mémoire — en réalité un symptôme du refus de création de processus dans un contexte de PID namespace mal initialisé, pas un problème matériel réel.
Avec --fork, unshare crée un processus enfant qui devient le PID 1 du namespace — exactement le rôle qu’occupe init sur un système complet. Mais un problème subsiste : sans --mount-proc, la commande ps continue d’afficher les processus de l’hôte, car elle lit /proc, et /proc n’a pas été remonté pour refléter le nouveau namespace — la vue des PID reste incohérente.
--mount-proc referme cette faille en remontant un /proc propre au namespace nouvellement créé. Le résultat, une fois combiné à --fork, est net : le shell devient PID 1, /proc/self/status confirme un NSpid de 2 pour son enfant, et ps ne montre plus que les processus internes au namespace — l’isolation des PID devient réellement effective, et non plus seulement nominale.
Combiner tous les namespaces
Assembler --mount -R "$PWD", --pid --fork --mount-proc et --uts en une seule commande reconstitue, à la main, l’essentiel de ce qu’un runtime de conteneur automatise. Un piège pratique mérite d’être noté : après isolement du mount namespace vers un système de fichiers minimal, les commandes usuelles peuvent sembler absentes (ls: not found) — elles existent bien, généralement via BusyBox, mais la variable PATH n’est simplement pas initialisée dans cet environnement dépouillé. Il faut l’exporter explicitement (export PATH=/bin:/sbin:/usr/bin:/usr/sbin) avant de pouvoir travailler normalement.
Le namespace réseau
Un net namespace démarre par défaut avec son interface loopback (lo) à l’état DOWN — il faut l’activer explicitement (ip link set lo up) avant toute connectivité, y compris entre deux namespaces reliés par une interface virtuelle (veth). Une fois un serveur HTTP lié explicitement à l’adresse du namespace serveur, une requête émise depuis le namespace client aboutit normalement — démontrant une isolation réseau complète entre deux processus pourtant exécutés sur la même machine physique, sans qu’aucune virtualisation matérielle n’entre en jeu.
Ce que ce TP enseigne, au-delà de la commande unshare
La leçon centrale dépasse la seule syntaxe de unshare : un conteneur n’est jamais qu’une combinaison négociée de plusieurs namespaces, chacun isolant une dimension précise du système (identité, système de fichiers, processus, réseau, nom d’hôte). Comprendre cette composition, plutôt que de traiter Docker ou Podman comme une boîte noire, permet de raisonner correctement sur ce qu’un conteneur isole réellement — et, symétriquement, sur ce qu’il ne cloisonne pas par défaut (le noyau reste partagé avec l’hôte, contrairement à une machine virtuelle complète — voir la note sur les hyperviseurs pour la distinction).