Aller au contenu
Conteneurs & Orchestration medium

Podman run avancé : composer une commande sûre et stable

21 min de lecture

logo podman

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

Vous déployez un service web nginx. Vos contraintes :

  1. Protéger l'hôte : le conteneur ne doit pas consommer toute la RAM/CPU
  2. Limiter l'impact d'un compromis : privilèges minimaux, filesystem restreint
  3. Éviter les galères de volumes : l'UID dans le conteneur doit correspondre à l'hôte
  4. Gérer proprement l'arrêt : signaux transmis, pas de processus zombies

Chaque section répond à l'une de ces contraintes.

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

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.

OptionCe que ça limiteExemple
--memoryRAM maximale--memory 256m
--cpusFraction de CPU--cpus 0.5 (50% d'un cœur)
--pids-limitNombre de processus--pids-limit 100
--ulimitRessources système--ulimit nofile=1024:1024

Commande type :

Fenêtre de terminal
podman run -d --memory 256m --cpus 0.5 --pids-limit 100 nginx:alpine

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.

Un conteneur sans limite peut consommer toutes les ressources de la machine. Les options cgroups définissent des plafonds.

Intention : empêcher un conteneur gourmand de déclencher l'OOM killer sur d'autres processus.

Fenêtre de terminal
podman run -d --name web-mem --memory 256m docker.io/library/nginx:alpine

Vérification (la valeur est en octets) :

Fenêtre de terminal
podman inspect web-mem --format '{{.HostConfig.Memory}}'
Résultat
268435456

268 435 456 octets = 256 Mo. Si le conteneur dépasse, il sera tué (OOM Killed).

Intention : éviter qu'un conteneur monopolise tous les cœurs.

Fenêtre de terminal
podman run -d --name web-cpu --cpus 0.5 docker.io/library/nginx:alpine

Vérification (quota / période = fraction de CPU) :

Fenêtre de terminal
podman inspect web-cpu --format 'Quota: {{.HostConfig.CpuQuota}} | Period: {{.HostConfig.CpuPeriod}}'
Résultat
Quota: 50000 | Period: 100000

50000 / 100000 = 0.5 CPU. Le conteneur utilise au maximum 50% d'un cœur.

Intention : empêcher les fork bombs.

Fenêtre de terminal
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"
Résultat
sh: can't fork: Resource temporarily unavailable

Le conteneur ne peut pas créer plus de 10 processus.

Intention : restreindre les fichiers ouverts, la mémoire verrouillée, etc.

Fenêtre de terminal
podman run --rm --ulimit nofile=64:64 docker.io/library/alpine:3.21 sh -c "ulimit -n"
Résultat
64

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.

OptionUsageExemple
--memoryLimite RAM--memory 512m
--cpusLimite CPU (fraction)--cpus 1.5
--pids-limitLimite processus--pids-limit=100
--ulimitLimites 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.

Intention : empêcher un attaquant d'écrire des fichiers malveillants.

Fenêtre de terminal
podman run --rm --read-only docker.io/library/alpine:3.21 touch /test.txt
Résultat
touch: /test.txt: Read-only file system

Pattern recommandé : --read-only + --tmpfs /tmp pour autoriser l'écriture uniquement dans /tmp :

Fenêtre de terminal
podman run --rm --read-only --tmpfs /tmp docker.io/library/alpine:3.21 sh -c \
"echo test > /tmp/ok.txt && cat /tmp/ok.txt"
Résultat
test

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 :

Fenêtre de terminal
podman run --rm docker.io/library/alpine:3.21 cat /proc/1/status | grep NoNewPrivs
Résultat
NoNewPrivs: 0

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 :

  1. Partez de --cap-drop=ALL

  2. Testez votre application, elle échoue sur une action spécifique

  3. Ajoutez uniquement la capability nécessaire avec --cap-add

  4. Retestez, répétez si besoin

Exemple : autoriser le bind sur les ports < 1024 (nécessite NET_BIND_SERVICE) :

Fenêtre de terminal
podman run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
docker.io/library/alpine:3.21 cat /proc/1/status | grep CapEff
Résultat
CapEff: 0000000000000400

Décodage (pour comprendre la valeur hexadécimale) :

Fenêtre de terminal
capsh --decode=0000000000000400
Résultat
0x0000000000000400=cap_net_bind_service

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.

OptionUsageExemple
--read-onlyFS lecture seule--read-only --tmpfs /tmp
--security-optOptions sécurité--security-opt no-new-privileges
--cap-dropRetire capabilities--cap-drop=ALL
--cap-addAjoute capability--cap-add=NET_BIND_SERVICE

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.

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.

Podman propose plusieurs modes de user namespaces :

ModeComportementCas d'usage
--userns=hostPas de remapping (défaut en rootless)Rarement le meilleur choix
--userns=keep-idVotre UID/GID hôte est préservé dans le conteneurRecommandé pour les volumes
--userns=nomapIsolation maximale, mais volumes complexesConteneurs sans volume

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.

Fenêtre de terminal
podman run --rm --userns=keep-id docker.io/library/alpine:3.21 id
Résultat
uid=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.

Sur les systèmes avec SELinux (Fedora, RHEL, CentOS), ajoutez le suffixe :Z pour que le conteneur puisse accéder au volume :

Fenêtre de terminal
podman run --rm --userns=keep-id \
-v ./data:/app/data:Z \
docker.io/library/alpine:3.21 touch /app/data/test.txt
SuffixeSignification
:ZRelabel privé (un seul conteneur)
:zRelabel partagé (plusieurs conteneurs)
:roLecture seule

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.

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.

L'option --init ajoute un mini-init (podman-init) qui :

  • Devient PID 1
  • Transmet les signaux à votre application
  • Nettoie les processus zombies
Fenêtre de terminal
podman run --rm docker.io/library/alpine:3.21 ps aux
Résultat
PID USER TIME COMMAND
1 root 0:00 ps aux

Le processus est PID 1, problèmes potentiels avec les signaux.

Ne faites jamais confiance à "j'ai mis l'option". Vérifiez systématiquement.

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.

Fenêtre de terminal
podman inspect <nom> --format '
Memory: {{.HostConfig.Memory}}
CPU Quota: {{.HostConfig.CpuQuota}}
CPU Period: {{.HostConfig.CpuPeriod}}
Pids Limit: {{.HostConfig.PidsLimit}}'

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.

Fenêtre de terminal
podman run --rm <image> cat /proc/1/status | grep Cap
# Puis décoder avec capsh --decode=<valeur>

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.

Fenêtre de terminal
podman run --rm <image> cat /proc/1/status | grep NoNewPrivs
# 0 = désactivé, 1 = activé

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.

Fenêtre de terminal
podman run --rm --userns=keep-id <image> id
# Doit afficher votre UID/GID

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.

Fenêtre de terminal
podman run --rm --read-only <image> touch /test 2>&1
# Doit afficher "Read-only file system"

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.

Objectif : développement local, arrêt propre, volumes fonctionnels.

Fenêtre de terminal
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:alpine

Pourquoi ces options ?

OptionRaison
--initArrêt propre avec Ctrl+C
--userns=keep-idVolumes sans problèmes de permissions
:ZCompatibilité SELinux

Objectif : réduire l'impact d'un compromis, ressources bornées.

Fenêtre de terminal
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:alpine

Pourquoi ces options ?

OptionRaison
--memory 256mProtège l'hôte contre les fuites mémoire
--cpus 0.5Évite la monopolisation CPU
--pids-limit 100Bloque les fork bombs
--read-onlyEmpêche l'écriture de fichiers malveillants
--tmpfs /tmp /var/cache/nginx /var/runNginx a besoin d'écrire dans ces dossiers
--security-opt no-new-privilegesBloque l'escalade via setuid
--cap-drop=ALL --cap-add=...Moindre privilège
--initGestion propre des signaux
:ro sur le volumeDonnées en lecture seule

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.

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é)

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

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).

Symptôme : l'application échoue avec --cap-drop=ALL.

Démarche :

  1. Lancez avec --cap-drop=ALL
  2. Observez l'erreur (souvent explicite)
  3. Ajoutez la capability minimale avec --cap-add
  4. Retestez

Si l'application ne fonctionne pas et vous ne trouvez pas pourquoi :

  1. Retirez une option à la fois (read-only, cap-drop, etc.)
  2. Testez après chaque retrait
  3. Identifiez l'option bloquante
  4. Cherchez le tmpfs ou la capability manquante
  5. Remettez la protection avec l'ajustement
  • 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-id minimum
  • Profil prod : ajoutez --memory --cpus --read-only --cap-drop=ALL --security-opt no-new-privileges
  • Toujours vérifier avec podman inspect et /proc/1/status
  • Déboguez en retirant une option à la fois, jamais en désactivant tout

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn