
🌐 Aussi en: English · Deutsch · Español
Deep Dive de la série autour de l’Ansible Tour. Niveau : administration Linux avancée. Le Tour explique les rôles ; les deep dives expliquent la technique en dessous – comment elle fonctionne, pourquoi elle est là, ce que cette installation en fait exactement, comment l’exploiter au quotidien et quelles autres options il y aurait eu.
Chaque image de cette installation est épinglée à une version exacte. C’est bon pour la reproductibilité et mauvais pour la sécurité sur un point précis : une image épinglée ne s’améliore pas toute seule. Les paquets Debian ou Alpine qu’elle contient vieillissent, la version de Go avec laquelle un binaire a été compilé vieillit, et de nouvelles vulnérabilités sont publiées chaque jour pour des logiciels qui n’ont pas changé. L’hôte reçoit ses mises à jour de sécurité chaque nuit par dnf-automatic ; les images ne recevaient rien – jusqu’à ce que quelqu’un regarde. Trivy regarde désormais chaque nuit. Cet article explique comment il trouve les vulnérabilités, comment il tourne sur le blog depuis le 19 octobre 2026, ce que le premier scan a trouvé – 241 vulnérabilités corrigeables – et pourquoi ce nombre est bien moins alarmant qu’il n’y paraît une fois trié.
🧭 Comment fonctionne Trivy
D’abord un inventaire, ensuite une base de données
Trivy n’exécute ni n’attaque rien. Il lit une image couche par couche et dresse un inventaire : quel système et quelle version (Debian 13, Alpine 3.21, Ubuntu 22.04 …), quels paquets dans quelles versions exactes d’après la base du gestionnaire de paquets, et quelles dépendances sont compilées dans les binaires ou listées dans des fichiers de verrouillage – modules Go lus dans les informations de compilation que porte chaque binaire Go, npm, pip, Composer, Java, etc. Puis il compare cet inventaire à sa base de vulnérabilités.
Cette base est construite par le projet Trivy à partir de nombreuses sources – les suivis de sécurité des distributions, la GitHub Advisory Database, la base de vulnérabilités Go et d’autres – et téléchargée sous forme d’un seul fichier de quelques centaines de mégaoctets. Pour chaque paquet, elle sait quelles versions sont touchées par quelle CVE et, si elle existe, dans quelle version le problème est corrigé.
Gravité et « version corrigée » – les deux champs qui comptent
- La gravité (CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN) vient de la source – souvent l’évaluation de la distribution, sinon un score CVSS. Elle décrit la vulnérabilité en général, pas son risque dans cette image. Un bug CRITICAL dans une bibliothèque que l’image n’appelle jamais reste listé comme CRITICAL.
- La version corrigée (Fixed Version) dit s’il existe un correctif. Sans lui, il ne reste qu’à accepter la vulnérabilité ou à remplacer le logiciel. Avec lui, la question devient : qui doit le livrer – la distribution (déjà fait), le mainteneur de l’image (reconstruction nécessaire) ou le projet amont (nouvelle version nécessaire) ?
Cette distinction est la clé de tout ce qui suit. Trivy signale ce qui est connu ; il ne peut pas savoir si le code vulnérable est atteignable. Lire les résultats est donc toujours une seconde étape, faite par un humain.
D’où vient l’image
Trivy peut lire une image depuis un registre (remote), un démon Docker ou Podman local, un fichier tar – ou directement depuis containerd, le runtime utilisé par K3s. Cette dernière option signifie : pas de second téléchargement, le scan voit exactement l’image exécutée par les pods. Ici, il lui faut le socket et le namespace de K3s :
CONTAINERD_ADDRESS=/run/k3s/containerd/containerd.sock \
CONTAINERD_NAMESPACE=k8s.io \
trivy image --image-src containerd docker.io/library/mariadb:10.11.19🛡️ Pourquoi il est là
- Les images épinglées vieillissent en silence. Le rafraîchissement d’images des playbooks de mise à jour récupère les images reconstruites sous le même tag – mais rien ne nous disait quand une reconstruction valait la peine, ni quand une image n’en recevrait jamais.
- L’hôte était couvert, pas les conteneurs. dnf-automatic corrige l’hôte chaque nuit ; les 14 images qui composent le cluster étaient hors de cette boucle.
- Les décisions ont besoin de données. « Faut-il changer cet exportateur ? » ou « Une mise à jour de K3s est-elle urgente ? » sont plus simples avec la liste de ce que chaque image contient vraiment.
🔧 Ce que fait ce dépôt
Un timer sur l’hôte, pas un opérateur dans le cluster
La manière habituelle d’utiliser Trivy dans Kubernetes est le Trivy Operator : il surveille le cluster, lance un job de scan par charge de travail et stocke les résultats comme ressources personnalisées. Sur un seul petit nœud, cela signifierait des CRD, un contrôleur, des pods de scan – et pour chacun des exceptions dans la politique d’images et les NetworkPolicies que cette installation applique. Le rôle common_trivy fait le même travail avec un binaire et un script : un timer systemd lance trivy_scan.py chaque nuit à 05:15, après les mises à jour de sécurité (01:00–02:00), la fenêtre de redémarrage (03:30) et les autres tâches nocturnes.
Le binaire : épinglé et vérifié
Trivy est installé depuis l’archive de la release GitHub, avec la version et son SHA-256 épinglés dans versions.yml. Ce second épinglage a une raison concrète : en mars 2026, la chaîne de publication de Trivy elle-même a été compromise et a brièvement distribué une version malveillante (avis GHSA-69fq-xp46-6×23). Un scanner de sécurité tourne en root et lit tout – c’est précisément l’outil dont on veut être sûr. La règle dans les notes : ne passer qu’à une release vieille d’au moins deux semaines et reprendre la somme de contrôle du fichier de checksums de cette release. Le get_url d’Ansible refuse une archive dont l’empreinte ne correspond pas.
Deux petites choses rencontrées à l’installation, toutes deux voulues ailleurs : /tmp est monté noexec par le durcissement CIS, l’archive est donc décompressée depuis /root ; et après décompression, restorecon donne au binaire son étiquette SELinux correcte – un fichier écrit via un chemin temporaire garde sinon l’étiquette de ce chemin, la même leçon qu’avec le binaire k3s.
Le script
- Mettre à jour la base de vulnérabilités une fois (
--download-db-only) ; tous les scans d’images tournent ensuite avec--skip-db-update. - Lister les images de tous les pods en cours d’exécution. Les pods terminés des Jobs et CronJobs sont exclus – la première version les incluait, et un job helm-install exécuté une fois au démarrage du cluster ajoutait 32 vulnérabilités pour une image qui ne tourne pas.
- Analyser chaque image depuis containerd. En cas d’échec, nouvel essai depuis le registre (voir plus bas). Un rapport JSON par image dans
/var/lib/trivy/reports. - Écrire des métriques pour le collecteur textfile du node_exporter :
trivy_image_vulnerabilities{image, severity, fixable},trivy_image_scan_ok,trivy_db_update_ok,trivy_last_run_timestamp_seconds. Le fichier est écrit sous un nom temporaire puis renommé, pour que node_exporter n’en lise jamais la moitié. - Comparer les vulnérabilités CRITICAL et HIGH corrigeables avec la nuit précédente et n’envoyer par e-mail que les nouvelles.
Pourquoi un e-mail pour les nouvelles vulnérabilités, et pas une alerte
La conception évidente serait une alerte Grafana « vulnérabilité critique avec correctif disponible ». Le premier scan a montré pourquoi ce serait une erreur : l’image MariaDB contient gosu, un petit utilitaire écrit en Go, compilé avec une ancienne version de Go dont la bibliothèque standard a une vulnérabilité CRITICAL. La corriger revient aux mainteneurs de l’image MariaDB. Une alerte se déclencherait, se répéterait toutes les quatre heures, et cela pendant des jours ou des semaines – jusqu’à être ignorée, et avec elle toutes les autres alertes. L’e-mail le dit une fois. Une vulnérabilité qui reste ne renvoie pas d’e-mail ; une qui disparaît est comptée comme « disparue ». Grafana n’alerte que si le scan lui-même est cassé : échec de la mise à jour de la base ou d’un scan d’image, ou dernier passage réussi vieux de plus de 36 heures.
Accepter une vulnérabilité
Certaines vulnérabilités seront acceptées volontairement. Elles vont dans la variable trivy_ignore, chacune avec une justification obligatoire, et finissent dans un fichier .trivyignore utilisé par le scan. La liste est vide exprès : chaque entrée est une décision, pas une valeur par défaut.
SELinux et le collecteur textfile
node_exporter tourne dans son propre domaine SELinux depuis le même jour (après une phase d’apprentissage permissive, en enforcing depuis les 20 et 21 octobre). Son répertoire textfile /var/lib/node_exporter/textfile a reçu son propre type, homelab_node_exporter_textfile_t, avec exactement deux règles : l’exportateur peut lister le répertoire et lire les fichiers. Le scan, en root, les écrit ; les nouveaux fichiers héritent du type du répertoire.
🔍 Ce que les premiers passages ont trouvé – sur l’outil lui-même
Des couches que containerd avait déjà jetées
Deux des 15 images ont échoué au premier passage : metrics-server et kube-state-metrics, avec « content digest … not found ». containerd décompresse les couches d’image en snapshots et peut ensuite supprimer les blobs compressés ; l’image tourne toujours, mais ne peut plus être exportée en entier. Trivy propose --image-src containerd,remote comme liste de repli – ce qui n’a pas aidé, car il ne se replie que si l’image manque dans la première source, pas si une couche manque. Le script réessaie désormais explicitement avec remote pour ces images.
Un AVC, trouvé par les alertes de journaux du même jour
La première version de l’unité de scan avait NoNewPrivileges=yes. Le script appelle k3s kubectl pour lister les pods, et le binaire k3s passe dans le domaine SELinux container_runtime_t. Sous NoNewPrivileges, SELinux refuse cette transition si la politique ne l’autorise pas explicitement, et consigne avc: denied { nnp_transition }. La nouvelle alerte Loki sur les vrais refus SELinux s’est déclenchée en quelques minutes. Le réglage a été retiré ; l’unité garde les autres restrictions systemd (/usr et /etc en lecture seule, pas de répertoires personnels, /tmp privé, priorité CPU et E/S minimale, plafond mémoire de 2 Go).
📊 Lire 241 vulnérabilités
Après les corrections, le scan signalait 209 vulnérabilités CRITICAL/HIGH corrigeables sur 14 images en cours. Triées selon qui peut les corriger, le tableau change complètement :
| Groupe | Vulnérabilités | Qui peut corriger | Action |
|---|---|---|---|
| php-fpm_exporter 2.2.0 | 72 (5 des 6 CRITICAL) | Personne : dernière release en 2022, encore sur OpenSSL 1.1 | Remplacer, reconstruire ou retirer – une décision, voir plus bas |
| MariaDB 10.11.19 | 26 | Mainteneurs de l’image : une reconstruction sous le même tag est déjà publiée (libssl3) | Rafraîchissement d’image dans le playbook de mise à jour ; gosu reste jusqu’à sa reconstruction en amont |
| WordPress 7.1.3 (php8.4-fpm) | 32 | Debian a corrigé curl, ghostscript, expat ; l’image officielle n’a pas été reconstruite depuis | Attendre – le rafraîchissement prendra automatiquement la prochaine reconstruction |
| nginx, Redis (Alpine) | 4 | Idem : correctifs dans Alpine, image pas encore reconstruite | Attendre |
| Binaires Go : CoreDNS, metrics-server, cert-manager, kube-state-metrics, mysqld_exporter | 75 | Projets amont : nouvelles versions avec un Go et des modules plus récents | Déjà sur les dernières versions ; celles fournies avec K3s viendront avec le prochain K3s |
Trois leçons de ce tableau :
- La plupart des vulnérabilités ne demandent rien de notre part. Elles sont corrigées en amont et arriveront avec la prochaine reconstruction ou version, que les playbooks de mise à jour récupèrent déjà.
- Une image pose un vrai problème. Un exportateur sans release depuis quatre ans, qui embarque encore OpenSSL 1.1, représente près d’un tiers de toutes les vulnérabilités et presque toutes les CRITICAL. Son exposition réelle est faible – il ne parle que FastCGI avec PHP-FPM dans le même pod et sert des métriques dans le cluster, derrière des NetworkPolicies –, mais une « faible exposition » n’est pas une raison de garder éternellement un logiciel abandonné.
- « Corrigé » ne veut pas dire « pertinent ». L’image WordPress liste
linux-libc-dev, les en-têtes du noyau – un conteneur utilise le noyau de l’hôte, les en-têtes ne servent qu’à compiler. Un candidat pourtrivy_ignore, avec exactement cette justification.
Une question est venue tout de suite : existe-t-il une image WordPress plus récente qui corrige ses 32 vulnérabilités ? Non. WordPress 7.1.3 est la version actuelle, et l’image 7.1.3-php8.4-fpm du blog a le même digest que celle du registre. Debian a publié les paquets corrigés, mais l’image officielle n’a pas été reconstruite depuis. Construire notre propre image avec un apt-get upgrade supplémentaire réglerait cela aujourd’hui – au prix de devoir maintenir un build d’image pour toujours. Attendre quelques jours la reconstruction officielle est le meilleur compromis.
🧰 Exploitation au quotidien
# quand a-t-il tourné, combien de temps, combien de mémoire ?
systemctl list-timers trivy-scan.timer
journalctl -u trivy-scan --since today
# le lancer maintenant (oneshot, rend la main à la fin)
systemctl start trivy-scan.service
# la ligne de synthèse du dernier passage
journalctl -u trivy-scan -n 20 | grep "images scanned"
# quelles vulnérabilités sont ouvertes (image, CVE, paquet, installé -> corrigé)
python3 -c 'import json; [print(v["severity"], v["id"], v["pkg"], v["installed"], "->", v["fixed"], v["image"]) for v in json.load(open("/var/lib/trivy/last-findings.json")).values()]' | sort | less
# une image en détail
trivy image --image-src containerd --skip-db-update docker.io/library/redis:7.4.11-alpineDans Grafana (Explore → Prometheus), les mêmes chiffres sont interrogeables :
# CRITICAL corrigeables par image
trivy_image_vulnerabilities{severity="CRITICAL", fixable="true"} > 0
# total HIGH+CRITICAL corrigeables dans le temps - baisse-t-il après une mise à jour ?
sum(trivy_image_vulnerabilities{severity=~"CRITICAL|HIGH", fixable="true"})
# âge du dernier scan en heures
(time() - trivy_last_run_timestamp_seconds) / 3600Que faire d’un e-mail
- L’image est-elle reconstruite en amont sous le même tag ? Alors la prochaine mise à jour (rafraîchissement d’image) la corrige – ou lancer le playbook de mise à jour tout de suite.
- Existe-t-il une version plus récente ? Alors relever l’épinglage dans
versions.yml, de préférence vers une version de quelques jours. - Ni l’un ni l’autre ? Alors c’est une vulnérabilité à accepter (avec justification dans
trivy_ignore) ou une raison de remplacer le logiciel.
Coût
La base de vulnérabilités et le cache des couches occupent environ 1,6 Go sous /var/cache/trivy. Un passage nocturne complet, téléchargement de la base compris, prend moins d’une demi-minute de CPU sur le blog ; les passages suivants réutilisent l’analyse des couches en cache et durent quelques secondes. Le scan tourne avec la priorité CPU et E/S la plus basse et un plafond de 2 Go de mémoire : il ne peut pas ralentir les sites.
🆘 Antisèche : symptôme → commande
La version courte de cet article, pour le moment où quelque chose ne va pas. À lancer sur l’hôte en root ; <…> sont des espaces réservés.
| Symptôme | Commande |
|---|---|
| Le scan a-t-il tourné ? | systemctl list-timers trivy-scan.timer |
| Le lancer maintenant | systemctl start trivy-scan.service |
| Qu’est-ce qui est ouvert en ce moment ? | python3 -c 'import json; [print(v["severity"], v["id"], v["pkg"], v["image"]) for v in json.load(open("/var/lib/trivy/last-findings.json")).values()]' | sort |
| Une image en détail | trivy image --image-src containerd --skip-db-update <image>:<tag> |
| Un mail sur une découverte CRITICAL | # 1. rebuilt upstream under the same tag? -> next update run |
| Résultats de scan trop anciens | (time() - trivy_last_run_timestamp_seconds) / 3600 |
| Espace occupé par le cache | du -sh /var/cache/trivy |
🔭 La suite
- Une décision sur l’exporter PHP-FPM d’ici le 20 janvier 2027, quand son acceptation expire : deux forks maintenus par une seule personne sont actifs (l’un avec des mises à jour automatiques des dépendances) ; sinon, prolonger l’acceptation.
- Scan de configuration – essayé une fois en octobre 2026 sur les manifestes en service du blog : 21 types de constats, presque tous des choix délibérés (ingress sur le réseau de l’hôte, volumes hostPath, images qui tournent en root). En job CI, il faudrait des templates rendus et une longue liste d’exceptions ; cela n’en vaut pas la peine pour l’instant.
- Le scan de secrets est déjà dans la CI : gitleaks vérifie tout l’historique git à chaque push, en plus du hook pre-commit.
- Un petit tableau de bord avec l’évolution des vulnérabilités ouvertes par image.
⚖️ Alternatives – et pourquoi Trivy
| Option | Point fort | Pourquoi pas ici |
|---|---|---|
| Trivy Operator | Continu, résultats comme ressources Kubernetes | Contrôleur, CRD et pods de scan sur un seul petit nœud ; exceptions dans la politique d’images et les NetworkPolicies. |
| Grype (Anchore) | Rapide, bonne intégration SBOM avec Syft | Comparable ; Trivy lit containerd directement et couvre plus d’écosystèmes dans un seul binaire. |
| Scan dans le registre (Harbor/Clair, Docker Scout, GitHub) | Analyse au moment du push | Pas de registre propre ici – les images viennent telles que publiées de Docker Hub, Quay et registry.k8s.io. |
| dnf updateinfo / OpenSCAP | Exact pour l’hôte | Ne couvre que les paquets de l’hôte, pas le contenu des images. |
| Trivy, en timer nocturne sur l’hôte | Un binaire, lit les images en cours depuis containerd, métriques + e-mail | Choisi. |
La valeur de l’outil n’est pas dans le nombre qu’il affiche mais dans le tri qu’il permet. 241 vulnérabilités sonnaient comme une urgence. Triées selon qui peut les corriger, elles sont devenues une vraie décision, un rafraîchissement d’image – et beaucoup d’attente côté amont, que la routine de mise à jour existante gère déjà.


