Vous avez lancé un scan de conteneur et il a hurlé : « 327 vulnérabilités, 14 critiques. »
Pourtant le service est un petit binaire Go, sans shell, sans gestionnaire de paquets, et il n’ouvre même pas de port entrant.
Le scanner panique quand même. Votre équipe sécurité veut un ticket. L’astreinte veut dormir.
Le scan de vulnérabilités pour conteneurs est utile — quand vous le traitez comme un tableau de bord d’instruments, pas comme une prophétie.
Voici comment séparer le signal du bruit, décider quoi corriger, et construire un workflow qui tient la route en production.
Ce que savent vraiment les scanners (et ce qu’ils ne savent pas)
La plupart des scanners de vulnérabilités Docker fonctionnent en inspectant le contenu de l’image et en rapprochant ce qu’ils trouvent avec des bases de données de vulnérabilités.
Ça semble simple jusqu’à ce que vous vous rappeliez ce qu’est une image de conteneur : un tas de fichiers plus des couches de métadonnées, construit dans un écosystème qui adore l’abstraction.
Les scanners ne « comprennent » pas votre application. Ils infèrent, font des correspondances et devinent.
Ce que les scanners font bien
- Détecter les versions de paquets connues vulnérables dans les gestionnaires de paquets OS (dpkg, rpm, apk) et dans les écosystèmes de langages communs.
- Signaler des images de base obsolètes quand l’amont a livré des correctifs mais que vous n’avez pas reconstruit.
- Inventorier les composants (comportement semblable à un SBOM) pour cesser les disputes sur ce qui est « dans l’image ».
- Trouver des pièges évidents : clés privées embarquées, fichiers modifiables par tous, ou un serveur SSH surprise — selon l’outil.
Où les scanners trompent souvent (ou au moins induisent en erreur)
- Accessibilité : une vulnérabilité dans une bibliothèque présente mais jamais chargée sera quand même signalée.
- Backports : les distributions entreprises patchent des vulnérabilités sans incrémenter la version comme s’y attendrait un simple rapprochement.
- Ambiguïté du « corrigé dans » : le scanner indique « corrigé dans 1.2.3 » mais votre distro livre « 1.2.2-ubuntu0.4 » avec le correctif rétroporté.
- Gravité sans contexte : le CVSS suppose un déploiement générique. Votre conteneur peut être non-root, sans réseau, système de fichiers en lecture seule, seccomp verrouillé. CVSS s’en fiche.
- Paquets fantômes : dépendances de compilation laissées dans la couche finale parce que quelqu’un a oublié les builds multi‑étapes.
Le scan de vulnérabilités est un générateur de signaux nécessaire, pas un calculateur de risque. Le calcul du risque est votre travail.
Plus précisément : votre travail est de créer un système qui calcule le risque de façon cohérente pour éviter l’improvisation à 2h du matin.
Une citation que je garde collée sur le tableau de bord mental, de John Allspaw : « L’automatisation doit être un multiplicateur de force, pas une source de mystère. »
Faits et contexte historique (pourquoi ce bazar existe)
Si le scan de vulnérabilités ressemble à une dispute avec une feuille de calcul très confiante, c’est parce que c’en est une. Un peu de contexte vous aide à prévoir les modes de défaillance.
- CVE a commencé en 1999 comme un système de nommage pour que les fournisseurs puissent parler du même bug sans traduction tribale.
- CVSS v2 (2005) et v3 (2015) ont standardisé le scoring de gravité, mais c’est toujours un modèle abstrait qui ne voit pas vos atténuations au runtime.
- Les conteneurs ont popularisé « l’infrastructure immuable », mais beaucoup d’équipes traitent encore les images comme des VM mutables : patcher à l’intérieur, livrer, oublier.
- Les distributions rétroportent des correctifs par politique (familles Debian, Ubuntu, RHEL), ce qui casse la logique simple « version < version_corrigée ».
- L’écosystème musl libc d’Alpine a changé le paysage des dépendances ; des images plus petites, mais une compatibilité différente et parfois des récits de vulnérabilités distincts.
- Heartbleed (2014) a enseigné : « mettre à jour OpenSSL » n’est pas une stratégie quand vous ne savez même pas où OpenSSL se trouve.
- Log4Shell (2021) a poussé les SBOM dans le mainstream corporate parce que les gens scannaient tout pour trouver où vivait le jar.
- Executive Order 14028 (2021) a accéléré les exigences SBOM et l’attente « prouver ce qui est à l’intérieur » dans les pipelines d’achat.
- VEX a émergé parce que des SBOM sans contexte « est-ce exploitable ? » créent de la fatigue d’alerte à l’échelle industrielle.
Fait #10, officieux mais douloureusement réel : les fournisseurs d’outils de sécurité ont découvert que « plus de résultats » vend souvent mieux que « des résultats plus précis ».
C’est une réalité de marché, pas une conspiration.
Un modèle de confiance pragmatique : trois couches de vérité
Quand vous triagez des findings, vous avez besoin d’une hiérarchie. Pas pour la philosophie — pour arrêter les débats sans fin.
Couche 1 : Ce qui est dans l’image (vérité d’inventaire)
C’est ce que les scanners savent généralement bien faire : énumérer paquets, bibliothèques et parfois fichiers.
La première question n’est pas « est-ce critique ? » La première question est « est‑ce réellement présent ? »
Signaux de confiance :
- Résultats dérivés des métadonnées du gestionnaire de paquets OS (base dpkg/rpm/apk) plutôt que du simple devinage par nom de fichier.
- Génération de SBOM avec sommes de contrôle et identifiants de paquet (purl, CPE, identifiants SPDX).
- Builds reproductibles ou au moins Dockerfiles déterministes pour confirmer que le scan concerne bien ce que vous pensez.
Couche 2 : Est‑ce vulnérable dans cette distro/écosystème (vérité d’avis)
Un CVE n’est pas une note de correction. C’est une étiquette.
« Vulnérable » dépend du statut de patch de la distro, des backports, des flags de compilation, des fonctionnalités désactivées et des décisions du fournisseur.
Signaux de confiance :
- Trackers de sécurité de la distro et avis du fournisseur référencés par le scanner, pas seulement le NVD.
- Prise en compte des rétroports : mappage par chaînes de release des paquets de la distro, pas seulement semver upstream.
- Outils qui distinguent « non corrigé », « corrigé », « affecté mais atténué » et « non affecté ».
Couche 3 : Est‑ce exploitable dans votre runtime (vérité de la réalité)
L’exploitabilité est l’endroit où le scan s’arrête et où l’exploitation opérationnelle commence :
exposition réseau, privilèges, accès en écriture au système de fichiers, secrets dans des variables d’environnement, durcissement du noyau, contrôles d’egress, et ce que fait réellement le chemin de code.
Signaux de confiance :
- Configuration runtime connue : IDs utilisateur, capacités Linux, seccomp/apparmor, rootfs en lecture seule, no-new-privileges.
- Exposition connue : quels ports sont joignables, règles d’ingress, politique de service mesh, restrictions d’egress.
- Analyse de la portée basée sur des preuves (SCA avec graphe d’appel) pour les dépendances de langage quand c’est faisable.
Si votre pipeline traite la Couche 1 comme la Couche 3, vous serez noyé d’alertes et finirez par être percé par quelque chose d’ennuyeux.
Catégories de bruit courantes (et quand ce n’est plus du bruit)
1) CVE « non corrigées » dans des paquets d’image de base que vous n’utilisez pas
Exemple : votre image inclut libxml2 comme dépendance transitive, le scanner signale des CVE, mais votre appli ne parse jamais d’XML.
C’est souvent un « risque tolérable », pas un « ignorer pour toujours ».
Quand ça devient réel :
- La bibliothèque vulnérable est utilisée par des composants système que vous exposez (nginx, curl, git, appels au gestionnaire de paquets).
- Le conteneur tourne en privilégié ou partage des namespaces hôtes.
- La bibliothèque est fréquemment utilisée via des chemins inattendus (p. ex. bibliothèques de traitement d’image via une fonctionnalité d’upload utilisateur).
2) Faux positifs dus aux backports
Debian et Ubuntu livrent fréquemment des correctifs sans incrémenter la version upstream.
Un scanner qui compare seulement les versions upstream va prétendre que vous êtes vulnérable alors que ce n’est pas le cas.
Quand ça devient réel :
- Vous êtes sur une distro sans culture de backport, ou vous utilisez des binaires upstream.
- Le fournisseur du paquet marque explicitement l’objet comme affecté et non corrigé.
3) CVE de dépendances de langage sans chemin de code joignable
JavaScript, Java, Python — des écosystèmes où un graphe de dépendances peut devenir une petite forêt de flammes.
Les scanners signalent des librairies présentes dans node_modules même si le code n’est jamais importé au runtime.
Quand ça devient réel :
- Vous déployez l’arbre de dépendances complet en production (commun dans les images Node).
- Il y a du chargement dynamique / réflexion qui rend l’analyse de portée difficile.
- Vous avez des entrées contrôlées par l’utilisateur qui pourraient atteindre des parseurs obscurs.
4) CVE qui nécessitent un accès local, dans un conteneur qui tourne en non-root
Beaucoup de CVE d’« élévation de privilèges locale » restent pertinentes si un attaquant peut exécuter du code dans votre conteneur.
S’ils ont déjà l’exécution de code, c’est déjà une mauvaise journée.
Mais l’EPL compte parce qu’il peut devenir un contournement du conteneur ou un mouvement latéral.
5) CVE du noyau signalées contre des images
Les conteneurs partagent le noyau de l’hôte. Si un scanner signale une CVE du noyau « dans l’image », considérez cela comme informatif au mieux.
Votre correctif est sur les nœuds hôtes, pas dans le Dockerfile.
Blague #1 : Les scores CVSS sont comme les prévisions météo — utiles, mais si vous planifiez une chirurgie dessus, vous aurez beaucoup de paperasse.
Un cadre de triage qui fonctionne sous pression
Voici le workflow que je veux que les équipes utilisent. Ce n’est pas parfait. C’est cohérent, et la cohérence bat l’héroïsme.
Étape A : Confirmer que vous avez scanné le bon artefact
Vous penseriez que c’est évident. Ça ne l’est pas.
Les gens scannent :latest, ou scannent un build local pendant que la production tourne sur un digest plus ancien.
Attachez toujours les findings à un digest immuable.
Étape B : Réduire à « exploitable + exposé + non atténué »
La gravité n’est pas une décision ; c’est un indice.
Votre décision devrait être guidée par :
- Surface exposée : le composant vulnérable est‑il atteignable depuis la frontière de confiance ?
- Maturité de l’exploit : exploit public ? weaponisé ? juste théorique ?
- Privilèges : peut‑il mener à root dans le conteneur ? à un escape ? à l’accès aux données ?
- Atténuations : config désactive la fonctionnalité, politiques réseau, sandboxing, WAF, rootfs en lecture seule.
- Temps pour corriger : pouvez‑vous reconstruire une image de base aujourd’hui, ou faudra‑t‑il un mois pour démêler ?
Étape C : Décider entre reconstruire, patcher ou accepter
- Reconstruire quand le correctif est dans les paquets amont et que vous avez juste besoin d’une nouvelle image de base.
- Patch quand vous contrôlez le code et que la vulnérabilité est dans votre arbre de dépendances.
- Accepter (avec trace) quand ce n’est pas exploitable, pas exposé, ou atténué — et que vous pouvez le prouver (VEX aide).
Étape D : Prévenir la récidive avec une hygiène de build
La plupart du « bruit des scanners » est auto‑infligé : images gigantesques, outils de build dans les couches runtime, images de base anciennes, gestion des dépendances sans discipline.
Corrigez le pipeline et le bruit diminue.
Tâches pratiques : commandes, sorties et décisions (12+)
Ce sont des tâches pratiques que vous pouvez exécuter sur une station de travail ou un runner CI avec Docker disponible.
Chacune inclut ce que la sortie signifie et quelle décision prendre.
Task 1: Identify the exact image digest you’re scanning
cr0x@server:~$ docker image inspect --format '{{.RepoTags}} {{.Id}}' myapp:prod
[myapp:prod] sha256:9d2c7c0a0d4e2fd0f2c8a7f0b6b1a1f2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8
Signification : L’ID d’image (digest) est immuable. Scannez celui‑ci, pas un tag qui peut bouger.
Décision : Si votre scanner rapporte des findings sans référence à un digest, considérez le rapport comme « artefact possiblement erroné » jusqu’à preuve du contraire.
Task 2: Confirm what production actually runs (digest, not tag)
cr0x@server:~$ docker ps --no-trunc --format 'table {{.Names}}\t{{.Image}}\t{{.ID}}'
NAMES IMAGE ID
myapp-prod myregistry.local/myapp@sha256:2b1d...e9c3 61c9f1c1f9d9b0a3b2c1d7b0e3a1c8f9d2e4b5a6c7d8e9f0a1b2c3d4e5f6a7b8
Signification : Le conteneur en cours d’exécution référence un digest depuis votre registre. Si le digest diffère de celui que vous avez scanné, votre scan est hors sujet.
Décision : Re‑scannez le digest en cours d’exécution, ou mettez le déploiement à jour pour correspondre au digest scanné avant d’ouvrir des tickets de remédiation.
Task 3: Quick inventory check—what OS are we dealing with?
cr0x@server:~$ docker run --rm myapp:prod cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
Signification : Le contexte de la distro importe. Debian rétroporte ? Oui, parfois. Alpine ? Base de données et cadence de patch différentes.
Décision : Configurez les scanners pour utiliser des flux spécifiques à la distro quand c’est possible ; ne comptez pas uniquement sur les données génériques « corrigé dans » upstream.
Task 4: Count packages in the runtime image (noise predictor)
cr0x@server:~$ docker run --rm myapp:prod dpkg-query -W | wc -l
143
Signification : Plus de paquets signifie généralement plus de CVE. Une image runtime avec 143 paquets peut être correcte, mais c’est un indice.
Décision : Si ce nombre est de plusieurs centaines ou milliers pour un service simple, planifiez un build multi‑étapes ou passez à distroless ; vous payez une taxe vulnérabilité pour la commodité.
Task 5: Find the biggest layers (often where build tools got baked in)
cr0x@server:~$ docker history --no-trunc myapp:prod | head
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:9d2c… 3 days ago /bin/sh -c apt-get update && apt-get install… 312MB
sha256:1aa3… 3 days ago /bin/sh -c useradd -r -u 10001 appuser 1.2MB
sha256:2bb4… 3 days ago /bin/sh -c #(nop) COPY file:app /app 24MB
Signification : Cette couche de 312MB sent les compilateurs, les headers, et tout le reste que quelqu’un a installé « temporairement ».
Décision : Séparez build et runtime ; supprimez les caches des gestionnaires de paquets ; gardez le runtime minimal pour réduire la surface CVE.
Task 6: Run Trivy scan with useful defaults (and read it correctly)
cr0x@server:~$ trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:prod
2026-01-03T10:12:41Z INFO Detected OS: debian
2026-01-03T10:12:41Z INFO Number of language-specific files: 1
myapp:prod (debian 12)
=================================
Total: 3 (HIGH: 2, CRITICAL: 1)
libssl3 3.0.11-1~deb12u2 (CRITICAL) CVE-202X-YYYY fixed in 3.0.11-1~deb12u3
zlib1g 1:1.2.13.dfsg-1 (HIGH) CVE-202X-ZZZZ fixed in 1:1.2.13.dfsg-2
Signification : --ignore-unfixed supprime les items « pas de correctif existant », ce qui réduit souvent le bruit au quotidien.
Les findings restants sont actionnables parce qu’une version corrigée existe dans le canal de la distro.
Décision : Si des versions corrigées existent, reconstruisez avec des paquets de base mis à jour. Sinon, évaluez l’exploitabilité et les contrôles compensatoires ; ne bloquez pas les releases sur les cas « non corrigés » par défaut.
Task 7: Generate an SBOM and treat it as an artifact
cr0x@server:~$ syft myapp:prod -o spdx-json > myapp.spdx.json
cr0x@server:~$ jq -r '.packages[0].name, .packages[0].versionInfo' myapp.spdx.json
base-files
12.4+deb12u5
Signification : Vous avez maintenant un inventaire lisible par machine lié à une construction d’image.
Décision : Stockez les SBOM avec les images (dans un stockage d’artefacts). Servez‑vous en pour différer les builds et répondre « qu’y a‑t‑il dedans ? » en minutes, pas en réunions.
Task 8: Correlate scanner findings with installed package status
cr0x@server:~$ docker run --rm myapp:prod dpkg-query -W libssl3
libssl3 3.0.11-1~deb12u2
Signification : Confirme ce qui est installé, pas ce que le scanner a deviné.
Décision : Si la version installée inclut déjà un correctif rétroporté (le fournisseur dit corrigé), marquez l’entrée du scanner comme faux positif et documentez‑le (préférablement en VEX).
Task 9: Check if the container runs as root (risk multiplier)
cr0x@server:~$ docker inspect --format '{{.Config.User}}' myapp:prod
Signification : Vide signifie que l’utilisateur par défaut est root. Beaucoup d’exploits passent d’« ennuyeux » à « catastrophique » quand ils obtiennent root dans le conteneur.
Décision : Si ça tourne en root, corrigez sauf raison impérieuse. Ajoutez USER 10001 (ou similaire) et gérez correctement les permissions de fichiers.
Task 10: Validate Linux capabilities at runtime
cr0x@server:~$ docker inspect --format '{{json .HostConfig.CapAdd}} {{json .HostConfig.CapDrop}}' myapp-prod
null ["ALL"]
Signification : CapDrop ["ALL"] est une bonne mesure de durcissement. Si vous voyez des capacités ajoutées (NET_ADMIN, SYS_ADMIN), le rayon d’impact augmente.
Décision : Si des capacités sont ajoutées « au cas où », retirez‑les et testez. Les capacités doivent être traitées comme des mots de passe root : émises parcimonieusement.
Task 11: Confirm read-only filesystem configuration (mitigation evidence)
cr0x@server:~$ docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' myapp-prod
true
Signification : Un rootfs en lecture seule réduit la persistance d’exploit et bloque de nombreuses chaînes d’attaque écriture‑puis‑exécution.
Décision : Si false, envisagez de le mettre à true et montez uniquement les chemins inscriptibles requis (/tmp, cache app) en tmpfs/volumes.
Task 12: Identify which ports are actually exposed/listening
cr0x@server:~$ docker exec myapp-prod ss -lntp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("myapp",pid=1,fd=7))
Signification : Un seul listener sur le port 8080. C’est votre surface d’attaque distante principale.
Décision : Si les findings du scanner ciblent des composants non impliqués dans ce chemin (p. ex. libs SSH, FTP), baissez l’urgence à moins qu’il y ait une chaîne d’exploitation interne.
Task 13: Detect “shell present” and other convenience tools (attack surface)
cr0x@server:~$ docker run --rm myapp:prod sh -lc 'command -v bash; command -v curl; command -v gcc; true'
/usr/bin/bash
/usr/bin/curl
/usr/bin/gcc
Signification : C’est beaucoup d’outils pour un conteneur runtime. Idéal pour le débogage. Idéal aussi pour les attaquants.
Décision : Déplacez les outils vers une image de debug ou un conteneur boîte à outils éphémère. Gardez les images de production fades.
Task 14: Compare two images to see if the CVE reduction is real
cr0x@server:~$ trivy image --severity HIGH,CRITICAL myapp:prod | grep -E 'Total:|CRITICAL|HIGH' | head
Total: 17 (HIGH: 13, CRITICAL: 4)
cr0x@server:~$ trivy image --severity HIGH,CRITICAL myapp:prod-slim | grep -E 'Total:|CRITICAL|HIGH' | head
Total: 4 (HIGH: 3, CRITICAL: 1)
Signification : L’image « slim » a réduit les findings de manière significative. Confirmez maintenant la fonctionnalité et les besoins opérationnels (logging, certificats CA, fichiers timezone).
Décision : Si l’image slim passe les tests d’intégration et conserve l’observabilité, promouvez‑la. Sinon, vous avez appris quelles dépendances sont réellement nécessaires.
Task 15: Detect “stale rebuild” risk (base image updates not pulled)
cr0x@server:~$ docker pull debian:12-slim
12-slim: Pulling from library/debian
Digest: sha256:7a8b...cdef
Status: Image is up to date for debian:12-slim
Signification : Vous avez confirmé que l’image de base locale correspond à l’état actuel du registre. En CI vous devez toujours puller pour éviter de builder sur des couches obsolètes.
Décision : Si le CI met en cache des images de base sans rafraîchissement, corrigez cela. La remédiation des vulnérabilités est souvent « reconstruire chaque semaine », pas « patcher manuellement dans un conteneur ».
Task 16: Verify you didn’t ship build-time secrets into layers
cr0x@server:~$ docker run --rm myapp:prod sh -lc 'grep -R --line-number -E "BEGIN (RSA|OPENSSH) PRIVATE KEY" / 2>/dev/null | head -n 3 || true'
Signification : L’absence de sortie est bonne. Les scanners trouvent parfois des secrets ; vous pouvez aussi vous auto‑vérifier.
Décision : Si vous trouvez quelque chose, traitez‑le comme un incident : révoquez les clés, reconstruisez les images, et corrigez immédiatement le Dockerfile/contexte de build.
Blague #2 : La seule chose plus persistante qu’une vulnérabilité est le cache de couche avec de mauvaises décisions intégrées.
Trois mini-récits du monde corporate
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Une société SaaS de taille moyenne exécutait des scans de conteneurs chaque nuit et avait une politique simple :
« Si c’est Critique, bloquer le déploiement. » L’intention était bonne. L’implémentation était brute.
Un vendredi, une nouvelle CVE critique est arrivée dans une bibliothèque de compression largement utilisée.
Leur scanner l’a signalée sur des dizaines d’images — API, workers, tâches cron, même un outil de migration ponctuel.
L’hypothèse : « Critique signifie exploitable depuis Internet. » La réponse sécurité a donc arrêté toutes les releases.
Pendant ce temps, le vrai problème opérationnel était ailleurs : une migration de schéma de base de données devait être déployée pour stopper une régression de performance lente.
Avec les déploiements bloqués, la régression est devenue visible pour les clients. La latence a augmenté. Les retries ont foisonné. Les coûts ont suivi.
Après un long week‑end, ils ont découvert que la bibliothèque « critique » n’était présente que dans une couche de toolchain de build pour plusieurs images, pas dans l’étape runtime.
Pour d’autres, elle était installée mais non joignable depuis leurs services exposés. La bonne correction aurait été une reconstruction planifiée et une exception ciblée avec justification.
La leçon n’était pas « ignorer les CVE critiques ». La leçon était « ne confondez pas les étiquettes de gravité avec l’exploitabilité dans votre architecture ».
Ils ont remplacé la règle de blocage par une règle de triage : bloquer seulement quand (1) une version corrigée existe, (2) le composant est dans l’image runtime, (3) joignable depuis un chemin exposé, ou (4) la CVE est connue pour être activement exploitée.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une équipe plateforme interne d’entreprise en avait assez des longs temps de build et de l’encombrement du registre.
Ils ont introduit un cache agressif des couches et ont épinglé les images de base sur des digests dans les Dockerfiles pour « assurer la reproductibilité ».
Les builds sont devenus plus rapides. Les résultats des scans sont devenus stables. Tout le monde était content.
Trois mois plus tard, une nouvelle vague d’alertes est arrivée — cette fois de la part d’auditeurs externes, pas de leur CI.
Les auditeurs ont scanné les images dans le registre et ont trouvé des paquets anciens et vulnérables qui avaient été corrigés amont depuis longtemps.
Les scanners de l’équipe plateforme ne les avaient pas signalés parce qu’ils ne reconstruisaient pas, et leur pipeline de scan ne tournait que sur les « images modifiées ».
L’optimisation avait créé un piège : épingler les images de base à des digests rendait les builds reproductibles, mais aussi permanemment périmés à moins que quelqu’un mette à jour l’épingle.
Leur cache facilitait l’oubli que les correctifs de sécurité arrivent via des reconstructions.
La remédiation n’était pas compliquée, juste opérationnellement pénible :
ils ont ajouté des reconstructions programmées (hebdomadaires pour services exposés, mensuelles pour outils internes), plus un bot PR automatisé qui met à jour les digests des images de base et lance les tests d’intégration.
Le cache est resté — mais il n’a plus servi d’excuse pour ne jamais reconstruire.
La leçon : les optimisations de performance qui réduisent la friction réduisent aussi le feedback. Si vous supprimez la « douleur » de reconstruire, ajoutez une cadence explicite de reconstruction ou votre posture de sécurité pourrira silencieusement.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une fintech avait une habitude dont personne ne se vantait : chaque image construite en CI produisait trois artefacts —
le digest de l’image, un SBOM, et une attestation signée des inputs de build (digest de l’image de base, SHA git, args de build).
Ce n’était pas excitant. C’était de la paperasse, automatisée.
Puis une vulnérabilité de haut niveau a été publiée dans une librairie utilisée par leur passerelle client.
Le tableau de bord des scanners s’est illuminé, comme les tableaux de bord savent le faire. L’équipe sécurité a posé la question habituelle : « Sommes‑nous affectés ? »
Historiquement, cette question déclenche des semaines de recherche dans les repos et des disputes sur quel service utilise quoi.
Cette fois, le SRE de garde a extrait le SBOM pour le digest de la passerelle en cours, a cherché le composant, et a eu une réponse en minutes :
oui, la librairie vulnérable était présente, et oui, la fonctionnalité affectée était activée dans leur config.
Ils ont aussi utilisé les attestations pour confirmer quels builds l’avaient introduite et quels environnements exécutaient ces digests.
Ils ont reconstruit l’image de base, déployé progressivement, et utilisé le mapping de digests pour confirmer que la prod avait bien été mise à jour.
Pas de devinettes. Pas de discussions « mais on a déployé, non ? » sur Slack. Juste une correction contrôlée.
La leçon : la différence entre panique et progrès, c’est la preuve.
Les SBOM et attestations ne sont pas du théâtre de sécurité quand ils sont intégrés aux opérations et liés à des digests immuables.
Playbook de diagnostic rapide
Quand vous avez un rapport de scanner et peu de temps, faites ceci dans l’ordre. L’objectif est d’identifier rapidement le goulet d’étranglement de la prise de décision.
1) Confirmer l’identité de l’artefact
- Avons‑nous un digest d’image ?
- La production exécute‑t‑elle ce digest ?
- Le timestamp du scan est‑il postérieur à la construction de l’image ?
Pourquoi en premier : La moitié du drame des scans vient d’artefacts non appariés. Corrigez ça et beaucoup de problèmes « urgents » disparaissent ou deviennent correctement ciblés.
2) Déterminer la disponibilité du correctif
- Y a‑t‑il une version corrigée du paquet dans le dépôt de la distro ?
- Y a‑t‑il une release upstream pour la dépendance langage ?
- Le problème est‑il marqué « non corrigé » parce qu’aucun patch n’existe ?
Pourquoi en second : « Pas de correctif » change la conversation de « patch maintenant » à « atténuer et surveiller ».
3) Vérifier l’exposition et les privilèges au runtime
- Quels ports écoutent ? Qu’est‑ce qui est joignable depuis l’extérieur ?
- Est‑ce que ça tourne en root ? Des capacités additionnelles ? Conteneur privilégié ?
- Rootfs en lecture seule ? profil seccomp ? no‑new‑privileges ?
Pourquoi en troisième : Si le runtime est verrouillé et que le composant vulnérable n’est pas exposé, vous avez le temps de reconstruire correctement plutôt que de précipiter un hotfix risqué.
4) Éliminer les dépendances de build de l’image runtime
- La CVE est‑elle dans des compilateurs, headers, gestionnaires de paquets, shells ?
- Cela est‑il nécessaire en production ?
Pourquoi : Les builds multi‑étapes peuvent supprimer des classes entières de findings sans « patcher » quoi que ce soit.
5) Décider : reconstruire, patcher, accepter avec preuve
- Reconstruire quand un correctif existe dans les paquets de base.
- Patcher quand le correctif est dans les dépendances applicatives.
- Accepter seulement avec une justification documentée et une date de révision ; idéalement produire des déclarations VEX.
Erreurs courantes : symptômes → cause racine → fix
1) Symptom: “We fixed it, but the scanner still shows it”
Cause racine : Vous avez mis à jour du code ou des paquets mais n’avez pas reconstruit l’image depuis une base fraîche ; ou vous avez reconstruit mais le déploiement exécute toujours l’ancien digest.
Correctif : Puller l’image de base fraîche en CI, reconstruire, pousser, et déployer par digest. Vérifier avec docker ps / équivalent orchestrateur que le digest a changé.
2) Symptom: “The scanner says vulnerable, vendor says not affected”
Cause racine : Backports ou sources d’avis mal appariées (NVD vs tracker sécurité de la distro).
Correctif : Privilégier des sources de scan conscientes de la distro ; enregistrer une exception avec preuve (chaîne de release du paquet + statut advisory du fournisseur). Envisager VEX pour encoder « non affecté ».
3) Symptom: “Every image has hundreds of CVEs, we ignore everything”
Cause racine : Images runtime gonflées, images de base anciennes, et aucune cadence de reconstruction. La fatigue d’alerte devient une politique.
Correctif : Builds multi‑étapes, base runtime minimale, reconstructions programmées, et blocage seulement sur catégories actionnables (corrigé+dans runtime+exposé+impact élevé).
4) Symptom: “Critical CVE in kernel package inside container”
Cause racine : Mauvaise interprétation : les images de conteneur n’apportent pas leur propre noyau ; les scanners signalent parfois paquets ou headers liés au noyau.
Correctif : Patch des nœuds hôtes. Pour l’image, retirer headers/outils du noyau sauf si nécessaire. Traiter « CVE noyau dans l’image » comme informatif sauf si vous livrez des outils liés au noyau.
5) Symptom: “We upgraded the base image and broke TLS/CA validation”
Cause racine : Passage à slim/distroless sans inclure explicitement les certificats CA ou les données de fuseau horaire attendues par votre appli.
Correctif : S’assurer que ca-certificates (ou équivalent) est présent ; copier explicitement les bundles de certificats si distroless. Ajouter des tests d’intégration pour TLS sortant.
6) Symptom: “Scan reports CVEs in packages we don’t have”
Cause racine : Le scanner détecte par signatures de fichiers ou devine des dépendances de langage depuis des manifests qui ne correspondent pas au contenu runtime.
Correctif : Confirmer avec des requêtes dans la base de paquets à l’intérieur de l’image ; générer un SBOM ; mettre à jour la config du scanner pour préférer les métadonnées du gestionnaire de paquets.
7) Symptom: “We can’t patch because the fixed version doesn’t exist yet”
Cause racine : Upstream n’a pas de patch, ou votre distro ne l’a pas livré, ou vous avez épinglé des dépôts.
Correctif : Atténuer : désactiver la fonctionnalité vulnérable, restreindre l’exposition, réduire les privilèges, ajouter des règles WAF, isoler le réseau. Suivre et reconstruire quand le correctif est disponible.
Listes de contrôle / plan étape par étape
Checklist opérations quotidienne (empêche le bruit de devenir culture)
- Scanner les images par digest, pas par tag.
- Stocker les résultats de scan liés au digest et à l’ID de build.
- Suivre les findings « actionnables » : correctif disponible + dans runtime + exposé/pertinent.
- Ouvrir des tickets avec nom/vers du paquet exact et chemin de remédiation (rebuild vs changement de code).
- Exiger une justification pour les exceptions, incluant des preuves (statut fournisseur, atténuations runtime).
Plan d’hygiène sécurité hebdomadaire (ennuyeux, efficace)
- Reconstruire les images exposées à Internet au moins chaque semaine depuis des bases fraîches.
- Re‑scanner les images reconstruites et comparer les deltas ; investiguer les augmentations.
- Remplacer les images de base dépassées ; arrêter d’utiliser des distros en fin de vie.
- Éliminer les outils de build des images runtime (multi‑étapes).
- Vérifier le durcissement runtime : utilisateur non‑root, drop des capacités, rootfs lecture seule quand possible.
Plan de triage étape par étape pour un finding « Critique »
- Vérification de l’artefact : confirmer que le digest en cours d’exécution correspond au digest scanné.
- Vérification du composant : vérifier que le paquet/bibliothèque est installé dans la couche runtime.
- Disponibilité du correctif : existe‑t‑il un paquet ou une release corrigée ?
- Exposition : le composant vulnérable est‑il dans le chemin de requête ou atteignable via une entrée utilisateur ?
- Privilèges : root ? capacités ajoutées ? système de fichiers inscriptible ?
- Atténuations : config désactive la fonctionnalité ? politiques réseau ? sandboxing ?
- Décision : reconstruire maintenant, patcher le code, ou accepter avec preuves et date de révision.
- Vérification : déployer par digest ; rescanner ; confirmer que la production exécute le nouveau digest.
FAQ
1) Devons‑nous bloquer les déploiements sur la base des scans de vulnérabilités ?
Oui, mais seulement sur des findings actionnables et pertinents : une version corrigée existe, le composant est dans l’image runtime, et le risque est pertinent (exposé/impact élevé).
Bloquer sur des findings « non corrigés » par défaut entraîne généralement des contournements.
2) Quel scanner devons‑nous faire confiance : Trivy, Docker Scout, Grype, autre ?
La confiance n’est pas une marque ; c’est un workflow. Utilisez au moins un scanner conscient de la distro et un autre qui peut générer un SBOM.
Le meilleur scanner est celui que vous tenez à jour et dont la sortie vous permet d’expliquer la situation à un auditeur sans danse interprétative.
3) Pourquoi mon image distroless a encore des CVE ?
Distroless réduit les paquets, mais n’élimine pas les vulnérabilités. Vous pouvez toujours livrer des bibliothèques vulnérables, des bundles CA, ou des runtimes de langage.
De plus, les scanners peuvent encore signaler des CVE pour des composants empaquetés dans votre binaire applicatif ou runtime.
4) Les vulnérabilités « non corrigées » sont‑elles sans danger à ignorer ?
Pas sûr de les ignorer. Sûr de les traiter différemment.
Si aucun correctif n’existe, vos options sont : atténuation (réduire l’exposition), remplacement (composant différent), ou acceptation avec preuves et déclencheur de revue.
5) Pourquoi voyons‑nous des CVE dans des paquets que nous n’utilisons jamais ?
Parce que les paquets sont installés, pas utilisés. Les scanners signalent la présence.
Votre travail est de réduire la présence (images plus petites) et d’évaluer la joignabilité/exposition de ce qui reste.
6) Comment gérer les faux positifs de backport sans créer une faille ?
Structurez les exceptions : liez‑les au digest d’image, à la chaîne de release du paquet, au statut du fournisseur (« corrigé via backport » / « non affecté »), et fixez une date de revalidation.
Préférez les déclarations de type VEX plutôt que des pages wiki ad‑hoc.
7) Devons‑nous préférer Alpine pour moins de CVE ?
Choisissez une image de base d’abord pour la compatibilité opérationnelle (problèmes glibc vs musl réels), puis pour l’hygiène sécurité.
Moins de CVE signalés n’est pas la même chose que moins de risque ; cela peut aussi être une couverture de reporting différente.
8) À quelle fréquence devons‑nous reconstruire les images ?
Services exposés à Internet : hebdomadaire est une bonne valeur par défaut. Jobs internes batch : mensuel peut suffire.
Si vous ne reconstruisez « que quand le code change », vous choisissez de manquer les correctifs de sécurité livrés via les mises à jour d’images de base.
9) Les atténuations runtime signifient‑elles que nous pouvons ignorer les vulnérabilités d’image ?
Les atténuations réduisent l’exploitabilité ; elles n’effacent pas les vulnérabilités.
Utilisez les atténuations pour gagner du temps et réduire le rayon d’impact, mais reconstruisez/patchtez quand des correctifs existent — surtout pour les CVE massivement exploitées.
10) Quelle est la façon la plus simple de réduire rapidement le bruit des scans ?
Builds multi‑étapes et suppression des outils de build des images runtime. Ensuite, ajoutez des reconstructions programmées depuis des bases fraîches.
Ces deux mesures seules réduisent généralement drastiquement les findings sans compromettre la fonctionnalité.
Conclusion : prochaines étapes qui réduisent vraiment le risque
Le scan de vulnérabilités est une lampe torche, pas un verdict de justice. Traitez‑le comme de la télémétrie : corrélez, confirmez, et agissez sur ce qui compte.
Si vous voulez moins de bruit et plus de sécurité, vous n’avez pas besoin d’un nouveau tableau de bord. Vous avez besoin de preuves et d’une cadence.
- Scanner par digest et prouver que la production exécute ce que vous avez scanné.
- Générer et stocker des SBOM pour chaque build ; utilisez‑les pour répondre « qu’y a‑t‑il dedans » instantanément.
- Reconstruire régulièrement depuis des images de base fraîches ; les mises à jour de sécurité arrivent souvent via la reconstruction, pas les changements de code.
- Réduire la surface runtime avec des builds multi‑étapes et des bases minimales.
- Durcir les defaults runtime : non‑root, drop des capacités, rootfs en lecture seule quand possible.
- Adopter des exceptions structurées (idéalement VEX) pour que « non affecté » ne devienne pas « ignoré pour toujours ».
Faites cela, et votre scanner cessera d’être un générateur de panique. Il deviendra ce qu’il aurait dû être : un outil qui vous aide à livrer des systèmes plus sûrs sans sacrifier vos weekends.