
podman run définit un contrat d'exécution : ressources autorisées, identité du processus, surface de privilèges et comportement runtime. Ce cours vous apprend à composer une commande adaptée à votre contexte, pas à mémoriser des options.
À la fin, vous saurez construire deux commandes :
- une commande dev : pratique, arrêt propre, volumes sans galères
- une commande prod : durcie, ressources limitées, privilèges minimaux
Scénario fil rouge
Section intitulée « Scénario fil rouge »Vous déployez un service web nginx. Vos contraintes :
- Protéger l'hôte : le conteneur ne doit pas consommer toute la RAM/CPU
- Limiter l'impact d'un compromis : privilèges minimaux, filesystem restreint
- Éviter les galères de volumes : l'UID dans le conteneur doit correspondre à l'hôte
- Gérer proprement l'arrêt : signaux transmis, pas de processus zombies
Chaque section répond à l'une de ces contraintes.
La checklist RUN SAFE
Section intitulée « La checklist RUN SAFE »Avant de lancer un conteneur en production, posez-vous ces 7 questions. L'acronyme RUN SAFE vous aide à n'en oublier aucune.
R, Resources
Risque : le conteneur consomme toute la RAM/CPU de l'hôte.
Options : --memory 256m --cpus 0.5 --pids-limit 100
U, User
Risque : fichiers créés appartiennent à nobody (UID 65534).
Option : --userns=keep-id
N, No-new-privs
Risque : escalade de privilèges via setuid/setgid.
Option : --security-opt no-new-privileges
S, Storage
Risque : attaquant écrit des fichiers malveillants.
Options : --read-only --tmpfs /tmp
A, Access
Risque : capabilities Linux trop permissives.
Options : --cap-drop=ALL --cap-add=...
F, Foreground
Risque : signaux non transmis, processus zombies.
Option : --init
E, Evidence
Risque : protections non appliquées (typo, override).
Vérif : podman inspect, /proc/1/status
Détail de chaque point
Section intitulée « Détail de chaque point »Chaque onglet reprend un point de la checklist selon le même plan : le risque concret si vous ne faites rien, l'option qui le traite, et la commande type. Les options sont indépendantes les unes des autres et se cumulent sans conflit sur une même ligne de commande.
Le problème : sans limite, un conteneur peut consommer 100% de la RAM ou du CPU de la machine, impactant tous les autres services.
La solution : définir des plafonds avec les cgroups.
| Option | Ce que ça limite | Exemple |
|---|---|---|
--memory | RAM maximale | --memory 256m |
--cpus | Fraction de CPU | --cpus 0.5 (50% d'un cœur) |
--pids-limit | Nombre de processus | --pids-limit 100 |
--ulimit | Ressources système | --ulimit nofile=1024:1024 |
Commande type :
podman run -d --memory 256m --cpus 0.5 --pids-limit 100 nginx:alpineLe problème : en mode rootless, le mapping UID peut créer des fichiers appartenant à nobody:65534 sur l'hôte. Impossible de les modifier sans sudo.
La solution : --userns=keep-id préserve votre UID/GID dans le conteneur.
| Mode | Comportement |
|---|---|
--userns=host | Pas de remapping (défaut) |
--userns=keep-id | Recommandé, votre UID est conservé |
--userns=nomap | Isolation max, mais volumes complexes |
Commande type :
podman run --userns=keep-id -v ./data:/app/data:Z alpine id# uid=1000(bob) gid=1000(bob)Le problème : un binaire setuid dans le conteneur peut permettre à un attaquant de passer de user à root.
La solution : --security-opt no-new-privileges empêche d'acquérir de nouveaux privilèges après le lancement.
Vérification :
podman run --rm --security-opt no-new-privileges alpine \ cat /proc/1/status | grep NoNewPrivs# NoNewPrivs: 11 = activé (bloqué), 0 = désactivé (risque).
Le problème : un attaquant qui compromet le conteneur peut écrire des backdoors ou scripts de persistance.
La solution : --read-only + --tmpfs pour les dossiers qui nécessitent l'écriture.
| Option | Ce que ça fait |
|---|---|
--read-only | Filesystem en lecture seule |
--tmpfs /tmp | Autorise l'écriture dans /tmp (RAM) |
--tmpfs /var/run | Pour les PID files, sockets |
Commande type :
podman run --read-only --tmpfs /tmp --tmpfs /var/run nginx:alpineLe problème : les capabilities Linux donnent des pouvoirs spécifiques. Par défaut, Podman en accorde ~11. Certaines (CAP_SYS_ADMIN) permettent quasi tout.
La solution : retirer toutes les capabilities, puis ajouter uniquement ce qui est nécessaire.
| Capability | Permet de... |
|---|---|
NET_BIND_SERVICE | Bind sur ports < 1024 |
CHOWN | Changer le propriétaire des fichiers |
SETUID/SETGID | Changer d'utilisateur |
SYS_ADMIN | Dangereuse, quasi tout |
Pattern moindre privilège :
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx:alpineLe problème : votre application devient PID 1 dans le conteneur. Si elle ne gère pas les signaux (SIGTERM), podman stop attend le timeout puis kill -9. Les processus enfants deviennent zombies.
La solution : --init ajoute un mini-init qui gère PID 1.
Avec --init | Sans --init |
|---|---|
| podman-init est PID 1 | Votre app est PID 1 |
| Signaux transmis proprement | Signaux potentiellement ignorés |
| Pas de zombies | Processus zombies possibles |
Commande type :
podman run --init nginx:alpineLe problème : vous pensez avoir mis les bonnes options, mais une typo ou un override les annule silencieusement.
La solution : toujours vérifier avec podman inspect et /proc/1/status.
| Vérification | Commande |
|---|---|
| Mémoire | podman inspect <nom> --format '{{.HostConfig.Memory}}' |
| Capabilities | cat /proc/1/status | grep Cap + capsh --decode= |
| NoNewPrivs | cat /proc/1/status | grep NoNewPrivs |
| Read-only | touch /test doit échouer |
| User | id doit afficher votre UID |
Droit au but
Section intitulée « Droit au but »Vous connaissez déjà les concepts et voulez juste les commandes ? Sautez directement à la recette qui vous intéresse :
Ou copiez directement un profil dev ou profil prod prêt à l'emploi.
Recette 1, Protéger l'hôte (Resources)
Section intitulée « Recette 1, Protéger l'hôte (Resources) »Un conteneur sans limite peut consommer toutes les ressources de la machine. Les options cgroups définissent des plafonds.
Limiter la mémoire
Section intitulée « Limiter la mémoire »Intention : empêcher un conteneur gourmand de déclencher l'OOM killer sur d'autres processus.
podman run -d --name web-mem --memory 256m docker.io/library/nginx:alpineVérification (la valeur est en octets) :
podman inspect web-mem --format '{{.HostConfig.Memory}}'268435456268 435 456 octets = 256 Mo. Si le conteneur dépasse, il sera tué (OOM Killed).
Limiter le CPU
Section intitulée « Limiter le CPU »Intention : éviter qu'un conteneur monopolise tous les cœurs.
podman run -d --name web-cpu --cpus 0.5 docker.io/library/nginx:alpineVérification (quota / période = fraction de CPU) :
podman inspect web-cpu --format 'Quota: {{.HostConfig.CpuQuota}} | Period: {{.HostConfig.CpuPeriod}}'Quota: 50000 | Period: 10000050000 / 100000 = 0.5 CPU. Le conteneur utilise au maximum 50% d'un cœur.
Limiter le nombre de processus
Section intitulée « Limiter le nombre de processus »Intention : empêcher les fork bombs.
podman run --rm --pids-limit=10 docker.io/library/alpine:3.21 sh -c \ "for i in 1 2 3 4 5 6 7 8 9 10 11 12; do sleep 100 & done"sh: can't fork: Resource temporarily unavailableLe conteneur ne peut pas créer plus de 10 processus.
Limiter les ressources système (ulimits)
Section intitulée « Limiter les ressources système (ulimits) »Intention : restreindre les fichiers ouverts, la mémoire verrouillée, etc.
podman run --rm --ulimit nofile=64:64 docker.io/library/alpine:3.21 sh -c "ulimit -n"64Récapitulatif Resources
Section intitulée « Récapitulatif Resources »Les quatre options se posent sur la même ligne de commande et n'ont pas de valeur par défaut : sans elles, aucune limite ne s'applique. Les trois premières s'appuient sur les cgroups et sont visibles dans podman inspect ; --ulimit agit au niveau du processus et se contrôle depuis l'intérieur du conteneur.
| Option | Usage | Exemple |
|---|---|---|
--memory | Limite RAM | --memory 512m |
--cpus | Limite CPU (fraction) | --cpus 1.5 |
--pids-limit | Limite processus | --pids-limit=100 |
--ulimit | Limites système | --ulimit nofile=1024:1024 |
Recette 2, Réduire la surface d'attaque (Access + Storage + No-new-privs)
Section intitulée « Recette 2, Réduire la surface d'attaque (Access + Storage + No-new-privs) »Un conteneur compromis ne doit pas pouvoir modifier le filesystem, escalader ses privilèges ou utiliser des capabilities dangereuses.
Filesystem en lecture seule
Section intitulée « Filesystem en lecture seule »Intention : empêcher un attaquant d'écrire des fichiers malveillants.
podman run --rm --read-only docker.io/library/alpine:3.21 touch /test.txttouch: /test.txt: Read-only file systemPattern recommandé : --read-only + --tmpfs /tmp pour autoriser l'écriture uniquement dans /tmp :
podman run --rm --read-only --tmpfs /tmp docker.io/library/alpine:3.21 sh -c \ "echo test > /tmp/ok.txt && cat /tmp/ok.txt"testBloquer l'escalade de privilèges
Section intitulée « Bloquer l'escalade de privilèges »Ce que fait no-new-privileges : empêche un processus d'acquérir des privilèges supplémentaires via setuid/setgid (ex: un binaire setuid ne peut plus élever les droits).
Vérification de l'état :
podman run --rm docker.io/library/alpine:3.21 cat /proc/1/status | grep NoNewPrivsNoNewPrivs: 0podman run --rm --security-opt no-new-privileges docker.io/library/alpine:3.21 \ cat /proc/1/status | grep NoNewPrivsNoNewPrivs: 1Capabilities minimales
Section intitulée « Capabilities minimales »Les capabilities Linux remplacent le tout-ou-rien du root par des privilèges granulaires. Par défaut, Podman attribue environ 11 capabilities.
Pattern moindre privilège :
-
Partez de
--cap-drop=ALL -
Testez votre application, elle échoue sur une action spécifique
-
Ajoutez uniquement la capability nécessaire avec
--cap-add -
Retestez, répétez si besoin
Exemple : autoriser le bind sur les ports < 1024 (nécessite NET_BIND_SERVICE) :
podman run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE \ docker.io/library/alpine:3.21 cat /proc/1/status | grep CapEffCapEff: 0000000000000400Décodage (pour comprendre la valeur hexadécimale) :
capsh --decode=00000000000004000x0000000000000400=cap_net_bind_serviceRécapitulatif sécurité
Section intitulée « Récapitulatif sécurité »Ces quatre options traitent trois surfaces distinctes : l'écriture sur le filesystem, l'escalade de privilèges et les capabilities accordées au processus. --cap-drop=ALL doit toujours précéder les --cap-add, sinon les capabilities ajoutées sont retirées à leur tour.
| Option | Usage | Exemple |
|---|---|---|
--read-only | FS lecture seule | --read-only --tmpfs /tmp |
--security-opt | Options sécurité | --security-opt no-new-privileges |
--cap-drop | Retire capabilities | --cap-drop=ALL |
--cap-add | Ajoute capability | --cap-add=NET_BIND_SERVICE |
Recette 3, Volumes sans galères (User)
Section intitulée « Recette 3, Volumes sans galères (User) »Les problèmes de permissions sur les volumes montés sont la première cause d'abandon de Podman en rootless. Ils viennent tous du même mécanisme : les user namespaces, qui traduisent les identifiants d'utilisateur entre le conteneur et l'hôte. Comprendre cette traduction règle le sujet une fois pour toutes, et une seule option suffit ensuite dans la grande majorité des cas.
Le problème des permissions
Section intitulée « Le problème des permissions »En mode rootless, l'utilisateur dans le conteneur est mappé vers votre UID hôte via les user namespaces. Mais ce mapping peut créer des incohérences sur les volumes montés.
Symptôme typique : fichiers créés par le conteneur appartenant à nobody ou 65534 sur l'hôte.
Les modes userns
Section intitulée « Les modes userns »Podman propose plusieurs modes de user namespaces :
| Mode | Comportement | Cas d'usage |
|---|---|---|
--userns=host | Pas de remapping (défaut en rootless) | Rarement le meilleur choix |
--userns=keep-id | Votre UID/GID hôte est préservé dans le conteneur | Recommandé pour les volumes |
--userns=nomap | Isolation maximale, mais volumes complexes | Conteneurs sans volume |
keep-id : le bon réflexe
Section intitulée « keep-id : le bon réflexe »keep-id demande à Podman de réserver, dans le namespace du conteneur, l'identifiant que vous portez sur l'hôte. La commande id exécutée dans le conteneur suffit à le confirmer.
podman run --rm --userns=keep-id docker.io/library/alpine:3.21 iduid=1000(bob) gid=1000(bob) groups=1000(bob)Votre utilisateur hôte (UID 1000) est préservé. Les fichiers créés sur un volume monté auront les bonnes permissions.
Combiner avec SELinux (:Z ou :z)
Section intitulée « Combiner avec SELinux (:Z ou :z) »Sur les systèmes avec SELinux (Fedora, RHEL, CentOS), ajoutez le suffixe :Z pour que le conteneur puisse accéder au volume :
podman run --rm --userns=keep-id \ -v ./data:/app/data:Z \ docker.io/library/alpine:3.21 touch /app/data/test.txt| Suffixe | Signification |
|---|---|
:Z | Relabel privé (un seul conteneur) |
:z | Relabel partagé (plusieurs conteneurs) |
:ro | Lecture seule |
Recette 4, Runtime sain (Foreground)
Section intitulée « Recette 4, Runtime sain (Foreground) »Un conteneur qui met dix secondes à s'arrêter, ou qui accumule des processus fantômes, souffre presque toujours du même défaut : personne n'assume le rôle de PID 1. Cette recette explique ce que le noyau attend de ce processus particulier et pourquoi une option d'une ligne suffit à régler la question.
Le problème du PID 1
Section intitulée « Le problème du PID 1 »Dans un conteneur, votre processus devient PID 1. Or, PID 1 a des responsabilités spéciales :
- Recevoir les signaux (SIGTERM, SIGINT)
- Récupérer les processus orphelins (éviter les zombies)
La plupart des applications ne sont pas conçues pour être PID 1.
La solution : --init
Section intitulée « La solution : --init »L'option --init ajoute un mini-init (podman-init) qui :
- Devient PID 1
- Transmet les signaux à votre application
- Nettoie les processus zombies
podman run --rm docker.io/library/alpine:3.21 ps auxPID USER TIME COMMAND 1 root 0:00 ps auxLe processus est PID 1, problèmes potentiels avec les signaux.
podman run --rm --init docker.io/library/alpine:3.21 ps auxPID USER TIME COMMAND 1 root 0:00 /run/podman-init -- ps aux 7 root 0:00 ps auxpodman-init est PID 1 et gère les signaux.
Méthode de vérification (Evidence)
Section intitulée « Méthode de vérification (Evidence) »Ne faites jamais confiance à "j'ai mis l'option". Vérifiez systématiquement.
Vérifier les limites de ressources
Section intitulée « Vérifier les limites de ressources »podman inspect lit la configuration enregistrée au lancement, ce qui permet de contrôler un conteneur déjà démarré. Une valeur à 0 signifie qu'aucune limite n'est posée, pas qu'elle vaut zéro.
podman inspect <nom> --format 'Memory: {{.HostConfig.Memory}}CPU Quota: {{.HostConfig.CpuQuota}}CPU Period: {{.HostConfig.CpuPeriod}}Pids Limit: {{.HostConfig.PidsLimit}}'Vérifier les capabilities
Section intitulée « Vérifier les capabilities »Les capabilities ne sont pas exposées en clair : /proc/1/status les publie sous forme de masque hexadécimal, la ligne CapEff donnant celles réellement effectives. capsh --decode traduit ce masque en noms lisibles.
podman run --rm <image> cat /proc/1/status | grep Cap# Puis décoder avec capsh --decode=<valeur>Vérifier no-new-privileges
Section intitulée « Vérifier no-new-privileges »Le drapeau est exposé par le noyau lui-même, ce qui en fait la preuve la plus fiable : une faute de frappe dans --security-opt laisse la valeur à 0 sans provoquer la moindre erreur au lancement.
podman run --rm <image> cat /proc/1/status | grep NoNewPrivs# 0 = désactivé, 1 = activéVérifier le user namespace
Section intitulée « Vérifier le user namespace »La commande id exécutée dans le conteneur montre l'identité telle que le conteneur la voit. Si elle renvoie uid=0(root) alors que vous avez demandé keep-id, le mapping n'a pas été appliqué et les fichiers créés sur vos volumes porteront le mauvais propriétaire.
podman run --rm --userns=keep-id <image> id# Doit afficher votre UID/GIDVérifier read-only
Section intitulée « Vérifier read-only »Le test se fait par la négative : une écriture qui réussit est le signe que la protection n'est pas active. La redirection 2>&1 est nécessaire, le message de refus arrivant sur la sortie d'erreur.
podman run --rm --read-only <image> touch /test 2>&1# Doit afficher "Read-only file system"Deux profils prêts à copier
Section intitulée « Deux profils prêts à copier »Les deux commandes ci-dessous rassemblent tout ce qui précède, appliqué au même service nginx. Le profil dev privilégie le confort de travail et ne retient que les deux options qui évitent les pertes de temps quotidiennes ; le profil prod applique les sept points de la checklist. Adaptez les valeurs de mémoire, de CPU et les capabilities à votre image : celles retenues ici sont celles dont nginx a besoin, pas un standard universel.
Profil DEV (confort + bonnes pratiques)
Section intitulée « Profil DEV (confort + bonnes pratiques) »Objectif : développement local, arrêt propre, volumes fonctionnels.
podman run -d --rm --name web-dev \ -p 8080:80 \ --init \ --userns=keep-id \ -v ./html:/usr/share/nginx/html:Z \ docker.io/library/nginx:alpinePourquoi ces options ?
| Option | Raison |
|---|---|
--init | Arrêt propre avec Ctrl+C |
--userns=keep-id | Volumes sans problèmes de permissions |
:Z | Compatibilité SELinux |
Profil PROD (durci + opérable)
Section intitulée « Profil PROD (durci + opérable) »Objectif : réduire l'impact d'un compromis, ressources bornées.
podman run -d --rm --name web-prod \ -p 8080:80 \ --memory 256m --cpus 0.5 --pids-limit 100 \ --read-only --tmpfs /tmp --tmpfs /var/cache/nginx --tmpfs /var/run \ --security-opt no-new-privileges \ --cap-drop=ALL --cap-add=NET_BIND_SERVICE --cap-add=CHOWN --cap-add=SETGID --cap-add=SETUID \ --init \ --userns=keep-id \ -v /data/nginx/html:/usr/share/nginx/html:ro,Z \ docker.io/library/nginx:alpinePourquoi ces options ?
| Option | Raison |
|---|---|
--memory 256m | Protège l'hôte contre les fuites mémoire |
--cpus 0.5 | Évite la monopolisation CPU |
--pids-limit 100 | Bloque les fork bombs |
--read-only | Empêche l'écriture de fichiers malveillants |
--tmpfs /tmp /var/cache/nginx /var/run | Nginx a besoin d'écrire dans ces dossiers |
--security-opt no-new-privileges | Bloque l'escalade via setuid |
--cap-drop=ALL --cap-add=... | Moindre privilège |
--init | Gestion propre des signaux |
:ro sur le volume | Données en lecture seule |
Pièges fréquents
Section intitulée « Pièges fréquents »Quatre situations reviennent systématiquement au moment de durcir un conteneur. Toutes se manifestent par un permission denied ou un crash au démarrage, et aucune n'appelle à retirer la protection en cause : chacune a une correction précise. La dernière section décrit la méthode à suivre quand la cause n'est pas identifiable du premier coup.
Ports < 1024 en rootless
Section intitulée « Ports < 1024 en rootless »Symptôme : permission denied sur le bind port 80.
Cause : en rootless, les ports < 1024 sont réservés.
Solutions :
- Utilisez un port > 1024 (
-p 8080:80) - Ou abaissez la limite système (non recommandé)
Volumes et UID
Section intitulée « Volumes et UID »Symptôme : permission denied en écrivant sur un volume monté.
Cause : l'UID dans le conteneur ne correspond pas à l'UID des fichiers.
Solution : --userns=keep-id
Filesystem read-only qui casse l'app
Section intitulée « Filesystem read-only qui casse l'app »Symptôme : l'application crash au démarrage avec --read-only.
Cause : l'app a besoin d'écrire dans /tmp, /var/run, etc.
Solution : ajoutez les tmpfs nécessaires (--tmpfs /tmp --tmpfs /var/run).
Capabilities insuffisantes
Section intitulée « Capabilities insuffisantes »Symptôme : l'application échoue avec --cap-drop=ALL.
Démarche :
- Lancez avec
--cap-drop=ALL - Observez l'erreur (souvent explicite)
- Ajoutez la capability minimale avec
--cap-add - Retestez
Debug : enlever temporairement une protection
Section intitulée « Debug : enlever temporairement une protection »Si l'application ne fonctionne pas et vous ne trouvez pas pourquoi :
- Retirez une option à la fois (read-only, cap-drop, etc.)
- Testez après chaque retrait
- Identifiez l'option bloquante
- Cherchez le tmpfs ou la capability manquante
- Remettez la protection avec l'ajustement
À retenir
Section intitulée « À retenir »- Checklist RUN SAFE : Resources, User, No-new-privs, Storage, Access, Foreground, Evidence
- Chaque option répond à une intention : protéger l'hôte, limiter un compromis, simplifier les volumes
- Profil dev :
--init --userns=keep-idminimum - Profil prod : ajoutez
--memory --cpus --read-only --cap-drop=ALL --security-opt no-new-privileges - Toujours vérifier avec
podman inspectet/proc/1/status - Déboguez en retirant une option à la fois, jamais en désactivant tout