Environnement : Arch Linux.

Commandes de base

date
uname -a
lsns --version || lsns -V
unshare --version
ip -V
id -u
cat /proc/$$/status | grep NoNewPrivs

1. Rappels : lsns et comptage des namespaces

Objectif

Lister les namespaces en cours d’utilisation et compter les types distincts.

Commandes

lsns --output=NS,TYPE,NPROCS,PID,USER,COMMAND > lsns.before.txt

Observation

Sur ma session utilisateur, j’observe 8 types de namespaces :

  • mnt, pid, net, ipc, uts, user, cgroup, time.

TIP

Le nombre observé dépend du kernel et de la visibilité (root vs user). Un utilisateur non-root voit déjà une grande partie des namespaces.

Évolution des namespaces

On fige l’état des namespaces, puis on démarre une instance d’alpine au sein d’un conteneur Docker :

sudo lsns --output=NS,TYPE,NPROCS,PID,USER,COMMAND > lsns.no_container.txt
 
docker run --rm -it alpine sh

Dans une autre instance du shell, on capture l’état courant des namespaces :

sudo lsns --output=NS,TYPE,NPROCS,PID,USER,COMMAND > lsns.with_container.txt

Subséquemment, on constate les différences suivantes entre les résultats de lsns :

--- lsns.sudo.no_container.txt	2026-01-31 19:46:56.908995754 +0100
+++ lsns.sudo.with_container.txt	2026-01-31 19:47:21.587995660 +0100
@@ -1,16 +1,16 @@
         NS TYPE   NPROCS   PID USER    COMMAND
-4026531832 mnt       278     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
-4026531833 net       277     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
-4026531834 time      298     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
-4026531835 cgroup    292     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
-4026531836 pid       297     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
-4026531837 user      282     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
-4026531838 uts       284     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
-4026531839 ipc       279     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531832 mnt       281     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531833 net       281     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531834 time      303     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531835 cgroup    296     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531836 pid       301     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531837 user      287     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531838 uts       287     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
+4026531839 ipc       283     1 root    /usr/lib/systemd/systemd --switched-root --system --deserialize=50
 4026532209 mnt         4   478 root    ├─/usr/lib/systemd/systemd-userdbd
 4026532210 uts         4   478 root    ├─/usr/lib/systemd/systemd-userdbd
-4026532374 mnt         1   498 root    ├─/usr/lib/systemd/systemd-udevd
-4026532375 uts         1   498 root    ├─/usr/lib/systemd/systemd-udevd
+4026532374 mnt         2   498 root    ├─/usr/lib/systemd/systemd-udevd
+4026532375 uts         2   498 root    ├─/usr/lib/systemd/systemd-udevd
 4026532558 mnt         2   631 dbus    ├─/usr/bin/dbus-broker-launch --scope system --audit
 4026532559 uts         1   636 root    ├─/usr/lib/systemd/systemd-logind
 4026532561 mnt         1   633 root    ├─/usr/bin/NetworkManager --no-daemon
@@ -62,6 +62,7 @@
 4026533319 user        1  2605 ryan    /opt/zen/zen -contentproc -isForBrowser ...
 4026533320 ipc         1  2605 ryan    /opt/zen/zen -contentproc -isForBrowser ...
 4026533321 net         1  2605 ryan    /opt/zen/zen -contentproc -isForBrowser ...
+4026533407 mnt         1 94808 root    sh
 4026533411 mnt         2 94263 ryan    bwrap --unshare-all ...
 4026533434 user        1  2615 ryan    /opt/zen/zen -contentproc ...
 4026533435 net         1  2615 ryan    /opt/zen/zen -contentproc ...
@@ -86,3 +87,8 @@
 4026533808 cgroup      2 94263 ryan    bwrap --unshare-all ...
 4026533809 net         2 94263 ryan    bwrap --unshare-all ...
 4026533870 user        2 94263 ryan    bwrap --unshare-all ...
+4026533871 uts         1 94808 root    sh
+4026533872 ipc         1 94808 root    sh
+4026533873 pid         1 94808 root    sh
+4026533874 cgroup      1 94808 root    sh
+4026533875 net         1 94808 root    sh

La comparaison montre plusieurs points importants :

  • aucun nouveau type de namespace n’apparaît : les types restent identiques à ceux décrits dans man 7 namespaces (mnt, pid, net, ipc, uts, user, cgroup, time) ;
  • le nombre de processus (NPROCS) attachés aux namespaces globaux de l’hôte augmente, ce qui reflète la création de nouveaux processus par le conteneur ;
  • de nouveaux namespaces apparaissent, associés à un processus sh lancé dans le conteneur. Ces namespaces (mnt, pid, net, ipc, uts, cgroup) ont NPROCS = 1, indiquant qu’ils sont privés au conteneur.

On observe ainsi que le conteneur repose sur la création de nouveaux namespaces dédiés à son processus « init », tout en coexistant avec les namespaces globaux de l’hôte. Cela illustre que les conteneurs ne créent pas de nouveaux mécanismes d’isolation, mais exploitent de manière coordonnée les namespaces existants.

2. Interprétation d’une sortie lsns type Firefox contentproc

Analyse

  • Chaque processus firefox -contentproc possède souvent :

    • son propre user namespace,
    • son propre ipc namespace,
    • parfois son propre net namespace.
  • NPROCS=1 indique un namespace privé à un seul processus.

Conclusion

Firefox utilise massivement les namespaces pour sandboxer ses processus de contenu. Il ne s’agit pas de conteneurs, mais d’une isolation fine par processus visant à réduire la surface d’attaque.

3. Comparaison avec man 7 namespaces

Les namespaces existants sont : mnt, pid, net, ipc, uts, user, cgroup, time. Les 8 types observés via lsns correspondent exactement à ceux décrits dans la documentation officielle.

4. Découverte : user namespaces

Première approche

lab/linux_hardening/tp-namespaces unshare -U
(eval):unsetopt:1: PRIVILEGED: can't drop privileges; failed to change group ID: argument invalide
(eval):1: can't change option: privileged
lab/linux_hardening/tp-namespaces whoami
nobody
lab/linux_hardening/tp-namespaces cat /proc/$$/uid_map
lab/linux_hardening/tp-namespaces exit
lab/linux_hardening/tp-namespaces whoami
ryan

unshare -U crée un nouveau user namespace sans mapping UID/GID par défaut. Le processus se retrouve avec un identifiant interne non mappé, ce qui explique que whoami retourne nobody et que /proc/$$/uid_map soit vide. Sans mapping explicite, le user namespace est fortement restreint et ne permet pas d’obtenir des privilèges effectifs.

INFO

Lors de l’exécution de unshare -U, le shell zsh peut afficher des messages tels que : (eval):unsetopt: PRIVILEGED: can't drop privileges; failed to change group ID.

Ce comportement est lié à la gestion interne du mode PRIVILEGED de zsh lors d’un changement de contexte UID/GID atypique, comme l’entrée dans un user namespace sans mapping explicite.

Des problématiques similaires ont fait l’objet de la vulnérabilité CVE-2019-20044, où zsh ne signalait pas correctement l’échec de l’abandon de privilèges. Depuis les versions corrigées (≥ 5.8), ces erreurs sont explicitement reportées afin d’éviter toute ambiguïté ou récupération involontaire de privilèges.

Dans ce TP, ces messages sont informatifs et n’indiquent pas une vulnérabilité active : le user namespace est correctement créé et reste fortement restreint en l’absence de mapping UID/GID.

Deuxième approche

lab/linux_hardening/tp-namespaces unshare -Ur
(eval):unsetopt:1: can't drop privileges; failed to set supplementary group list: opération non permise
(eval):1: can't change option: privileged
lab/linux_hardening/tp-namespaces whoami
root
lab/linux_hardening/tp-namespaces id
uid=0(root) gid=0(root) groupes=0(root),65534(nobody)
lab/linux_hardening/tp-namespaces cat /proc/$$/uid_map
         0       1000          1
lab/linux_hardening/tp-namespaces exit

Après l’exécution de unshare -Ur, le processus se retrouve avec l’UID 0 à l’intérieur du user namespace, ce qui explique que whoami retourne root. La commande id confirme que l’utilisateur et le groupe principaux sont root, tandis que certains groupes supplémentaires restent non mappés.

Le fichier /proc/$$/uid_map montre le mapping automatique : 0 1000 1, indiquant que l’UID 0 du namespace correspond à l’UID utilisateur sur l’hôte. Cela permet d’obtenir des privilèges complets dans le namespace sans être root sur le système hôte.

5. Mount namespace et mini rootfs Alpine

Dans un premier temps, on récupère l’image :

mkdir -p alpine-root
cd alpine-root
wget https://dl-cdn.alpinelinux.org/alpine/v3.13/releases/x86_64/alpine-minirootfs-3.13.1-x86_64.tar.gz
tar -xzf alpine-minirootfs-3.13.1-x86_64.tar.gz

Puis on expérimente le montage suivant :

linux_hardening/tp-namespaces/alpine-root unshare -Ur --mount -R "$PWD" /bin/sh
 
/ # pwd
/
/ # ps
/bin/sh: ps: not found
/ # ls
/bin/sh: ls: not found
/ # whoami
root
/ # id
uid=0(root) gid=0(root) groups=65534(nobody),65534(nobody),0(root)
/ # exit

L’exécution de unshare -Ur --mount -R "$PWD" /bin/sh crée un nouveau mount namespace et définit $PWD comme nouvelle racine du système de fichiers.

La commande pwd retourne /, ce qui confirme que l’arborescence visible correspond désormais au contenu du répertoire Alpine extrait, et non plus au système de fichiers de l’hôte.

L’identité utilisateur reste root à l’intérieur du user namespace, montrant que les namespaces user et mount se combinent sans interférer.

6. PID namespace

linux_hardening/tp-namespaces/alpine-root unshare -Ur --pid
compinit:563: fork failed: ne peut allouer de la mémoire
Error: Could not source /home/ryan/.config/zsh/conf.d/hyde/terminal.zsh
/etc/profile.d/debuginfod.sh:17: fork failed: ne peut allouer de la mémoire
/etc/profile.d/flatpak.sh:30: fork failed: ne peut allouer de la mémoire
/etc/profile.d/gpm.sh:4: fork failed: ne peut allouer de la mémoire
/home/ryan/.config/zsh/.zshrc:55: fork failed: ne peut allouer de la mémoire
RyanHP# ls
zsh: fork failed: ne peut allouer de la mémoire
RyanHP#

En lançant unshare -Ur --pid sans --fork, le shell se retrouve dans un PID namespace mal initialisé pour un usage interactif. zsh tente de forker (compinit, scripts de profil), mais les appels à fork() échouent et remontent un ENOMEM (« ne peut allouer de la mémoire »), qui est ici un symptôme de refus de création de processus dans ce contexte, pas nécessairement un manque de RAM.

Avec --fork, unshare crée un processus enfant qui devient le PID 1 du namespace, ce qui correspond au modèle attendu (un « init » dans le namespace). Le shell peut alors forker normalement et l’environnement redevient stable.

linux_hardening/tp-namespaces/alpine-root unshare -Ur --pid --fork
erreur: ID de processus hors limites
 
Usage:
 ps [options]
 
 Essayez 'ps --aide <simple|liste|sortie|threads|divers|tous>'
  ou 'ps --aide <s|l|o|h|d|t>'
 pour plus d'aide.
 
Pour plus de détails, consultez ps(1).
zsh compinit: insecure directories and files, run compaudit for list.
Ignore insecure directories and files and continue [y] or abort compinit [n]? ycompinit:480: compdump: function definition file not found
Error: Could not source /home/ryan/.config/zsh/conf.d/hyde/terminal.zsh
RyanHP# ./bin/ps
zsh: aucun fichier ou dossier de ce nom: ./bin/ps
RyanHP# ps
    PID TTY          TIME CMD
    866 ?        00:00:18 systemd
    ...
  48714 ?        00:01:21 nvim

En lançant un PID namespace sans --mount-proc, la commande ps (ici via BusyBox) affiche de nombreux processus de l’hôte (systemd, waybar, pipewire). Cela s’explique par le fait que ps lit les informations depuis /proc, et que dans ce cas /proc n’a pas été remonté pour correspondre au PID namespace créé : la vue des PID est donc incohérente et reflète l’hôte.

L’option --mount-proc remonte un /proc propre au namespace et rend la vue des processus isolée (on ne voit alors plus les processus externes).

WARNING

Lancer unshare -Ur --pid --fork --mount-proc sans préciser de commande relance le shell par défaut (ici zsh), qui exécute automatiquement la configuration utilisateur et peut invoquer ps dès le démarrage. Dans ce contexte, ps (procps-ng) peut afficher « ID de processus hors limites » avant même toute commande manuelle.

linux_hardening/tp-namespaces/alpine-root unshare -Ur --pid --fork --mount-proc /bin/sh

Une fois le shell opérationnel :

sh-5.3# echo "PID=$$"
cat /proc/self/status | grep NSpid
ps
exit
PID=1
NSpid:	2
    PID TTY          TIME CMD
      1 pts/2    00:00:00 sh
      4 pts/2    00:00:00 ps
exit

Avec unshare -Ur --pid --fork --mount-proc, le shell lancé devient le processus PID 1 du namespace. Le fichier /proc est isolé, ce qui fait que ps ne montre plus que les processus appartenant au PID namespace courant. L’isolation des PID est donc effective et complète.

7. Combinaison des namespaces

linux_hardening/tp-namespaces/alpine-root unshare -Ur --mount -R "$PWD" --pid --fork --mount-proc --uts sh
 
unshare: failed to execute sh: No such file or directory
linux_hardening/tp-namespaces/alpine-root unshare -Ur --mount -R "$PWD" --pid --fork --mount-proc --uts /bin/sh
 
/ # ls
/bin/sh: ls: not found
/ # pwd
/
/ # ps
/bin/sh: ps: not found
/ # whoami
root
/ # id
uid=0(root) gid=0(root) groups=65534(nobody),65534(nobody),0(root)

NOTE

Après l’isolement du mount namespace avec -R, les commandes usuelles (ls, ps) peuvent sembler absentes. En réalité, elles existent bien (via BusyBox), mais la variable PATH n’est pas initialisée dans l’environnement minimal. Il est nécessaire de définir explicitement :

export PATH=/bin:/sbin:/usr/bin:/usr/sbin

linux_hardening/tp-namespaces/alpine-root unshare -Ur --mount -R "$PWD" --pid --fork --mount-proc --uts \
  /bin/sh -c 'export PATH=/bin:/sbin:/usr/bin:/usr/sbin; exec /bin/sh'
 
/ # ls
alpine-minirootfs-3.13.1-x86_64.tar.gz
bin
dev
etc
...

8. Namespaces réseau

TIP

Par défaut, un network namespace démarre avec l’interface loopback lo à l’état DOWN. J’ai dû activer lo dans client et server (ip link set lo up) avant de valider la connectivité. Ensuite, en liant explicitement le serveur HTTP sur l’IPv4 du veth (--bind 10.10.10.20), le curl depuis le namespace client fonctionne.

lab/linux_hardening/tp-namespaces sudo ip netns exec server /usr/bin/python3 -m http.server 8000 &
sudo ip netns exec client curl 10.10.10.20:8000
 
[2] 173124
::ffff:10.10.10.10 - - [31/Jan/2026 21:02:53] "GET / HTTP/1.1" 200 -
       <!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<style type="text/css">
:root {
color-scheme: light dark;
}
</style>
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="alpine-root/">alpine-root/</a></li>

Nettoyage

. vars
sudo ip netns del $namespace1
sudo ip netns del $namespace2