
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 »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 »| 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é »| 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) »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 »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) »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 <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 »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 »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 »podman run --rm --userns=keep-id <image> id# Doit afficher votre UID/GIDVérifier read-only
Section intitulée « Vérifier read-only »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 »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 »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