Aller au contenu

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

# Scanner une image locale
trivy image mon-app:latest

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 :

# CVE-2024-XXXXX: faux positif, package non utilise a runtime
CVE-2024-XXXXX

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

podman build -t mon-app:vulnerable .
trivy image --severity HIGH,CRITICAL mon-app:vulnerable

Résultat typique :

mon-app:vulnerable (debian 11)
===============================
Total: 187 (HIGH: 142, CRITICAL: 45)

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 :

  1. Upgrade de l'image de base : python:3.9python:3.12-slim
  2. 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

podman build -t mon-app:secure .
trivy image --severity HIGH,CRITICAL mon-app:secure

Résultat après remédiation :

mon-app:secure (debian 12)
===========================
Total: 3 (HIGH: 3, CRITICAL: 0)

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.