Fullmoon System

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.

Architecture des conteneurs Linux : utilisateurs non privilégiés, couches OCI, namespaces, noyau partagé et cgroups v2
Les environnements isolés issus des couches d'images OCI partagent un noyau Linux ; les cgroups contrôlent leurs ressources

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
Le mode rootless associe root dans le conteneur à des UID non privilégiés sur l'hôte, ce qui réduit les dommages possibles sans constituer une frontière de sécurité absolue. Maintenez à jour le noyau hôte, le moteur, le runtime OCI et les images, et évitez les capabilities, périphériques, sockets et espaces de noms hôtes inutiles.

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

  1. Utilisez podman ps --all et l'état du service pour repérer les codes de sortie et les boucles de redémarrage.
  2. Corrélez les horodatages des événements de l'application et du moteur avec podman logs et events.
  3. Examinez l'ID et l'empreinte de l'image, la commande, l'environnement, les montages et les options de sécurité.
  4. Vérifiez les adresses de publication des ports, le réseau rootless, le DNS et les règles du pare-feu hôte.
  5. Examinez les refus AVC SELinux, les étiquettes des volumes, les correspondances UID et les permissions.
  6. Vérifiez les limites cgroup de mémoire, PID et CPU, ainsi que les indicateurs OOM et de pression.
  7. 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

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.