Comprendre les conteneurs Linux : namespaces, cgroups v2 et Podman rootless
EdwardMoon
Un conteneur Linux n'est pas une petite machine virtuelle. C'est un processus qui partage le noyau Linux de l'hôte : les espaces de noms de processus, de montage, de réseau et d'utilisateurs isolent sa vue du système, tandis que les cgroups limitent le CPU, la mémoire ou les PID. Cette distinction est essentielle pour concevoir les permissions, les images, les volumes, le réseau et l'isolation des pannes.
Ce laboratoire utilise cgroup v2 et Podman rootless sous Rocky Linux 9. Conservez SELinux et firewalld activés, évitez d'exécuter des scripts d'installation distants directement dans un shell et consignez les empreintes et la provenance des images au lieu de vous fier aux seuls tags. Les résultats dépendent des versions de Podman et du noyau installées : vérifiez l'état réel à chaque étape.

Conteneurs Linux et machines virtuelles
| Critère | Conteneur | Machine virtuelle |
|---|---|---|
| Kernel | Partage le noyau de l'hôte | Noyau distinct pour chaque système invité |
| Isolation | namespaces·LSM·seccomp·capabilities | Hyperviseur et matériel virtuel |
| Image | Couches OCI et métadonnées | Image disque complète |
| Démarrage | Création d'un processus isolé | Démarrage d'un système invité |
| Ressources | cgroups et limites du runtime | vCPU, RAM et périphériques virtuels alloués |
| Frontière de sécurité | Exposition commune aux vulnérabilités du noyau | Noyaux séparés entre l'invité et l'hôte |
Composants d'un conteneur Linux
| Composant | Rôle | Vérification courante |
|---|---|---|
| OCI image | Couches du système de fichiers racine et métadonnées d'exécution | podman image inspect·history |
| Container engine | pull·build·network·storage·lifecycle | podman info |
| OCI runtime | Crée les namespaces et cgroups, puis démarre le processus | crun --version ou runc --version |
| conmon | Surveille les processus et gère les entrées-sorties standard et les arrêts | podman info –debug |
| namespaces | Isole les identifiants, montages et réseaux visibles du processus | lsns·/proc/PID/ns |
| cgroups v2 | Mesure et limite le CPU, la mémoire, les entrées-sorties et les PID | podman stats·/sys/fs/cgroup |
| SELinux/seccomp | Restreint l'accès aux fichiers et aux appels système | getenforce·podman inspect |
Vérifier l'environnement du laboratoire
Vérifier Podman et la prise en charge par le noyau
uname -r
cat /etc/os-release
podman --version
podman info --debug
podman info --format '{{.Host.CgroupsVersion}}'
stat -fc %T /sys/fs/cgroup
getenforce
systemctl is-active firewalld
Vérifiez que le système de fichiers indique cgroup2fs et que Podman annonce cgroup v2. Quadlet exige cgroup v2. Avec SELinux en mode Enforcing, attribuez les bonnes étiquettes aux volumes ; ne désactivez pas tout le mécanisme de sécurité pour contourner une erreur de configuration.
Vérifier les plages UID et GID du mode rootless
id
grep -E "^${USER}:" /etc/subuid /etc/subgid
command -v newuidmap newgidmap
command -v pasta
podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map
Podman rootless utilise les plages d'identifiants subordonnés de /etc/subuid et /etc/subgid pour associer les UID du conteneur à des UID non privilégiés de l'hôte. Si aucune plage n'est attribuée, un administrateur doit en allouer sans chevauchement avec usermod --add-subuids et --add-subgids. Les répertoires personnels sur NFS ne comprennent pas les espaces de noms d'utilisateurs : envisagez un système de fichiers local pour le graphroot rootless.
Utiliser les namespaces Linux
Un namespace isole la vue d'un processus sur des ressources globales. Celui des PID lui donne une vue distincte des identifiants de processus ; celui des montages sépare les tables de montage ; celui du réseau isole les interfaces, routes et ports ; celui des utilisateurs sépare les correspondances UID/GID et la portée des capabilities.
Lister les namespaces actuels
lsns
readlink /proc/self/ns/user
readlink /proc/self/ns/pid
readlink /proc/self/ns/mnt
readlink /proc/self/ns/net
Créer des namespaces utilisateur et PID sans privilèges
unshare --user --map-root-user --pid --fork sh -c '
id
echo "namespace PID: $$"
readlink /proc/self/ns/user
readlink /proc/self/ns/pid
'
Le uid=0 affiché désigne root dans le nouvel espace de noms utilisateur, et non root sur l'hôte. Les correspondances UID et les capabilities autorisées restent limitées : le processus ne peut pas lire tous les fichiers de l'hôte ni contrôler n'importe quel périphérique. Le noyau reste toutefois partagé ; ses vulnérabilités et les montages inadaptés de périphériques ou de sockets demeurent des risques.
Images OCI et empreintes
Une image se compose de couches immuables et de métadonnées : configuration, point d'entrée et variables d'environnement. Un tag de registre peut être déplacé vers une autre empreinte. En production, consignez l'empreinte vérifiée du manifeste et appliquez des politiques relatives aux signatures, aux SBOM et aux vulnérabilités.
Télécharger une image avec son nom complet
IMAGE='docker.io/library/busybox:1.36.1'
podman pull "$IMAGE"
podman image inspect "$IMAGE" --format 'ID={{.Id}} Digest={{.Digest}} Created={{.Created}}'
podman history --no-trunc "$IMAGE"
podman images --digests
Un nom court peut désigner différents registres selon registries.conf. Utilisez le nom complet, registre et espace de noms compris. Si vous utilisez un tag par commodité en développement, consignez l'empreinte renvoyée par inspect dans le dossier de validation, puis fixez l'image avec image@sha256 dans Quadlet et les manifestes de déploiement.
Examiner le manifeste OCI
skopeo inspect docker://docker.io/library/busybox:1.36.1 | jq '{Name,Digest,Created,Architecture,Os}'
skopeo inspect --raw docker://docker.io/library/busybox:1.36.1 | jq .
Exécuter un conteneur avec Podman rootless
Exposez BusyBox httpd uniquement sur l'interface de bouclage, passez le système de fichiers racine en lecture seule, retirez toutes les capabilities par défaut, empêchez l'élévation de privilèges et limitez les PID, la mémoire et le CPU. Fournissez uniquement les emplacements inscriptibles nécessaires via tmpfs ou des volumes explicites.
IMAGE='docker.io/library/busybox:1.36.1'
install -d -m 0750 "$HOME/container-data"
printf 'hello from rootless Podman\n' > "$HOME/container-data/index.html"
podman run --detach --rm --name web-demo \
--read-only --cap-drop=all --security-opt=no-new-privileges \
--pids-limit=128 --memory=256m --cpus=0.50 \
--publish 127.0.0.1:8080:8080 \
--volume "$HOME/container-data:/www:ro,Z" \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=32m \
"$IMAGE" httpd -f -p 8080 -h /www
Vérifier l'état d'exécution et les limites
podman ps
podman port web-demo
curl --fail --silent --show-error http://127.0.0.1:8080/
podman stats --no-stream web-demo
podman top web-demo user hpid pid args
podman inspect web-demo > web-demo.inspect.json
jq '.[0].HostConfig | {ReadonlyRootfs,Memory,NanoCpus,PidsLimit}' web-demo.inspect.json
Omettre l'adresse IP de l'hôte dans --publish peut exposer le port sur toutes les interfaces. Pour un service derrière un proxy inverse local, utilisez 127.0.0.1. Avant toute exposition externe, examinez séparément les zones firewalld, les sources autorisées, TLS et l'authentification.
Suivre les processus et namespaces du conteneur
HOST_PID=$(podman inspect --format '{{.State.Pid}}' web-demo)
printf 'host PID=%s
' "$HOST_PID"
ps -o user,pid,ppid,cmd -p "$HOST_PID"
sudo lsns -p "$HOST_PID"
sudo readlink "/proc/${HOST_PID}/ns/user"
sudo readlink "/proc/${HOST_PID}/ns/net"
sudo cat "/proc/${HOST_PID}/cgroup"
Le PID 1 du conteneur apparaît comme un PID ordinaire sur l'hôte. nsenter donne un accès puissant pour diagnostiquer une frontière d'isolation : limitez son usage en exploitation. Commencez par les interfaces du moteur, comme podman exec, logs et inspect.
podman exec web-demo sh -c '
echo "container PID=$$"
id
cat /proc/self/status | grep -E "^(Name|Pid|NSpid|CapEff|NoNewPrivs):"
'
podman logs web-demo
podman events --since 10m --filter container=web-demo
Utiliser cgroups v2
cgroups v2 mesure et limite les ressources ; il ne fournit pas l'isolation des namespaces. Les limites mémoire contiennent les risques d'OOM, celles des PID limitent les fork bombs et les quotas CPU réduisent les interférences entre charges voisines. En mode rootless, la délégation systemd utilisateur et la politique de l'hôte peuvent empêcher l'utilisation de certains contrôleurs.
podman stats --no-stream web-demo
podman inspect web-demo --format '{{.State.CgroupPath}}'
systemd-cgls "/user.slice/user-${UID}.slice"
systemctl --user status
podman update --memory=192m --pids-limit=96 web-demo
podman stats --no-stream web-demo
Des limites trop basses peuvent provoquer des arrêts OOM ou des requêtes en échec sous une charge normale. Réglez-les avec le modèle mémoire de l'application, les paramètres JVM ou le nombre de workers, les contrôles de santé et la politique de redémarrage. Surveillez aussi la marge de ressources de l'hôte et les indicateurs PSI de pression sur les ressources.
Volumes et SELinux
Utiliser un répertoire hôte dédié et une étiquette privée
install -d -m 0750 "$HOME/volume-label-demo"
printf 'hello from a labeled volume\n' > "$HOME/volume-label-demo/index.html"
podman run --rm --read-only --cap-drop=all \
--security-opt=no-new-privileges \
--volume "$HOME/volume-label-demo:/data:ro,Z" \
docker.io/library/busybox:1.36.1 cat /data/index.html
ls -Zd "$HOME/volume-label-demo"
L'option :Z applique au chemin une étiquette SELinux privée pour un seul conteneur. Envisagez :z pour un chemin partagé entre plusieurs conteneurs. Réétiqueter largement des répertoires système ou personnels sensibles peut toutefois perturber les services de l'hôte. Montez uniquement des répertoires dédiés et planifiez d'abord les sauvegardes, les propriétaires et les correspondances UID.
Pratiques dangereuses pour la sécurité des conteneurs
| Pratique | Risque | Solution recommandée |
|---|---|---|
| Mode entièrement privilégié | Désactive la plupart des restrictions de périphériques, capabilities et LSM | Autoriser uniquement les capabilities et périphériques nécessaires |
| host network/PID | Partage les namespaces et la visibilité de l'hôte | Utiliser un namespace réseau dédié et des publications de ports explicites |
| Podman/Docker socket mount | Permet de créer arbitrairement des conteneurs et montages sur l'hôte | Utiliser un proxy API restreint ou un compte d'automatisation distinct |
| latest tag | Les redéploiements peuvent produire des résultats différents | Utiliser une empreinte vérifiée et une politique de signature |
| Désactivation complète des protections | Supprime la protection SELinux et du pare-feu | Adapter uniquement les étiquettes, ports et politiques nécessaires |
| Exécution de la sortie de curl dans un shell | Exécute du code distant sans examen ni contrôle d'intégrité | Utiliser les paquets officiels et vérifier les signatures ou sommes de contrôle |
Auditer les permissions et montages
podman inspect web-demo --format '{{json .HostConfig.SecurityOpt}} {{json .HostConfig.CapDrop}}'
podman inspect web-demo --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} rw={{.RW}}{{println}}{{end}}'
podman diff web-demo
podman top web-demo capeff label
Gérer les conteneurs avec Quadlet
Au lieu d'encapsuler un podman run ponctuel dans un script shell, placez un fichier Quadlet .container dans le chemin de recherche des unités utilisateur rootless pour confier le cycle de vie et les journaux à systemd. En production, remplacez la valeur Image ci-dessous par une empreinte sha256 vérifiée.
Arrêter le conteneur temporaire du laboratoire
podman stop web-demo
podman ps --all --filter name=web-demo
# Le conteneur ayant été lancé avec --rm, il doit disparaître après un arrêt normal.
podman container exists web-demo; printf 'exit=%s
' "$?"
mkdir -p "$HOME/.config/containers/systemd"
${EDITOR:-vi} "$HOME/.config/containers/systemd/web-demo.container"
~/.config/containers/systemd/web-demo.container
[Unit]
Description=Rootless read-only web demo
[Container]
Image=docker.io/library/busybox:1.36.1
ContainerName=web-demo
Exec=httpd -f -p 8080 -h /www
Volume=%h/container-data:/www:ro,Z
PublishPort=127.0.0.1:8080:8080
ReadOnly=true
NoNewPrivileges=true
DropCapability=all
PidsLimit=128
[Service]
MemoryMax=256M
Restart=on-failure
TimeoutStartSec=120
[Install]
WantedBy=default.target
Examiner la sortie Quadlet et l'état du service
mkdir -p "$HOME/.config/containers/systemd"
chmod 0700 "$HOME/.config/containers/systemd"
chmod 0644 "$HOME/.config/containers/systemd/web-demo.container"
systemctl --user daemon-reload
systemctl --user start web-demo.service
systemctl --user status web-demo.service
journalctl --user -u web-demo.service --since '-10 min'
podman info --format '{{.Host.CgroupsVersion}}'
curl --fail --silent --show-error http://127.0.0.1:8080/
N'activez pas directement le service généré par Quadlet. Laissez le générateur traiter [Install] WantedBy dans le fichier .container. Un administrateur peut utiliser loginctl enable-linger pour maintenir un service rootless après déconnexion, après avoir validé les conséquences d'exploitation et de sécurité de processus continuant à tourner sans session ouverte.
Démarche de diagnostic d'un conteneur
- Utilisez podman ps --all et l'état du service pour repérer les codes de sortie et les boucles de redémarrage.
- Corrélez les horodatages des événements de l'application et du moteur avec podman logs et events.
- Examinez l'ID et l'empreinte de l'image, la commande, l'environnement, les montages et les options de sécurité.
- Vérifiez les adresses de publication des ports, le réseau rootless, le DNS et les règles du pare-feu hôte.
- Examinez les refus AVC SELinux, les étiquettes des volumes, les correspondances UID et les permissions.
- Vérifiez les limites cgroup de mémoire, PID et CPU, ainsi que les indicateurs OOM et de pression.
- Reproduisez le problème sur un hôte isolé avec la même empreinte et une entrée minimale.
podman ps --all --size
podman inspect web-demo
podman logs --timestamps web-demo
podman events --since 30m
podman stats --no-stream web-demo
journalctl --user -u web-demo.service --since '-30 min'
sudo ausearch -m AVC,USER_AVC -ts recent
ss -lntp | grep ':8080'
Points à vérifier en exploitation
- Comprendre la différence de noyau partagé entre conteneurs et VM et les rôles distincts des namespaces et cgroups.
- Vérifier les plages subuid/subgid, cgroup v2, le runtime, le stockage local et les outils réseau en mode rootless.
- Consigner les noms complets des registres, les empreintes vérifiées, les signatures, les SBOM et les résultats d'analyse de vulnérabilités.
- Appliquer un rootfs en lecture seule, no-new-privileges, la suppression des capabilities et les limites PID, mémoire et CPU.
- Publier les ports uniquement sur les IP nécessaires et éviter le mode privilégié, les namespaces hôtes et les sockets du moteur.
- Utiliser des volumes dédiés et des étiquettes SELinux en gardant les protections actives.
- Vérifier le service Quadlet, les journaux, la santé, la sauvegarde-restauration et le retour arrière des mises à jour.
Documentation officielle et articles associés
- Documentation officielle de Podman, dont le mode rootless
- Options de sécurité et de ressources de podman run
- Documentation officielle de Podman Quadlet
- Documentation cgroup v2 du noyau Linux
- Manuel des namespaces Linux
- Architecture du noyau Linux et diagnostic
- Déploiement sécurisé et sauvegardes de NetBox avec Docker
Conclusion
Exploiter des conteneurs Linux en sécurité exige davantage qu'une commande de lancement. Il faut comprendre les risques du noyau partagé et gérer ensemble namespaces, cgroups v2, correspondances utilisateur, SELinux et chaîne d'approvisionnement OCI. Privilégiez le mode rootless, fixez les empreintes des images, réduisez les capabilities, montages et ports, et appliquez des systèmes de fichiers en lecture seule et des limites de ressources. Vérifiez enfin les journaux Quadlet/systemd, la santé, les sauvegardes et le retour arrière pour rendre le service reproductible.