Aller au contenu
Conteneurs & Orchestration medium

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

17 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

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

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

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.

Fenêtre de terminal
podman inspect <nom> --format '
Memory: {{.HostConfig.Memory}}
CPU Quota: {{.HostConfig.CpuQuota}}
CPU Period: {{.HostConfig.CpuPeriod}}
Pids Limit: {{.HostConfig.PidsLimit}}'
Fenêtre de terminal
podman run --rm <image> cat /proc/1/status | grep Cap
# Puis décoder avec capsh --decode=<valeur>
Fenêtre de terminal
podman run --rm <image> cat /proc/1/status | grep NoNewPrivs
# 0 = désactivé, 1 = activé
Fenêtre de terminal
podman run --rm --userns=keep-id <image> id
# Doit afficher votre UID/GID
Fenêtre de terminal
podman run --rm --read-only <image> touch /test 2>&1
# Doit afficher "Read-only file system"

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

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 tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.