Scan d'images conteneur¶
Détecter les vulnérabilités cachees dans vos images Docker et Podman avant qu'elles n'atteignent la production.
Pourquoi scanner les images¶
Votre code peut être parfaitement propre — linters au vert, zero CVE dans vos dépendances — mais l'image conteneur qui le porte embarque bien plus que votre application :
- Un système d'exploitation complet (ou minimal)
- Des bibliotheques système (glibc, openssl, zlib...)
- Des binaires utilitaires (curl, wget, bash...)
- Des gestionnaires de paquets et leurs caches
Chaque couche (layer) de l'image est une surface d'attaque potentielle. Un package système obsolète peut contenir une faille d'elevation de privileges. Une version d'OpenSSL non patchee peut permettre l'interception de trafic chiffre.
Le faux sentiment de sécurité
Une image ubuntu:20.04 non mise à jour peut contenir plus de 200 CVE connues, dont plusieurs critiques. Votre code a beau être impeccable, l'image qui le porte est un vecteur d'attaque majeur. Scanner les dépendances applicatives ne suffit pas — il faut scanner l'image entière.
Trivy image mode¶
Au chapitre 04, nous avons utilisé Trivy en mode filesystem pour scanner les dépendances applicatives (pip, npm, etc.). Ici, nous passons en mode image : Trivy inspecte les couches OCI de l'image construite, y compris les packages système installes par le gestionnaire de paquets de l'OS (apt, rpm, apk).
Préparation de la base offline¶
Avant de scanner, Trivy doit télécharger sa base de vulnérabilités. En environnement restreint (CI sans accès Internet, air-gapped), preparez le cache en amont :
# Telecharger la base de donnees uniquement
trivy image --download-db-only
# Le cache se trouve dans ~/.cache/trivy/
# Copiez ce repertoire dans votre environnement CI
cp -r ~/.cache/trivy/ /chemin/ci/cache/trivy/
Scan basique¶
Trivy détecté automatiquement l'OS de l'image, identifie le gestionnaire de paquets, et croise chaque package installe avec sa base de CVE.
Options utiles¶
# Filtrer par severite
trivy image --severity HIGH,CRITICAL mon-app:latest
# Ignorer les CVE sans correctif disponible
trivy image --ignore-unfixed mon-app:latest
# Combiner les deux
trivy image --severity HIGH,CRITICAL --ignore-unfixed mon-app:latest
# Export JSON pour traitement automatise
trivy image --format json --output rapport.json mon-app:latest
# Export SARIF pour integration GitHub/Gitea
trivy image --format sarif --output rapport.sarif mon-app:latest
JSON vs SARIF
Le format JSON est idéal pour le scripting et le traitement programmatique. Le format SARIF (Static Analysis Results Interchange Format) est le standard pour l'intégration avec GitHub Security, Gitea, et les plateformes de code review.
Lecture du rapport¶
Trivy classe les vulnérabilités en deux catégories :
Packages OS (apt/rpm/apk)¶
Ce sont les paquets installes par le gestionnaire de paquets de l'image de base. Vous n'avez généralement pas de contrôle direct sur eux — la remédiation passe par la mise à jour de l'image de base ou le choix d'une image plus légère.
Packages applicatifs (pip/npm/gem)¶
Ce sont vos dépendances applicatives, installees par votre gestionnaire de paquets. La remédiation passe par la mise à jour de vos fichiers requirements.txt, package-lock.json, etc.
Structure d'une vulnérabilité¶
Pour chaque finding, Trivy fournit :
| Champ | Description |
|---|---|
| Library | Le package concerne (ex: libssl3) |
| Installed Version | La version présenté dans l'image |
| Fixed Version | La version qui corrige la faille |
| CVE ID | L'identifiant unique (ex: CVE-2024-5535) |
| Severity | Le niveau de gravite (CRITICAL, HIGH, MEDIUM, LOW) |
Exemple de rapport Trivy
mon-app:latest (ubuntu 22.04)
==============================
Total: 14 (HIGH: 9, CRITICAL: 5)
┌──────────────┬────────────────┬──────────┬────────────────┬────────────────┬──────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed │ Fixed │ Title │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ libssl3 │ CVE-2024-5535 │ CRITICAL │ 3.0.2-0ubuntu1 │ 3.0.2-0ubuntu2 │ OpenSSL: buffer overread in │
│ │ │ │ │ │ SSL_select_next_proto │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ libcurl4 │ CVE-2024-2398 │ HIGH │ 7.81.0-1 │ 7.81.0-1.1 │ curl: HTTP/2 push headers │
│ │ │ │ │ │ memory leak │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ zlib1g │ CVE-2023-45853 │ CRITICAL │ 1.2.11-4 │ 1.2.11-4.1 │ zlib: integer overflow in │
│ │ │ │ │ │ MiniZip zipOpenNewFileInZip4 │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ libc6 │ CVE-2024-2961 │ HIGH │ 2.35-0ubuntu3 │ 2.35-0ubuntu3.8│ glibc: iconv buffer overflow │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ libgnutls30 │ CVE-2024-0553 │ HIGH │ 3.7.3-4ubuntu1 │ 3.7.3-4ubuntu2 │ GnuTLS: timing side-channel │
│ │ │ │ │ │ in RSA-PSK key exchange │
└──────────────┴────────────────┴──────────┴────────────────┴────────────────┴──────────────────────────────────┘
Python (pip)
============
Total: 3 (HIGH: 2, CRITICAL: 1)
┌──────────────┬────────────────┬──────────┬────────────────┬────────────────┬──────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed │ Fixed │ Title │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ requests │ CVE-2024-35195 │ HIGH │ 2.28.0 │ 2.32.0 │ requests: session fixation │
│ │ │ │ │ │ via verify parameter │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ cryptography │ CVE-2024-26130 │ CRITICAL │ 41.0.0 │ 42.0.4 │ cryptography: NULL pointer │
│ │ │ │ │ │ deref in PKCS12 parsing │
├──────────────┼────────────────┼──────────┼────────────────┼────────────────┼──────────────────────────────────┤
│ jinja2 │ CVE-2024-34064 │ HIGH │ 3.1.2 │ 3.1.4 │ Jinja2: XSS via xmlattr filter │
└──────────────┴────────────────┴──────────┴────────────────┴────────────────┴──────────────────────────────────┘
Stratégies de remédiation¶
Choisir la bonne image de base¶
Le choix de l'image de base est la décision la plus impactante pour la sécurité de vos conteneurs. Moins de packages installes = moins de surface d'attaque.
| Image | Taille | Packages | Surface d'attaque |
|---|---|---|---|
| ubuntu:24.04 | ~78 MB | ~100 | Élevée |
| python:3.12 | ~1 GB | ~400 | Très élevée |
| python:3.12-slim | ~150 MB | ~100 | Moyenne |
| python:3.12-alpine | ~50 MB | ~30 | Faible |
| gcr.io/distroless/python3 | ~30 MB | ~10 | Très faible |
Distroless : le minimum vital
Les images distroless de Google ne contiennent ni shell, ni gestionnaire de paquets, ni utilitaires. Elles sont extremement securisees mais compliquent le debug en production. C'est un compromis à évaluer selon votre contexte.
Multi-stage build¶
Le pattern multi-stage permet de séparer l'environnement de build (complet, avec compilateurs et outils) de l'environnement de runtime (minimal, avec uniquement les artefacts nécessaires).
# Stage build
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt
COPY . .
# Stage runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/deps /app/deps
COPY --from=builder /app/src /app/src
ENV PYTHONPATH=/app/deps
CMD ["python", "src/main.py"]
L'image finale ne contient que python:3.12-slim + vos dépendances compilees + votre code source. Tous les outils de build (gcc, make, pip) restent dans le stage builder et ne sont jamais embarqués dans l'image de production.
Rebuild régulier¶
Les images doivent être reconstruites régulièrement pour intégrer les correctifs de sécurité des images de base :
# Forcer le pull de l'image de base la plus recente
# et reconstruire sans cache
podman build --pull --no-cache -t mon-app:latest .
Les images ne se patchent pas toutes seules
Une image construite il y a 3 mois utilisé les packages système d'il y a 3 mois. Même si l'image de base a été mise à jour depuis, votre image locale reste figee. Planifiez des rebuilds hebdomadaires au minimum.
Bonnes pratiques¶
Scanner avant de pousser¶
Integrez le scan dans votre workflow avant le push vers le registry :
# Build + scan + push
podman build -t mon-app:latest .
trivy image --severity HIGH,CRITICAL --exit-code 1 mon-app:latest
# Si trivy retourne 0 (pas de CRITICAL/HIGH), on pousse
podman push mon-app:latest registry.example.com/mon-app:latest
Politique de seuil¶
Utilisez --exit-code 1 pour faire échouer le pipeline CI si des vulnérabilités critiques sont détectées :
# Bloquer sur les CRITICAL uniquement
trivy image --exit-code 1 --severity CRITICAL mon-app:latest
# Bloquer sur HIGH et CRITICAL
trivy image --exit-code 1 --severity HIGH,CRITICAL mon-app:latest
Fichier .trivyignore¶
Certaines CVE peuvent être des faux positifs ou concerner des packages non utilisés a runtime. Documentez obligatoirement chaque exclusion :
Pas de .trivyignore sans justification
Chaque ligne du .trivyignore doit être accompagnee d'un commentaire expliquant pourquoi la CVE est ignoree. Un .trivyignore sans justification est une bombe a retardement. Revoyez ce fichier à chaque sprint.
Demo complète¶
Voici un scenario complet : partir d'une image vulnerable, la scanner, appliquer les remediations, et vérifier l'amélioration.
Étape 1 : Image vulnerable¶
Creons un Dockerfile base sur une vieille image Python :
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "src/main.py"]
Étape 2 : Premier scan¶
Résultat typique :
187 vulnérabilités
L'image python:3.9 est basée sur Debian Bullseye, qui n'est plus supportee. Les packages système ne reçoivent plus de correctifs de sécurité. Chaque jour qui passe ajoute des CVE sans correctif.
Étape 3 : Remédiation¶
On applique deux stratégies simultanément :
- Upgrade de l'image de base :
python:3.9→python:3.12-slim - Multi-stage build : séparer build et runtime
# Stage build
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt
COPY . .
# Stage runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/deps /app/deps
COPY --from=builder /app/src /app/src
ENV PYTHONPATH=/app/deps
CMD ["python", "src/main.py"]
Étape 4 : Re-scan¶
Résultat après remédiation :
De 187 a 3 vulnérabilités
En changeant simplement l'image de base et en adoptant le multi-stage build, on passe de 187 a 3 vulnérabilités HIGH/CRITICAL. Les 3 restantes sont des CVE sans correctif disponible (--ignore-unfixed les masquerait).
Comparaison¶
| Métrique | python:3.9 | python:3.12-slim multi-stage |
|---|---|---|
| Taille de l'image | ~1.2 GB | ~180 MB |
| CVE CRITICAL | 45 | 0 |
| CVE HIGH | 142 | 3 |
| Packages système | ~450 | ~95 |
La réduction de la surface d'attaque est drastique, pour un effort de remédiation minimal.
À retenir
Scanner le code ne suffit pas. L'image conteneur est le veritable livrable en production — c'est elle qu'il faut sécuriser. Choisissez des images de base minimales, adoptez le multi-stage build, et scannez systematiquement avant chaque push vers le registry.