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.

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
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.
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.
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.
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.
- Figez vos outils.
:latestest une promesse permanente que quelqu'un d'autre choisira les octets que vous exécuterez demain. - Votre scanner est une dépendance. Tout ce que vous installez pour vérifier des choses est une chose à vérifier.
- Remplacer un outil compromis, c'est de la remédiation. Ce n'est pas une enquête. Demandez-vous si vous avez été touché, et posez la question le jour même.
- Méfiez-vous de la vérification qui ressemble à une vérification. Les commits ne sont pas des pipelines, et une définition de job vide n'est pas un environnement vide.
- Les runners éphémères et les identifiants à courte durée de vie sont ce qui sépare un mauvais téléchargement d'un mauvais mois.
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
flochai

