Article · 1 octobre 2026 · 6 min de lecture

Debug Detective : le tuyau

Toutes les autres affaires de cette série commencent par quelque chose de cassé. Celle-ci commence par une phrase en réunion. Aucune alerte, aucun job en échec, aucun log à ouvrir. Mon pipeline était vert et l'avait toujours été, et ce qui clochait dedans, c'était l'outil que j'avais installé pour me dire quand quelque chose clochait.

Didi immobile dans son bureau, la loupe baissee le long du corps, ecoutant Sudo la mouche a son oreille. Trois cles pendent a sa ceinture, dont une d'une forme differente des autres.

L'affaire que je n'ai pas résolue

Je n'ai rien trouvé. On me l'a dit. Mon mentor l'a mentionné à voix haute, en réunion, et c'est là toute la découverte.

Il n'y a pas de capture d'écran pour cette section, et il ne peut pas y en avoir. Toutes les autres pièces de cette série s'ouvrent sur le symptôme à l'écran. Celle-ci s'ouvre sur un collègue qui dit une phrase, parce que rien sur aucun de mes écrans n'allait jamais me la dire. Sudo n'aurait pas pu aider non plus. Elle rend visible la colonne cachée, et là rien n'était caché dans une colonne. Ce n'était pas dans mon système du tout.

Ce qui s'était réellement passé

Le 19 mars 2026, quelqu'un détenant des identifiants de publication volés a publié un Trivy v0.69.4 malveillant. 76 des 77 tags de version de trivy-action ont été réécrits de force vers des commits voleurs d'identifiants, et les sept tags de setup-trivy ont été remplacés. Trois jours plus tard, des images empoisonnées arrivaient sur Docker Hub, 0.69.5 et 0.69.6.

La charge lisait la mémoire du processus du runner via /proc/<pid>/mem et balayait plus de cinquante chemins à la recherche de clés SSH, d'identifiants cloud, de jetons Kubernetes et de configurations Docker. Ce qu'elle récoltait, elle le chiffrait et l'expédiait. Quand elle ne pouvait pas expédier, elle déversait le butin dans un dépôt public nommé tpcp-docs.

Les fenêtres étaient courtes. Trois heures pour le binaire. Dix pour les images.

La ligne dans mon propre pipeline

Historique git montrant que le composant de scan utilisait aquasec/trivy:latest avant le passage à Grype.
La ligne telle qu'elle était, et le commit qui l'a enfin supprimée.

latest. Le tag que tous les tutoriels vous tendent. Celui qu'on écrit quand la version ne semble pas être la partie intéressante de la ligne.

Pendant l'incident, latest pointait sur le build malveillant. Les mainteneurs ont dû le faire revenir à la main vers une version saine.

Ce que j'ai fait, et ce que je n'ai pas fait

J'ai remplacé Trivy par Grype. Un fichier, une dizaine de minutes, commité le 28 mars à 09:15, six jours après la fermeture de la seconde fenêtre.

Puis je suis passé à autre chose.

Je n'ai pas vérifié si j'avais été touché. Je n'ai rien fait tourner. J'ai traité une compromission de chaîne d'approvisionnement comme une montée de version. C'est l'erreur dont parle cet article, et je l'ai commise en pleine rédaction d'une série sur le fait de ne pas commettre ce genre d'erreur.

Deux fausses pistes, les miennes

Cinq mois plus tard j'ai enfin posé la question, et je me suis trompé deux fois avant de tomber juste.

D'abord j'ai raisonné à partir des commits. Aucun commit dans les quatre dépôts ne tombait dans l'une des fenêtres, donc aucun pipeline n'avait pu tourner, donc j'étais tranquille. C'est un indicateur indirect, et les indicateurs indirects sont exactement là où cette série trouve ses fantômes. Un pipeline se déclenche aussi sur une relance, un lancement manuel, un tag ou une planification.

Ensuite j'ai raisonné à partir de la définition du job. Le job de scan ne déclare ni variables, ni secrets, ni id_tokens, donc il ne porte aucun identifiant, donc il n'y avait rien à voler. Faux également. GitLab injecte les variables de projet et de groupe dans chaque job, que le job les demande ou non.

Les deux donnaient l'impression de vérifier. Aucune des deux ne vérifiait quoi que ce soit.

Ce que disait vraiment le log

Chaque pipeline est journalisé, et les fenêtres sont connues à la minute près. Dix-sept jobs de scan ont tourné cette semaine-là. L'un d'eux a démarré à 16:44:55 UTC le 22 mars, une heure après la mise en ligne des images empoisonnées.

Un script listant dix-sept jobs de scan et signalant celui qui a tourné dans la fenêtre malveillante.
Dix-sept jobs cette semaine-là. Un seul dans la fenêtre.

Puis la trace du job, qui tranche :

16:45:55Z  Unable to find image 'aquasec/trivy:latest' locally
16:45:56Z  latest: Pulling from aquasec/trivy
16:45:58Z  Status: Downloaded newer image for aquasec/trivy:latest

Pas une couche en cache. Un téléchargement à froid, depuis Docker Hub, dans la fenêtre. Et l'empreinte téléchargée figure dans l'avis de sécurité parmi les images malveillantes.

Une comparaison d'empreintes montrant qu'une empreinte de la trace du job correspond à la liste des images Trivy malveillantes.
Quatre empreintes dans la trace. L'une d'elles est sur la liste.

Je l'ai exécutée. Pendant cinq mois j'avais supposé que non, sur la seule foi de n'avoir jamais regardé.

Il n'y a aucun dépôt tpcp-docs dans mon espace de noms, et c'est la seule bonne nouvelle que laisse cette charge. C'est aussi la moitié la plus faible de la preuve : ce dépôt servait de repli quand l'exfiltration échouait, et la voie principale était un envoi chiffré qui ne laisse rien derrière lui. J'ai donc fait tourner tout ce que le job pouvait atteindre et j'ai cessé d'essayer de prouver une absence.

Ce qui a limité les dégâts

Deux choses, dont aucune n'était une décision prise à propos des chaînes d'approvisionnement.

Le job tournait sur un runner SaaS partagé, donc le conteneur a été jeté quelques minutes plus tard. Il n'y avait pas d'hôte durable en dessous portant des clés SSH ou un kubeconfig.

Et le seul accès cloud de tout le pipeline passe par OIDC, échangé contre un jeton qui meurt avec le job. Il n'y avait aucune clé statique à prendre dans ce processus, parce que j'avais supprimé les clés statiques des mois plus tôt pour une raison totalement étrangère.

Le travail structurel paie un jour que vous n'aviez pas prévu, contre une menace à laquelle vous ne pensiez pas.

Et vous n'avez pas le droit de vous sentir malin pour autant, parce que vous n'avez pas été malin. Vous avez été soigneux.

Note du détective

Le fantôme n'était pas dans la machine. Il était dans l'actualité, et je ne la lisais pas.

Vous ne savez pas ce que votre pipeline livrerait ?

La plupart des équipes ne se sont jamais demandé quels jobs peuvent lire quels identifiants. Si vous voulez un deuxième regard là-dessus, écrivez-moi.

Contactez-nous