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évrier | Exfiltration du token d’accès privilégié |
| 1er mars | Divulgation publique de l’incident initial, rotation de credentials |
| 19 mars ~17:43 UTC | Release v0.69.4 + tags trivy-action empoisonnés |
| 19 mars ~20:38 UTC | Containment — artefacts malveillants retirés |
| 20 mars | Versions sûres + IOC publiés |
| 21 mars | Publication du GHSA-69fq-xp46-6x23 |
| 22 mars ~16:00 UTC | Images Docker Hub 0.69.5/0.69.6 compromises |
| 22 mars ~21:40 UTC | Dépôts internes publiés en public |
| 22 mars | Premier billet de blog Aqua |
| 23 mars | CVE-2026-33634 reçu par la NVD, engagement de Sygnia |
| 24 mars | Isolation enterprise, compromission kics-github-action |
| 25 mars | Phase remédiation/documentation |
| 1er avril | Analyse 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 :
- Pris le tree du
HEADdemaster(57a97c7e). - Remplacé uniquement
entrypoint.shpar le payload infostealer. - Récupéré le commit original que le tag pointait.
- 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 ».
- Défini le parent sur
57a97c7eau lieu du parent original. - 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 :
- Écrit un dropper Python dans
~/.config/systemd/user/sysmon.py. - Crée un service systemd utilisateur pour exécuter le script de façon persistante.
sysmon.pydort 5 minutes, puis interroge un C2 hébergé sur la blockchain ICP (tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io).- 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/pgloget 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
- Aqua Security — Trivy Supply Chain Attack: What You Need to Know — billet officiel, 5 mises à jour (22 mars → 1er avril 2026)
- GitHub Discussion #10425 — discussion maintainers et communauté
- GHSA-69fq-xp46-6x23 — GitHub Security Advisory
- Wiz Research — TeamPCP Supply Chain Attack — attribution, IOC binaires, modèle SITF
- Socket Security — Trivy GitHub Actions Compromise — catalogue complet des 75+7 tags compromis
- Aikido Security — CanisterWorm — worm npm auto-propagant
- Wiz Research — KICS GitHub Action Compromise — compromission parallèle