Sécurité Docker
Un container Docker mal configuré peut compromettre l’hôte entier. La sécurité Docker s’articule autour de quatre axes : les images, la configuration runtime, les secrets, et le réseau.
Sécurité des images§
Images de base minimales§
# Mauvais : image complète avec shell, outils, surfaces d'attaque
FROM python:3.12
# Mieux : image slim (sans outils inutiles)
FROM python:3.12-slim
# Optimal : distroless (pas de shell, pas de package manager)
FROM gcr.io/distroless/python3-debian12
# Alpine (très petit, mais musl libc — attention aux incompatibilités)
FROM python:3.12-alpine
Dockerfile sécurisé§
# Épingler par digest SHA256 (tag mutable peut être modifié)
FROM python:3.12-slim@sha256:abc123def456...
# Métadonnées de traçabilité
LABEL maintainer="[email protected]" \
version="1.0" \
org.opencontainers.image.source="https://github.com/org/repo"
# Mettre à jour les paquets et supprimer le cache en une seule couche
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Créer un utilisateur non-root
RUN groupadd -r appuser && useradd -r -g appuser -u 1000 appuser
WORKDIR /app
# Copier d'abord les dépendances (meilleure utilisation du cache)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copier le code source
COPY --chown=appuser:appuser . .
# Passer à l'utilisateur non-root
USER appuser
# Ne pas exposer de port root (<1024)
EXPOSE 8080
# Utiliser exec form pour les signaux Unix
ENTRYPOINT ["python", "-m", "uvicorn"]
CMD ["main:app", "--host", "0.0.0.0", "--port", "8080"]
Analyse avec hadolint§
# Linter de Dockerfile
hadolint Dockerfile
# Dans CI (GitLab)
hadolint_check:
image: hadolint/hadolint
script:
- hadolint Dockerfile
Règles importantes :
DL3008: épingler les versions de paquets aptDL3025: utiliser exec form pour CMDDL4006: utiliserset -o pipefaildans les RUN
Scan de vulnérabilités§
# Trivy — scanner de CVEs le plus utilisé
trivy image python:3.12-slim
trivy image --severity CRITICAL,HIGH monapp:latest
# Format JSON pour intégration CI
trivy image --format json --output trivy.json monapp:latest
# Scan du Dockerfile (mauvaises configurations)
trivy config Dockerfile
# Grype (Anchore)
grype monapp:latest
grype --fail-on high monapp:latest # retourne exit code 1 si HIGH+
Configuration runtime sécurisée§
docker run sécurisé§
# Mauvais : container avec trop de privilèges
docker run -it --privileged -v /:/host ubuntu bash
# Bon : limiter au strict nécessaire
docker run \
--read-only \ # filesystem en lecture seule
--tmpfs /tmp:rw,noexec,nosuid \ # /tmp en mémoire (rw mais pas exec)
--user 1000:1000 \ # user non-root
--cap-drop ALL \ # supprimer toutes les capabilities
--cap-add NET_BIND_SERVICE \ # n'ajouter que ce qui est nécessaire
--security-opt no-new-privileges \ # empêcher escalade de privilèges
--security-opt seccomp=seccomp.json \ # profil seccomp personnalisé
--pids-limit 100 \ # limiter le fork bombing
--memory 512m \ # limiter la mémoire
--cpu-shares 512 \ # limiter le CPU
--network app-network \ # réseau dédié, pas de réseau host
monapp:latest
Linux Capabilities§
Les capabilities décomposent les privilèges root en unités granulaires.
| Capability | Risque | Usage légitime |
|---|---|---|
SYS_ADMIN | Critique (quasi-root) | Très rare |
NET_ADMIN | Élevé | Outils réseau |
SYS_PTRACE | Élevé | Débogage |
NET_BIND_SERVICE | Faible | Écouter sur un port < 1024 |
CHOWN | Moyen | Changer les propriétaires de fichiers |
# Vérifier les capabilities d'un container en cours
docker inspect <id> | jq '.[0].HostConfig.CapAdd, .[0].HostConfig.CapDrop'
Profils seccomp§
Seccomp filtre les appels système disponibles pour le container.
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": ["read", "write", "open", "close", "stat", "fstat",
"mmap", "mprotect", "munmap", "brk", "rt_sigaction",
"rt_sigprocmask", "ioctl", "access", "pipe", "select",
"sched_yield", "mremap", "msync", "mincore", "madvise",
"dup", "dup2", "pause", "nanosleep", "getitimer",
"alarm", "setitimer", "getpid", "socket", "connect",
"accept", "sendto", "recvfrom", "sendmsg", "recvmsg",
"shutdown", "bind", "listen", "getsockname",
"getpeername", "socketpair", "setsockopt", "getsockopt",
"clone", "fork", "vfork", "execve", "exit", "wait4",
"kill", "uname", "getuid", "getgid", "setuid", "setgid",
"exit_group", "futex", "set_tid_address"],
"action": "SCMP_ACT_ALLOW"
}
]
}
docker run --security-opt seccomp=seccomp.json monapp:latest
Gestion des secrets§
Ce qu’il ne faut pas faire§
# NE JAMAIS mettre des secrets dans le Dockerfile
ENV DB_PASSWORD=motdepasse123 # visible dans les layers
ARG API_KEY=secret # visible dans l'historique de build
# NE JAMAIS mettre des secrets dans les variables d'env docker-compose
environment:
- DB_PASSWORD=motdepasse123 # visible dans docker inspect
Docker Secrets (Swarm)§
# Créer un secret
echo "motdepasse123" | docker secret create db_password -
# Utiliser dans un service Swarm
docker service create \
--secret db_password \
monapp:latest
# Monté à /run/secrets/db_password (fichier, non en env)
BuildKit et secrets de build§
# Monter un secret au moment du build sans le laisser dans les layers
RUN --mount=type=secret,id=pip_token \
pip install --index-url https://$(cat /run/secrets/pip_token)@pypi.company.com/simple/ monpaquetprive
docker build --secret id=pip_token,src=./token.txt .
Approche recommandée en production§
# Docker Compose — injecter depuis les variables d'environnement de l'hôte
# (peuplées depuis un vault comme HashiCorp Vault ou AWS Secrets Manager)
services:
app:
image: monapp:latest
environment:
DB_PASSWORD: ${DB_PASSWORD} # depuis le shell, pas dans le fichier
secrets:
- db_password
secrets:
db_password:
external: true
Réseau Docker sécurisé§
# Isoler les services dans des réseaux dédiés
docker network create --driver bridge frontend-net
docker network create --driver bridge backend-net
# Le frontend accède uniquement au réseau frontend
docker run --network frontend-net nginx
# L'app accède aux deux réseaux
docker run --network frontend-net --network backend-net app
# La DB n'est accessible que depuis le réseau backend
docker run --network backend-net postgres
# Ne jamais utiliser --network host en production
# (expose tous les ports de l'hôte au container)
# docker-compose.yml avec isolation réseau
services:
nginx:
networks: [frontend]
app:
networks: [frontend, backend]
db:
networks: [backend]
networks:
frontend:
backend:
internal: true # pas d'accès internet pour le réseau backend
Audit et monitoring§
# Docker Bench for Security — audit CIS Docker Benchmark
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-v /etc:/etc:ro -v /lib/systemd:/lib/systemd:ro \
-v /usr/bin/containerd:/usr/bin/containerd:ro \
-v /usr/bin/runc:/usr/bin/runc:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /var/lib:/var/lib:ro -v /var/run/docker.sock:/var/run/docker.sock:ro \
docker/docker-bench-security
# Falco — détection comportementale en temps réel
# Règle Falco : détecter un shell dans un container
# (un container de prod ne devrait jamais ouvrir un shell interactif)
- rule: Terminal Shell in Container
desc: A shell was used as the entrypoint/exec point into a container
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
output: >
A shell was spawned in a container with an attached terminal
(user=%user.name container=%container.name image=%container.image.repository)
priority: NOTICE
Checklist sécurité Docker§
| Domaine | Vérification |
|---|---|
| Image | Image de base minimale, paquets à jour, pas de credentials |
| Utilisateur | Processus non-root (USER != root) |
| Filesystem | --read-only, tmpfs pour /tmp |
| Capabilities | --cap-drop ALL, add uniquement le strict nécessaire |
| Réseau | Réseaux dédiés, pas de --network host |
| Ressources | Limites mémoire/CPU/pids |
| Secrets | Pas d’env vars, utiliser secrets ou volumes |
| Image registry | Images signées (Cosign), registre privé authentifié |
| Scan | Trivy ou Grype dans le pipeline CI |
| Runtime | Falco pour la détection comportementale |
—The Gardener