---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 13. Hardening Docker et sécurité des images

Docker a révolutionné le déploiement applicatif, mais l'adoption massive des conteneurs a élargi la surface d'attaque des infrastructures modernes. Ce chapitre explore les mécanismes d'isolation sous-jacents, les vecteurs d'attaque spécifiques aux conteneurs et les techniques de hardening applicables à chaque niveau de la pile Docker.

## Namespaces et cgroups : isolation et ses limites

Docker repose sur deux primitives du noyau Linux pour isoler les conteneurs : les **namespaces** et les **cgroups**.

### Les namespaces Linux

Un namespace est un mécanisme qui partitionne les ressources du noyau de sorte que chaque ensemble de processus ne voit qu'un sous-ensemble de ces ressources. Docker utilise six types de namespaces :

| Namespace | Ressource isolée | Impact sécurité |
|-----------|-----------------|-----------------|
| `pid` | Arbre des processus | Les processus du conteneur ne voient pas ceux de l'hôte |
| `net` | Interfaces réseau, ports | Réseau isolé par défaut |
| `mnt` | Points de montage | Système de fichiers indépendant |
| `uts` | Hostname, domainname | Identité réseau propre |
| `ipc` | File d'attente de messages, sémaphores | IPC cloisonné |
| `user` | UIDs/GIDs | Remappage des utilisateurs (optionnel) |

### Les cgroups (Control Groups)

Les cgroups limitent les ressources consommables (CPU, mémoire, I/O, réseau). Ils protègent contre les attaques de type **DoS par épuisement de ressources** mais n'empêchent pas les escalades de privilèges.

### Limites fondamentales de l'isolation

L'isolation Docker est **logicielle**, pas matérielle. Tous les conteneurs partagent le même noyau Linux de l'hôte. Cette architecture crée des risques structurels :

- **Vulnérabilités du noyau** : une faille dans le kernel (ex. CVE-2022-0185 dans `fs/legacy_fs.c`) peut permettre une évasion de conteneur.
- **Partage des appels système** : un conteneur peut appeler n'importe quel syscall non filtré par seccomp.
- **Absence d'isolation matérielle** : contrairement aux VMs (VMware, KVM), Docker ne virtualise pas le matériel.

```{admonition} Distinction conteneur vs machine virtuelle
:class: important
Une VM exécute son propre noyau sur un hyperviseur. Un conteneur partage le noyau de l'hôte. En cas de compromission du noyau, **tous les conteneurs de l'hôte sont exposés**. Les déploiements haute sécurité utilisent Kata Containers ou gVisor pour ajouter une isolation noyau.
```

## Surfaces d'attaque Docker

### Supply chain des images

Les images Docker sont construites par couches, souvent en héritant d'images publiques. Un attaquant peut :

1. **Publier une image malveillante** sur Docker Hub avec un nom proche d'une image légitime (typosquatting).
2. **Compromettre un registre privé** pour substituer des images.
3. **Injecter du code dans un Dockerfile** lors d'un build CI/CD compromis.
4. **Exploiter des dépendances obsolètes** dans des images non maintenues.

### Évasion de conteneur (Container Escape)

Les techniques d'évasion les plus documentées :

- **`--privileged` flag** : donne accès à tous les devices et capabilities de l'hôte. Un attaquant peut monter le filesystem hôte et modifier `/etc/cron.d`.
- **Socket Docker exposé** (`/var/run/docker.sock`) : donne un contrôle total sur le démon Docker, équivalent à `root` sur l'hôte.
- **Montages sensibles** : monter `/` ou `/etc` de l'hôte dans le conteneur.
- **Capabilities dangereuses** : `CAP_SYS_ADMIN`, `CAP_NET_ADMIN`, `CAP_SYS_PTRACE`.

### Risques liés au socket Docker

```{admonition} Le socket Docker est une backdoor root
:class: warning
Monter `/var/run/docker.sock` dans un conteneur revient à donner les privilèges root sur l'hôte. N'importe quel conteneur avec accès au socket peut lancer un conteneur `--privileged` avec le filesystem hôte monté.
```

## Hardening runtime

### Principe 1 : Utilisateur non-root

Par défaut, les processus dans un conteneur s'exécutent en `root` (UID 0). La directive `USER` dans le Dockerfile y remédie :

```dockerfile
# Mauvaise pratique — utilisateur root implicite
FROM node:20
COPY app/ /app
CMD ["node", "/app/server.js"]

# Bonne pratique — utilisateur dédié
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup app/ /app
USER appuser
CMD ["node", "/app/server.js"]
```

### Principe 2 : Capabilities Linux

Linux divise les privilèges root en unités discrètes appelées **capabilities**. La stratégie recommandée est `drop ALL, add only needed` :

```dockerfile
# Dans docker-compose.yml ou docker run
cap_drop:
  - ALL
cap_add:
  - NET_BIND_SERVICE   # Autoriser les ports < 1024
  - CHOWN              # Changer les propriétaires de fichiers
```

### Principe 3 : Profils seccomp

Seccomp (Secure Computing Mode) filtre les appels système autorisés. Docker applique un profil par défaut bloquant ~44 syscalls dangereux. Un profil personnalisé peut être plus restrictif :

```json
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["read", "write", "open", "close", "stat", "fstat",
                "mmap", "mprotect", "munmap", "brk", "rt_sigaction",
                "rt_sigprocmask", "exit", "futex", "clone", "execve"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
```

### Principe 4 : Profil AppArmor

AppArmor confine les programmes en définissant quelles ressources ils peuvent accéder :

```
#include <tunables/global>

profile docker-app flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  network inet tcp,
  network inet udp,

  deny @{PROC}/sys/kernel/shm* wklx,
  deny mount,
  deny /sys/** wklx,

  /app/** r,
  /tmp/** rw,
}
```

## Images sécurisées : multi-stage builds et distroless

### Multi-stage builds

Les multi-stage builds séparent l'environnement de compilation de l'environnement d'exécution, réduisant drastiquement la surface d'attaque :

```dockerfile
# Stage 1 : Build (contient compilateur, outils de dev)
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/app ./cmd/app

# Stage 2 : Runtime (image minimale, pas de shell, pas de compilateur)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /bin/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
```

### Images distroless et scratch

**Distroless** (Google) : images sans gestionnaire de paquets, sans shell, sans outils de débogage. Contient uniquement les bibliothèques d'exécution nécessaires.

**scratch** : image vide absolue, utilisée pour les binaires statiques Go ou Rust.

```{admonition} Avantages des images minimales
:class: tip
Moins de paquets = moins de CVE potentielles. Une image `scratch` avec un binaire Go statique n'a littéralement aucune dépendance système pouvant être vulnérable. En contrepartie, le débogage en production devient plus difficile — prévoyez des outils de diagnostic externes (sidecar containers, ephemeral debug containers).
```

## Scan de vulnérabilités : Trivy et Grype

### Trivy (Aqua Security)

Trivy est un scanner de vulnérabilités polyvalent couvrant images, systèmes de fichiers, dépôts Git et configurations IaC :

```bash
# Scanner une image locale
trivy image python:3.12-slim

# Résultat JSON pour intégration CI
trivy image --format json --output results.json myapp:latest

# Scanner uniquement les CVE critiques et élevées
trivy image --severity HIGH,CRITICAL myapp:latest

# Générer un SBOM
trivy image --format spdx-json --output sbom.json myapp:latest
```

### Grype (Anchore)

Grype se concentre sur la détection de vulnérabilités dans les SBOM et images :

```bash
# Analyser une image
grype myapp:latest

# Analyser un SBOM existant
grype sbom:./sbom.json

# Politique : échouer si CVSS >= 7.0
grype myapp:latest --fail-on high
```

### Intégration CI/CD

```yaml
# GitHub Actions — scan de sécurité intégré
- name: Scan image avec Trivy
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    format: sarif
    output: trivy-results.sarif
    severity: HIGH,CRITICAL
    exit-code: 1  # Bloquer le pipeline si vulnérabilité trouvée
```

## Rootless Docker et Podman

### Docker rootless

Docker rootless exécute le démon Docker sans privilèges root, limitant l'impact d'une compromission :

```bash
# Installation Docker rootless
dockerd-rootless-setuptool.sh install

# Utilisation identique
docker run --rm hello-world
```

### Podman : sans démon

Podman (Red Hat) est une alternative à Docker qui ne nécessite pas de démon centralisé. Chaque commande `podman` est un processus distinct s'exécutant sous l'utilisateur courant :

```bash
# Aucun démon requis
podman run --rm -it python:3.12 python3

# Compatible avec les Dockerfiles existants
podman build -t myapp .

# Pods natifs (sans Kubernetes)
podman pod create --name mypod
```

### BuildKit et secrets

BuildKit (backend de build Docker moderne) permet de passer des secrets sans les stocker dans les couches de l'image :

```dockerfile
# Utilisation de secrets BuildKit — le secret n'est jamais dans l'image
FROM python:3.12-slim
RUN --mount=type=secret,id=pip_token \
    PIP_INDEX_URL=$(cat /run/secrets/pip_token) pip install mypackage
```

## Docker Content Trust et signatures Cosign

### Docker Content Trust (DCT)

DCT utilise The Update Framework (TUF) pour signer et vérifier les images :

```bash
# Activer la vérification des signatures
export DOCKER_CONTENT_TRUST=1
docker pull myregistry.io/myapp:latest  # Échoue si non signé

# Signer une image
docker trust sign myregistry.io/myapp:v1.0.0
```

### Cosign (Sigstore)

Cosign permet la signature sans infrastructure de clés traditionnelle grâce à la signature keyless via OIDC :

```bash
# Signature keyless (utilise l'identité GitHub Actions OIDC)
cosign sign --yes ghcr.io/myorg/myapp:v1.0.0

# Vérification
cosign verify \
  --certificate-identity="https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/heads/main" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp:v1.0.0
```

```{admonition} Transparence des signatures avec Rekor
:class: note
Cosign enregistre automatiquement les signatures dans **Rekor**, un journal de transparence immuable (similaire aux Certificate Transparency logs pour TLS). Cela permet d'auditer rétrospectivement toutes les signatures d'une image.
```

## Visualisations

```{code-cell} python3
:tags: [hide-input]
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
```

### Comparaison des tailles d'images

```{code-cell} python3
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

images = [
    "ubuntu:22.04\n(full)",
    "python:3.12\n(standard)",
    "python:3.12-slim",
    "python:3.12-alpine",
    "distroless\n(python3)",
    "scratch\n(binaire Go)"
]
tailles_mo = [77, 1020, 130, 52, 55, 8]

couleurs = ["#e74c3c", "#e67e22", "#f1c40f", "#2ecc71", "#27ae60", "#1abc9c"]

fig, ax = plt.subplots(figsize=(10, 5))

bars = ax.barh(images, tailles_mo, color=couleurs, edgecolor="white", linewidth=0.8)

for bar, val in zip(bars, tailles_mo):
    ax.text(bar.get_width() + 10, bar.get_y() + bar.get_height() / 2,
            f"{val} Mo", va="center", fontsize=10, color="#2c3e50")

ax.set_xlabel("Taille compressée (Mo)", fontsize=11)
ax.set_title("Taille des images Docker selon la base choisie\n(données synthétiques représentatives)", fontsize=13)
ax.set_xlim(0, 1200)
ax.invert_yaxis()

sns.despine(left=True, bottom=False)
plt.savefig("docker_image_sizes.png", dpi=100, bbox_inches="tight")
plt.show()
```

### Heatmap des risques Docker

```{code-cell} python3
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

vecteurs = ["Supply chain\n(image malveillante)", "Runtime escape\n(--privileged)", "Socket Docker\nexposé", "Réseau\n(exposition ports)", "Secrets dans\nl'image"]
dimensions = ["Probabilité\n(1-5)", "Impact\n(1-5)", "Score risque\n(P×I)"]

data_prob  = [4, 3, 2, 3, 4]
data_impact = [4, 5, 5, 3, 4]
data_score  = [p * i for p, i in zip(data_prob, data_impact)]

matrix = np.array([data_prob, data_impact, data_score], dtype=float)
df_heat = pd.DataFrame(matrix, index=dimensions, columns=vecteurs)

fig, ax = plt.subplots(figsize=(11, 4))

sns.heatmap(df_heat, annot=True, fmt=".0f", cmap="YlOrRd",
            linewidths=0.5, linecolor="white",
            vmin=1, vmax=25, ax=ax,
            cbar_kws={"label": "Score"})

ax.set_title("Heatmap des risques Docker par vecteur d'attaque", fontsize=13, pad=15)
ax.set_xticklabels(ax.get_xticklabels(), rotation=0, ha="center")
ax.set_yticklabels(ax.get_yticklabels(), rotation=0)

plt.savefig("docker_risk_heatmap.png", dpi=100, bbox_inches="tight")
plt.show()
```

### Scoring sécurité d'un Dockerfile

```{code-cell} python3
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

regles = {
    "Utilisateur non-root (USER)": (True, 2),
    "Multi-stage build": (True, 2),
    "COPY au lieu de ADD": (True, 1),
    "Pas de secrets en ARG/ENV": (False, 3),
    "Image de base minimale": (True, 2),
    "Version épinglée (tag digest)": (False, 2),
    "Healthcheck défini": (True, 1),
    ".dockerignore présent": (True, 1),
    "Pas de curl|bash pipe install": (True, 2),
    "Pas de --privileged": (True, 3),
}

noms = list(regles.keys())
passes = [v[0] for v in regles.values()]
poids = [v[1] for v in regles.values()]

score_obtenu = sum(p for ok, p in zip(passes, poids) if ok)
score_max = sum(poids)
pourcentage = score_obtenu / score_max * 100

couleurs_barres = ["#2ecc71" if ok else "#e74c3c" for ok in passes]
labels_barres = ["✓" if ok else "✗" for ok in passes]

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5), gridspec_kw={"width_ratios": [3, 1]})

bars = ax1.barh(noms, poids, color=couleurs_barres, edgecolor="white", linewidth=0.8)
for bar, label in zip(bars, labels_barres):
    ax1.text(bar.get_width() + 0.05, bar.get_y() + bar.get_height() / 2,
             label, va="center", fontsize=13, fontweight="bold",
             color="#2c3e50")

ax1.set_xlabel("Poids de la règle", fontsize=10)
ax1.set_title("Règles de sécurité Dockerfile", fontsize=12)
ax1.set_xlim(0, 4.5)
ax1.invert_yaxis()
sns.despine(ax=ax1, left=True)

niveau = "Bon" if pourcentage >= 75 else ("Moyen" if pourcentage >= 50 else "Insuffisant")
couleur_score = "#2ecc71" if pourcentage >= 75 else ("#f39c12" if pourcentage >= 50 else "#e74c3c")

ax2.pie([score_obtenu, score_max - score_obtenu],
        colors=[couleur_score, "#ecf0f1"],
        startangle=90,
        wedgeprops={"width": 0.4, "edgecolor": "white"})
ax2.text(0, 0, f"{pourcentage:.0f}%", ha="center", va="center",
         fontsize=20, fontweight="bold", color=couleur_score)
ax2.text(0, -0.6, f"Niveau : {niveau}", ha="center", va="center",
         fontsize=11, color="#2c3e50")
ax2.set_title(f"Score global\n({score_obtenu}/{score_max} pts)", fontsize=12)

plt.suptitle("Analyse de sécurité d'un Dockerfile exemple", fontsize=13, y=1.02)
plt.savefig("dockerfile_security_score.png", dpi=100, bbox_inches="tight")
plt.show()

print(f"\nRécapitulatif : {score_obtenu}/{score_max} points ({pourcentage:.0f}%) — Niveau : {niveau}")
```

## Résumé

1. **Isolation partielle** : Docker repose sur les namespaces et cgroups Linux, mais partage le noyau hôte. Une vulnérabilité noyau compromet tous les conteneurs.

2. **Surfaces d'attaque majeures** : supply chain des images (images malveillantes, typosquatting), évasion de conteneur via `--privileged` ou socket Docker exposé, secrets dans les couches d'image.

3. **Hardening runtime** : exécuter en utilisateur non-root (`USER` dans Dockerfile), appliquer la politique `drop ALL + add minimal` pour les capabilities Linux, utiliser des profils seccomp et AppArmor personnalisés.

4. **Images minimales** : les multi-stage builds séparent compilation et exécution ; les images distroless et scratch réduisent la surface d'attaque à son minimum absolu.

5. **Scan de vulnérabilités** : Trivy et Grype s'intègrent dans les pipelines CI/CD pour bloquer les images avec des CVE critiques avant le déploiement.

6. **Alternatives sécurisées** : Docker rootless et Podman (sans démon) limitent l'impact d'une compromission. BuildKit permet de passer des secrets sans les persister dans les couches.

7. **Chaîne de confiance** : Docker Content Trust et Cosign (Sigstore) permettent de signer et vérifier l'intégrité des images. Rekor assure la transparence immuable des signatures.
