---
title: "Hardening Docker & Conteneurs"
domain: security
subdomain: hardening
type: snippet
tags: [hardening, docker, containers, security, rootless, capabilities, seccomp, apparmor, image-scanning]
difficulty: intermediate
status: stable
updated: "2025-05-14"
---
## Images sécurisées

```bash
# Scanner les vulnérabilités d'une image
docker scout cves {{IMAGE}}:{{TAG}}
trivy image {{IMAGE}}:{{TAG}}
grype {{IMAGE}}:{{TAG}}

# Scanner avec un seuil de sévérité (CI/CD — fail si CRITICAL)
trivy image --exit-code 1 --severity CRITICAL {{IMAGE}}:{{TAG}}

# Inspecter le contenu d'une image
docker sbom {{IMAGE}}:{{TAG}}       # Software Bill of Materials
syft {{IMAGE}}:{{TAG}}              # SBOM alternatif
dive {{IMAGE}}:{{TAG}}              # Analyser les layers

# Mettre à jour les images
docker pull {{IMAGE}}:{{TAG}}       # Récupérer la dernière version
# Puis rebuilder les conteneurs qui en dépendent
```

```dockerfile
# Dockerfile sécurisé — bonnes pratiques
FROM alpine:3.20 AS builder
# ... build steps ...

# Stage final minimal
FROM gcr.io/distroless/static-debian12
# Ou : FROM scratch (pour des binaires statiques Go/Rust)

# Copier uniquement les artefacts nécessaires
COPY --from=builder /app/binary /app/binary

# Utilisateur non-root dédié
RUN addgroup -g 10001 appgroup && adduser -u 10001 -G appgroup -s /bin/sh -D appuser
# Pour distroless : USER avec UID directement
USER 10001:10001

# Pas de secrets dans les ARG/ENV
# ARG API_KEY  ← dangereux, visible dans docker history
# Utiliser --secret ou des volumes au runtime

EXPOSE 8080
ENTRYPOINT ["/app/binary"]
```

<Warning>
Ne jamais mettre de credentials, tokens ou clés dans un Dockerfile — même supprimés dans un layer ultérieur, ils restent visibles via `docker history --no-trunc IMAGE`. Utiliser BuildKit secrets : `RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret`.
</Warning>

## Exécution rootless

```bash
# Installer Docker rootless (daemon sans root)
dockerd-rootless-setuptool.sh install

# Vérifier que le daemon tourne en rootless
systemctl --user status docker
docker info | grep -i rootless

# Podman — rootless nativement (drop-in replacement Docker)
podman run --rm {{IMAGE}} id
# uid=1000(user) gid=1000(user) groups=1000(user)  ← pas de root

# Forcer un utilisateur non-root à l'exécution
docker run --user 1000:1000 {{IMAGE}} id

# Vérifier l'utilisateur d'un conteneur en cours
docker inspect {{CONTAINER}} --format='{{.Config.User}}'
```

## Capabilities Linux

```bash
# Principe : drop ALL, ajouter uniquement ce qui est nécessaire
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE {{IMAGE}}

# Capabilities couramment nécessaires (et seulement elles)
# NET_BIND_SERVICE : écouter sur les ports < 1024
# CHOWN           : changer le propriétaire de fichiers
# SETUID, SETGID  : changer l'UID/GID du processus
# DAC_OVERRIDE    : bypass des permissions sur les fichiers (à éviter)

# Capabilities dangereuses — ne JAMAIS accorder en production
# CAP_SYS_ADMIN   : quasi-root, permet mount, namespace, etc.
# CAP_NET_ADMIN   : modifier la config réseau de l'hôte
# CAP_SYS_PTRACE  : attacher à des processus (débogage)
# CAP_DAC_OVERRIDE: bypass complet des ACLs fichiers
# CAP_SYS_MODULE  : charger des modules kernel

# Lister les capabilities d'un conteneur en cours
docker inspect {{CONTAINER}} --format='{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}'

# Vérifier les capabilities depuis l'intérieur du conteneur
capsh --print
cat /proc/1/status | grep Cap
```

<Danger>
`docker run --privileged` donne au conteneur un accès complet à l'hôte : tous les devices, toutes les capabilities, bypass des namespaces. Un conteneur privileged compromis = hôte compromis. Ne jamais utiliser en production, même temporairement.
</Danger>

## Seccomp

```bash
# Le profil seccomp par défaut de Docker bloque ~44 syscalls dangereux
# Vérifier le profil actif d'un conteneur
docker inspect --format='{{.HostConfig.SecurityOpt}}' {{CONTAINER}}

# Appliquer un profil seccomp restrictif custom
docker run --security-opt seccomp=/path/to/profile.json {{IMAGE}}

# Désactiver seccomp (à éviter — uniquement pour debug)
docker run --security-opt seccomp=unconfined {{IMAGE}}
```

```json
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["read", "write", "close", "fstat", "mmap", "mprotect",
                "munmap", "brk", "rt_sigaction", "rt_sigprocmask",
                "exit", "exit_group", "futex", "nanosleep",
                "clock_gettime", "gettimeofday", "getpid", "getuid",
                "getgid", "geteuid", "getegid", "socket", "connect",
                "sendto", "recvfrom", "sendmsg", "recvmsg", "bind",
                "listen", "accept", "accept4", "epoll_create",
                "epoll_create1", "epoll_ctl", "epoll_wait",
                "epoll_pwait", "openat", "open", "stat", "lstat",
                "access", "ioctl", "fcntl", "dup", "dup2", "pipe"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
```

## AppArmor / SELinux

```bash
# AppArmor — profil par défaut Docker
docker run --security-opt apparmor=docker-default {{IMAGE}}

# Appliquer un profil AppArmor custom
# 1. Créer le profil
cat > /etc/apparmor.d/docker-custom << 'EOF'
#include <tunables/global>
profile docker-custom flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  network,
  capability,
  file,
  umount,
  deny @{PROC}/* w,
  deny /sys/[^f]*/** wklx,
  deny /sys/f[^s]*/** wklx,
  deny /sys/fs/[^c]*/** wklx,
  deny /sys/fs/c[^g]*/** wklx,
  deny /sys/fs/cg[^r]*/** wklx,
  deny /sys/firmware/** rwklx,
  deny /sys/kernel/security/** rwklx,
}
EOF
# 2. Charger le profil
apparmor_parser -r -W /etc/apparmor.d/docker-custom
# 3. Appliquer
docker run --security-opt apparmor=docker-custom {{IMAGE}}

# SELinux — label de type conteneur
docker run --security-opt label=type:container_t {{IMAGE}}

# Vérifier les labels SELinux
docker inspect {{CONTAINER}} --format='{{.HostConfig.SecurityOpt}}'
```

## Réseau

```bash
# Pas de réseau si inutile
docker run --network=none {{IMAGE}}

# Network Docker custom (isolation entre conteneurs)
docker network create --driver bridge --opt com.docker.network.bridge.enable_icc=false app-network
docker run --network=app-network {{IMAGE}}

# Exposer uniquement sur localhost (jamais 0.0.0.0 en prod)
docker run -p 127.0.0.1:8080:8080 {{IMAGE}}

# Inspecter le réseau d'un conteneur
docker inspect {{CONTAINER}} --format='{{json .NetworkSettings.Networks}}' | jq

# Désactiver ICC dans daemon.json (Inter-Container Communication)
# Voir section daemon.json ci-dessous
```

<Warning>
`--network=host` supprime l'isolation réseau du conteneur — il partage la stack réseau de l'hôte. Toute écoute sur un port dans le conteneur est directement exposée sur l'hôte. À éviter absolument en production.
</Warning>

## Secrets management

```bash
# Docker Secrets (Swarm mode)
echo "{{SECRET_VALUE}}" | docker secret create my-secret -
docker service create --secret my-secret {{IMAGE}}
# Disponible dans le conteneur : /run/secrets/my-secret

# BuildKit secrets (build time — ne pas persister dans les layers)
docker build --secret id=mysecret,src=./secret.txt -t {{IMAGE}} .
# Dans le Dockerfile :
# RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret

# Volume read-only pour les secrets
docker run -v /host/secrets/myapp:/run/secrets:ro {{IMAGE}}

# HashiCorp Vault Agent sidecar
# Vault Agent s'authentifie auprès de Vault et écrit les secrets dans un volume partagé
```

<Danger>
Ne jamais passer de secrets via variables d'environnement (`-e SECRET=value`) — `docker inspect` les expose en clair à quiconque peut exécuter cette commande sur l'hôte. Les variables d'environnement sont aussi visibles dans `/proc/PID/environ` depuis l'intérieur du conteneur.
</Danger>

## Filesystem et volumes

```bash
# Root filesystem en lecture seule + tmpfs pour les répertoires qui nécessitent des écritures
docker run \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --tmpfs /var/run:rw,noexec,nosuid,size=10m \
  {{IMAGE}}

# Empêcher l'escalade de privilèges (sudo, setuid)
docker run --no-new-privileges {{IMAGE}}

# Monter les volumes en lecture seule si possible
docker run -v /host/data:/app/data:ro {{IMAGE}}

# Vérifier les options de sécurité appliquées
docker inspect {{CONTAINER}} --format='
  ReadOnlyRootfs: {{.HostConfig.ReadonlyRootfs}}
  NoNewPrivileges: {{.HostConfig.SecurityOpt}}
  Privileged: {{.HostConfig.Privileged}}'
```

## daemon.json hardening

```json
{
  "icc": false,
  "no-new-privileges": true,
  "userns-remap": "default",
  "live-restore": true,
  "userland-proxy": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "seccomp-profile": "/etc/docker/seccomp-profile.json"
}
```

```bash
# Appliquer la configuration
# Éditer /etc/docker/daemon.json puis :
systemctl restart docker

# Vérifier la configuration active
docker info | grep -A5 "Security Options"
docker info | grep "Logging Driver"
```

## Checklist sécurité Docker

<Checklist
  storageKey="hardening-docker"
  items={[
    { id: "image-scanned", label: "Image scannée (Trivy / Docker Scout) avant déploiement, zéro CRITICAL", critical: true },
    { id: "nonroot-user", label: "Conteneur exécuté avec USER non-root (UID > 0) dans le Dockerfile", critical: true },
    { id: "cap-drop", label: "--cap-drop=ALL, capabilities ajoutées uniquement si nécessaires", critical: true },
    { id: "no-privileged", label: "Aucun conteneur --privileged en production", critical: true },
    { id: "no-new-privileges", label: "--no-new-privileges activé (daemon.json ou docker run)" },
    { id: "readonly-fs", label: "--read-only sur le root filesystem, --tmpfs pour /tmp" },
    { id: "network-isolated", label: "Réseau isolé (network custom, --network=none si pas nécessaire, pas de --network=host)" },
    { id: "secrets-externalized", label: "Secrets externalisés (Docker Secrets, Vault), aucun secret en variable d'environnement" },
    { id: "daemon-hardened", label: "daemon.json configuré : icc=false, no-new-privileges, userns-remap, logs limités" },
    { id: "logs-enabled", label: "Logs configurés (json-file avec rotation, ou driver centralisé)" },
    { id: "trivy-cicd", label: "Trivy ou équivalent intégré dans le pipeline CI/CD avec seuil de blocage" },
    { id: "apparmor-seccomp", label: "AppArmor ou SELinux + profil seccomp appliqués" }
  ]}
/>

<Tip>
`docker-bench-security` (CIS Docker Benchmark) est un script qui audite automatiquement la configuration Docker de l'hôte : daemon, images, conteneurs en cours, sécurité du runtime. Exécuter avec `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker/docker-bench-security` et corriger tous les `[WARN]`.
</Tip>
