Aller au contenu
medium

L'affaire Trivy — Acte IV : Aqua a parlé, voici ce que ça change

17 min de lecture

Dans mon dernier billet sur l’affaire Trivy, j’écrivais : « Tant qu’Aqua Security n’aura pas publié une analyse complète et transparente de l’incident, je ne peux pas recommander cet outil en conscience. » Aqua a fini par publier cette analyse. Elle a été mise à jour cinq fois entre le 22 mars et le 1er avril 2026, avec l’appui du cabinet d’investigation forensique Sygnia et les contributions de Wiz Research, Socket Security, Aikido Security et CrowdStrike. Le résultat est un document dense, techniquement riche, mais qui mérite une lecture critique. Voici ce qu’il révèle, ce qu’il confirme, et ce qu’il ne dit pas.

La chronologie d’une communication laborieuse

Remettons d’abord les dates en perspective. L’affaire ne commence pas le 19 mars. Fin février 2026, l’attaquant exploite une misconfiguration dans l’environnement GitHub Actions de Trivy et exfiltre un token d’accès privilégié. Le 1er mars, l’équipe Trivy révèle publiquement cet incident initial et lance une rotation de credentials. C’est le sujet de mon premier billet. Mais la rotation est incomplète — on le saura plus tard.

Le 19 mars, l’attaquant réutilise les accès résiduels. Il publie la release empoisonnée v0.69.4 et réécrit la quasi-totalité des tags de trivy-action. Le soir même, l’équipe Trivy contient l’attaque en supprimant les artefacts malveillants.

Le 20 mars, les versions sûres et les premiers IOC sont publiés. Le 21 mars, le GHSA GHSA-69fq-xp46-6x23 est publié. Le 23 mars, le CVE-2026-33634 arrive côté NVD. Jusque-là, la réaction est raisonnablement rapide.

Puis le rythme ralentit. Le 22 mars, deux choses se produisent : Aqua publie le premier billet de blog, et l’attaquant frappe à nouveau — images Docker Hub 0.69.5 et 0.69.6, publication de dépôts internes en public. Le containment initial n’a pas tenu.

Les mises à jour s’enchaînent ensuite : 23 mars (engagement de Sygnia, Docker Hub), 24 mars (isolation enterprise, kics-github-action compromis), 25 mars (phase de remédiation et documentation), puis le 1er avril avec l’analyse technique complète.

DateÉvénement
Fin févrierExfiltration du token d’accès privilégié
1er marsDivulgation publique de l’incident initial, rotation de credentials
19 mars ~17:43 UTCRelease v0.69.4 + tags trivy-action empoisonnés
19 mars ~20:38 UTCContainment — artefacts malveillants retirés
20 marsVersions sûres + IOC publiés
21 marsPublication du GHSA-69fq-xp46-6x23
22 mars ~16:00 UTCImages Docker Hub 0.69.5/0.69.6 compromises
22 mars ~21:40 UTCDépôts internes publiés en public
22 marsPremier billet de blog Aqua
23 marsCVE-2026-33634 reçu par la NVD, engagement de Sygnia
24 marsIsolation enterprise, compromission kics-github-action
25 marsPhase remédiation/documentation
1er avrilAnalyse technique complète, IOC binaires, attribution

Trois constats. D’abord, le GHSA est arrivé le 21 mars, puis le CVE le 23 mars — ce n’est pas un silence complet. Mais pour un outil exécuté dans des milliers de pipelines, le niveau de détail utile est arrivé bien plus tard : les détails techniques substantiels n’apparaissent que dans le billet Aqua enrichi jusqu’au 1er avril, soit treize jours après l’attaque du 19 mars. Ensuite, le billet a été enrichi par vagues successives, chaque mise à jour ajoutant du contenu que des chercheurs externes avaient déjà publié. On reconnaît les contributions de Wiz et Socket, signalées par des astérisques dans le texte d’Aqua.

La transparence est réelle. Le rythme, lui, est discutable.

Ce que l’analyse technique d’Aqua révèle de nouveau

Passons au contenu. Le billet d’Aqua, une fois complet, fournit des détails que mes trois premiers articles ne couvraient pas ou seulement partiellement.

Le tag poisoning n’était pas une réécriture brute

Ce que BoostSecurity avait identifié comme une réécriture de tags, Aqua (avec les données de Socket) détaille maintenant comme une falsification forensique méthodique. Le GHSA parle de 76 tags sur 77 réécrits sur trivy-action (le catalogue Socket en dénombre 75 — la numérotation varie selon les sources). Pour chacun de ces tags, l’attaquant a :

  1. Pris le tree du HEAD de master (57a97c7e).
  2. Remplacé uniquement entrypoint.sh par le payload infostealer.
  3. Récupéré le commit original que le tag pointait.
  4. Cloné les métadonnées de ce commit : auteur, email, committer, les deux timestamps, le message complet avec numéro de PR et références « Fixes ».
  5. Défini le parent sur 57a97c7e au lieu du parent original.
  6. Force-push du tag vers ce commit forgé.

Le résultat : un git log qui affiche exactement les métadonnées attendues pour chaque version. Seuls quelques indices forensiques trahissent la falsification :

  • Les commits originaux étaient signés GPG par GitHub lors du merge via l’interface web. Les commits de l’attaquant ne le sont pas.
  • Chaque commit forgé prétend dater de 2021 ou 2022, mais a un parent de mars 2026 — une lignée impossible.
  • Chaque commit malveillant modifie uniquement entrypoint.sh, alors que les originaux touchaient plusieurs fichiers.

Ce niveau de sophistication explique pourquoi la détection a été si difficile. Un pipeline qui audite ses Actions en regardant les messages de commit et les dates n’aurait rien vu d’anormal. Il fallait vérifier les signatures GPG ou les SHAs.

Le payload en trois étages

L’entrypoint.sh malveillant, injecté dans trivy-action et setup-trivy, fait 204 lignes. Les 102 premières contiennent l’infostealer. Les 102 suivantes contiennent le code Trivy légitime. Le scan réel s’exécute normalement après l’exfiltration, ce qui rend la détection par l’utilisateur quasi impossible : le résultat attendu apparaît dans les logs.

Le payload opère en trois temps :

Étage 1 — Collecte. Sur les runners GitHub-hosted, le malware localise les processus Runner.Worker et lit /proc/<pid>/mem pour extraire les secrets du heap .NET. Il cherche le pattern JSON {"value":"<secret>", "isSecret": true} — le format interne que GitHub Actions utilise pour stocker les secrets en mémoire. Sur les runners self-hosted, un script Python se présente lui-même comme « TeamPCP Cloud stealer » et cible plus de 50 chemins sensibles : clés SSH, credentials AWS/GCP/Azure, tokens Kubernetes, configs Docker, clés TLS, configs VPN WireGuard, historique shell, et même les wallets de cryptomonnaies (Bitcoin, Ethereum, Solana, Monero, Ledger…).

Étage 2 — Chiffrement. Les données collectées sont chiffrées en AES-256-CBC avec dérivation PBKDF2. La clé de session est ensuite enveloppée avec la clé publique RSA-4096 de l’attaquant via OAEP. Le tout est empaqueté dans tpcp.tar.gz.

Étage 3 — Exfiltration. Canal primaire : POST HTTPS vers scan.aquasecurtiy[.]org (typosquat). Canal de secours : si le C2 échoue et qu’un GITHUB_PAT est disponible, le malware crée un dépôt public tpcp-docs sur le compte GitHub de la victime et y uploade le bundle chiffré en tant qu’asset de release. Le GITHUB_TOKEN n’est pas suffisant — il faut un PAT utilisateur.

Le binaire v0.69.4 : persistance sur machine développeur

Le binaire malveillant va plus loin que l’Action. Il vérifie que GITHUB_ACTIONS n’est pas "true" pour distinguer un runner CI/CD d’une machine développeur. Sur une machine locale, il :

  1. Écrit un dropper Python dans ~/.config/systemd/user/sysmon.py.
  2. Crée un service systemd utilisateur pour exécuter le script de façon persistante.
  3. sysmon.py dort 5 minutes, puis interroge un C2 hébergé sur la blockchain ICP (tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io).
  4. Si le C2 renvoie un lien qui n’est pas une URL YouTube (oui, le RickRoll servait de placeholder), le dropper télécharge le payload dans /tmp/pglog et l’exécute.

Le 22 mars, le canister ICP servait activement un payload nommé kamikaze.sh. Il a été signalé et marqué « Unavailable Due to Policy Violation » le même jour à 21:31 UTC.

L’attaquant a aussi tenté de voler les clés GPG et les accès Docker Hub

Un détail que mes articles précédents ne mentionnaient pas : l’attaquant a utilisé le compte de service aqua-bot pour pousser des workflows malveillants dans les dépôts tfsec, traceeshark et trivy-action. Ces workflows étaient conçus pour exfiltrer des clés GPG, des credentials Docker Hub, Twitter et Slack (Aqua utilise Teams, pas Slack — les credentials ciblées sont donc probablement héritées). L’exfiltration devait passer par un endpoint Cloudflare Tunnel (plug-tab-protective-relay.trycloudflare.com).

L’attaquant a aussi usurpé l’identité de contributeurs en falsifiant les métadonnées de commit : rauchg (poussé vers actions/checkout) et DmitriyLewen (poussé vers aquasecurity/trivy).

TeamPCP : l’attribution

Aqua attribue formellement l’attaque au groupe TeamPCP, également suivi sous les noms DeadCatx3, PCPcat et ShellForce. L’auto-identification dans le code Python du stealer (« TeamPCP Cloud stealer ») pourrait être un false flag, mais le recoupement technique est solide : ciblage cloud-native, exploitation d’API Docker et Kubernetes mal configurées, campagnes de cryptomining et de ransomware par worm, usage d’infrastructure ICP. Wiz traque formellement cet acteur sur threats.wiz.io.

Ce que le billet d’Aqua ne dit pas — ou dit en creux

« Incomplete early containment »

C’est la phrase la plus importante du billet, et elle est noyée dans la timeline : « Subsequent investigation revealed the rotation was not fully comprehensive, allowing the threat actor to retain residual access via still-valid credentials. »

Traduit : la rotation de credentials de début mars n’a pas fonctionné. L’attaquant a conservé un accès résiduel qui lui a permis de frapper à nouveau le 19 mars, puis le 22 mars. C’est exactement ce que BoostSecurity avait supposé dans son analyse du 20 mars. Aqua le confirme maintenant, mais la formulation reste clinique. On aimerait savoir combien de credentials n’avaient pas été rotées, pourquoi la rotation était incomplète, et quels processus ont été mis en place pour empêcher que ça se reproduise.

L’isolation enterprise : défense ou argument commercial ?

Aqua insiste — trois fois dans le billet — sur le fait que l’environnement commercial est architecturalement isolé : pas de dépôts partagés, pas d’infrastructure CI/CD commune, pas de secrets partagés, SSO, IP allowlisting, ZTNA, fork contrôlé avec revue de sécurité avant intégration.

C’est rassurant. Mais c’est aussi très exactement ce qu’un éditeur de sécurité doit dire après un incident qui touche sa version open source gratuite. La question n’est pas de savoir si l’architecture commerciale était isolée — c’est de savoir si cette isolation a été vérifiée par un tiers et si les résultats de cette vérification seront rendus publics. Le billet ne mentionne aucun audit externe de l’environnement commercial.

CanisterWorm : la propagation continue

La section « What’s Next » du billet confirme que le groupe TeamPCP a pivoté vers l’écosystème npm en utilisant les tokens de publication volés dans les pipelines compromis. Aikido Security a documenté ce worm auto-propagant baptisé CanisterWorm. StepSecurity avait déjà alerté. Aqua le reconnaît maintenant dans son billet, ce qui confirme que l’incident Trivy n’est pas un événement clos — c’est le vecteur initial d’une campagne de propagation supply chain en cours.

Les enseignements

Après quatre billets et six semaines de suivi, voici ce que cette affaire enseigne de façon durable.

1. Une rotation de secrets bâclée est pire que pas de rotation

La communauté a accepté la communication d’Aqua du 1er mars comme un incident contenu. Ce n’était pas le cas. Une rotation partielle a donné une fausse assurance de containment pendant 18 jours, durant lesquels l’attaquant préparait le second acte. Toute rotation de secrets doit être exhaustive, vérifiée, et accompagnée d’un audit de tous les usages du secret roté.

2. Le badge Immutable de GitHub ne suffit pas

La fonctionnalité immutable releases de GitHub protège réellement le tag après publication — trivy-action@0.35.0 en est la preuve. Mais elle ne protège pas d’un scénario où l’attaquant force-push un tag puis publie une release immutable qui fige l’état malveillant. Le badge « Immutable » affiché par GitHub devient alors un faux signal de confiance. L’épinglage sur SHA complet reste la seule protection véritablement immuable pour un pipeline.

3. Un outil de sécurité compromis n’est pas un CVE classique

L’attaque Trivy n’est pas une vulnérabilité logicielle. C’est une compromission d’identité machine suivie d’un empoisonnement de chaîne de distribution. Le modèle CVE/GHSA, conçu pour des bugs dans le code, est mal adapté à ce type d’incident. Le GHSA et le CVE sont arrivés en quelques jours, mais le contenu actionnable pour les équipes — détails des payloads, IOC binaires, attribution — n’a été publié par Aqua que treize jours plus tard.

4. La lecture de /proc/pid/mem est un angle d’attaque sous-estimé

Le fait que le malware lise la mémoire des processus runner pour extraire les secrets GitHub Actions en clair est un rappel brutal : les secrets d’un pipeline ne sont pas chiffrés en mémoire pendant l’exécution. C’est un vecteur d’attaque connu, mais la plupart des équipes ne le modélisent pas dans leur threat model CI/CD.

5. La blockchain comme C2 complique la réponse

Héberger le C2 sur un canister ICP est un choix délibéré : il n’y a pas de serveur à saisir, pas de domaine à blackholer, pas d’hébergeur à contacter. La suppression a fini par arriver (« Unavailable Due to Policy Violation »), mais le délai est structurellement plus long que pour un C2 classique. Les équipes de réponse à incident doivent intégrer les endpoints blockchain dans leur surveillance réseau.

6. La transparence d’un éditeur se mesure au rythme, pas au volume

Aqua a fini par publier un document technique riche. Mais l’essentiel de la valeur ajoutée est arrivé treize jours après l’incident, et une partie du contenu a été apportée par des chercheurs externes (Wiz, Socket). La transparence n’est pas un billet de blog détaillé publié deux semaines plus tard — c’est une communication claire dans les premières 48 heures, suivie d’enrichissements.

Ce que ça change dans mes recommandations

La question posée dans mon billet précédent était : « Aqua a-t-elle publié l’analyse complète et transparente qui permettrait de recommander Trivy à nouveau ? »

La réponse est partiellement oui. L’analyse technique est sérieuse. La collaboration avec Sygnia est un bon signal. L’admission de la rotation incomplète est un point de transparence important, même si la formulation reste prudente.

Mais plusieurs points me retiennent encore :

  • Pas d’audit externe publié sur l’isolation de l’environnement commercial.
  • Pas de détail sur le nombre et la nature des credentials non rotées en mars.
  • L’incident n’est pas clos : CanisterWorm continue de se propager.
  • La communication a été lente. Un outil exécuté dans des milliers de pipelines mérite un advisory clair dans les 24 heures, pas un billet enrichi par vagues sur deux semaines.

Ma recommandation reste donc la même : pour les pipelines de production, privilégiez Grype comme scanner principal et Syft pour la génération de SBOM.

Ce n’est pas un jugement définitif. Si Aqua publie un post-mortem avec l’analyse Sygnia complète, un audit indépendant de l’environnement commercial, et des garanties vérifiables sur les nouvelles protections de la chaîne de release, je réévaluerai.

Sources

Les autres billets de blog

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn