Retour à tous les articles
Publié le · par Renaud Deraison

Ils ont changé l'adresse, pas le paquet

Pendant quatorze heures, le 31 août 2026, certaines requêtes vers registry.coder.com ont reçu leur réponse de serveurs qu'un attaquant avait ajoutés au pool d'origine Cloudflare de Coder. Le nom d'hôte était le bon, le certificat était valide, les modules portaient les noms et les versions qu'ils avaient toujours eus, et le code renvoyé est parti à la recherche de clés SSH, de jetons OIDC et de mots de passe de bases de données. Coder ne dispose pas des journaux de l'attaquant : son avis de sécurité vous demande donc d'inspecter votre propre trafic sortant. C'est une question à laquelle la plupart des machines qui font tourner un agent de codage ne peuvent pas répondre.

Le nom d'hôte était celui de Coder. Le certificat était valide. Le module portait le nom et la version que votre template épinglait déjà. Pendant quatorze heures, le 31 août, certains des serveurs qui répondaient appartenaient à quelqu'un d'autre.

Un ingénieur plateforme met à jour un template de workspace un lundi matin. terraform init récupère les modules que le template nomme (Docker, Zed, aider) depuis registry.coder.com, là même d'où ils venaient le mois dernier. Terraform affiche son mur de vert habituel et le build réussit. Aucun outil ne signale quoi que ce soit, ni ce matin-là, ni ce mois-là.

Le 1er septembre, Coder a publié GHSA-vx42-ghc9-gw65, classé critique avec un CVSS de 9,0. Dans ses termes : « Un acteur malveillant non identifié a obtenu l'accès à l'infrastructure Cloudflare de Coder et a ajouté des adresses IP non autorisées au pool utilisé par le registre de modules de Coder. » Entre 07h35 et 21h45 UTC le lundi 31 août, certaines requêtes portant sur des modules Coder ont été servies par des machines contrôlées par l'attaquant. Coder construit des environnements de développement cloud auto-hébergés ; BleepingComputer cite Dropbox, Palantir, Square, Mercedes-Benz, KKR, EnBW, des organismes du gouvernement américain et des sous-traitants de la défense parmi ses utilisateurs.

Personne n'a touché au paquet

Toutes les habitudes de chaîne d'approvisionnement d'une équipe rigoureuse portent sur l'artefact. Le nom est-il bien orthographié. L'éditeur est-il bien celui qu'il prétend. Cette version a-t-elle été publiée hier. L'empreinte du lockfile correspond-elle encore. Lisez le diff avant de monter de version.

L'attaquant a changé quelles machines répondaient quand votre client a demandé l'adresse, et a laissé l'artefact intact. Il n'a publié aucun paquet, n'a pris le contrôle d'aucun compte de mainteneur, n'a enregistré aucun nom sosie et n'a rien modifié dans le registre de Coder. Le nom s'est résolu comme il se résout toujours. La poignée de main TLS s'est achevée face à un certificat valide pour le vrai nom d'hôte. Le module s'appelait docker, et zed, et aider, parce que ce sont les modules qu'un template Coder demande, et il portait le numéro de version que le template épinglait déjà.

Ce qui est revenu était différent. Les copies empoisonnées embarquaient un script shell nommé dlp.sh, avec sa propre build pour les modules docker, aider, rstudio-server, windows-rdp et zed, et sa propre empreinte pour chacun dans l'avis de sécurité. L'avis décrit la charge utile comme du code « conçu pour identifier un certain nombre d'identifiants et les exfiltrer vers un serveur distant dont le nom de domaine imite un domaine que Coder utilise régulièrement. » Ce domaine est coder-infra[.]com, enregistré le 28 août, trois jours avant que quiconque s'en serve, pointant vers 199.91.220.205, avec le rappel sur http://www[.]coder-infra[.]com/cli/check.

La liste de ce qu'il visait se lit comme un inventaire de la confiance d'une plateforme de développement : variables d'environnement et secrets du provisionneur, jetons OIDC des utilisateurs, clés SSH, identifiants d'authentification externe, et variables de configuration de coderd, dont les mots de passe de base de données et les identifiants du fournisseur d'authentification.

Vous n'aviez rien à faire d'inhabituel pour être dans la fenêtre. D'après l'avis : « Les utilisateurs qui se servent du registre de modules principal de Coder (registry.coder.com) comme source de leurs templates et qui ont mis à jour des composants pendant la période où ce code était disponible ont pu installer les paquets malveillants. » Créer un template, monter la version d'un template, lancer un dry run ou construire un workspace avec le cache de modules désactivé, c'est du travail de lundi ordinaire.

31 août, 07h35–21h45 UTC — la fenêtre de quatorze heuresle build demandeterraform initnouveau template,montée de version, dry runle nom se résoutregistry.coder.comCloudflare, la vraieporte d'entréeune IP dans le poolajoutée par l'attaquant,qui sert un module modifiédlp.shle rappelcoder-infra[.]comenregistré le 28 août,utilisé le 31Ce que le client a vérifié, et ce qu'il a trouvéle nom d'hôtecelui de Coder, pas undomaine sosiele certificatvalide, pour le vrai nom,sur la vraie edgele nom du moduledocker, zed, aider —ceux que vous demandezla versioncelle déjà présente dansle template. Rien de neufLes quatre contrôles passent. Quel serveur a répondu : la seule chose qu'aucun contrôle côté client ne regarde.
L'attaquant a ajouté ses propres adresses IP au pool d'origine derrière registry.coder.com : le nom d'hôte, le certificat, le nom du module et la version épinglée étaient donc tous authentiques pendant que les octets venaient d'ailleurs. Un build empoisonné diffère d'un build sain sur un seul point observable : une connexion sortante vers un domaine enregistré trois jours plus tôt.

« Examinez les journaux de pare-feu, de proxy, de DNS et de flux VPC »

C'est la cinquième étape des mesures correctives de Coder, et la ligne la plus instructive de l'avis. Les étapes un à quatre sont des choses pour lesquelles Coder peut aider : du SQL pour retrouver les modules mis en cache pendant la fenêtre, du SQL pour les purger, des versions corrigées (2.37.0, 2.36.4, 2.35.7 et 2.34.9, toutes sorties le 1er septembre), et une liste d'identifiants à faire tourner. L'étape cinq va au-delà de tout cela, parce que les serveurs qui diraient qui a été touché appartiennent à l'attaquant. Coder ne peut pas les voir, et le dit : il ne peut pas identifier de façon concluante chaque déploiement compromis.

La question vous revient donc, et c'est une question de preuves. Est-ce que quoi que ce soit sur la machine qui a exécuté ce module s'est connecté à coder-infra[.]com ? Pour un VPC de production, la plupart des équipes savent répondre dans l'heure, parce que quelqu'un de l'équipe plateforme possède déjà les journaux de flux.

Posez maintenant la même question à propos de la machine où travaille votre agent de codage. Un agent lance terraform init, npm install, make, une construction de conteneur, une suite de tests, un script trouvé dans le dépôt, un binaire téléchargé par un module. Chacun de ces gestes ouvre des connexions, et personne ne les note quelque part où vous pourriez les chercher une semaine plus tard. La transcription de l'agent contient les appels d'outils que le modèle a faits. Elle n'a aucune ligne pour ce qu'un script shell à l'intérieur d'un module Terraform a fait avec le réseau, parce que le modèle ne l'a jamais vu se produire.

Vous ne pouvez pas acheter après coup une réponse à l'étape cinq. Soit quelque chose a enregistré la requête au moment où elle quittait la machine, soit il n'y a rien à chercher.

Ce que le script aurait trouvé

Prenez la charge utile au mot et laissez-la s'exécuter. Elle balaie l'environnement à la recherche d'identifiants, lit les clés SSH, attrape les jetons OIDC et d'authentification externe, récupère les variables de configuration qui contiennent le mot de passe de la base, et poste le tout vers coder-infra[.]com.

Bromure Agentic Coding donne à chaque workspace sa propre VM Ubuntu sous l'hyperviseur d'Apple, avec un proxy côté hôte comme unique route de l'invité vers le réseau. Lancez le même balayage là-dedans et il revient avec un jeu complet d'identifiants, tous factices.

ANTHROPIC_API_KEY contient un leurre sk-ant-api03-brm-…, GH_TOKEN une chaîne ghp_ de la bonne longueur, et ~/.git-credentials, ~/.docker/config.json et ~/.kube/config contiennent des substituts assortis. Les formes tiennent, si bien que gh, docker et kubectl les acceptent sans broncher. Le proxy garde vos vraies valeurs chiffrées sur votre Mac et les substitue sur le fil une fois la requête sortie de la VM, uniquement lorsqu'elle est destinée à l'hôte pour lequel cet identifiant a été émis. Aucun interrupteur ne désactive cela ; c'est ainsi que fonctionnent les identifiants dans l'application.

Les clés SSH sont le cas le plus net : il n'y en a aucune à lire dans la VM. SSH_AUTH_SOCK pointe vers un pont au-dessus d'un socket virtuel jusqu'à un agent sur l'hôte, et ce protocole a une requête qui veut dire « signe ce défi » et aucune requête qui veut dire « donne-moi la clé ». ls ~/.ssh/id_* ne renvoie rien pendant que git push continue de fonctionner.

AWS mérite son propre paragraphe, parce que c'est contre AWS que tourne Terraform. Le ~/.aws/config de la VM pointe vers un assistant qui fournit votre véritable identifiant de clé d'accès associé à une clé secrète factice. Terraform, les SDK AWS, boto3 et la CLI aws lisent tous cet assistant sans aucune configuration, puis signent leurs requêtes avec la fausse clé et produisent une signature bien formée qu'AWS rejetterait. L'hôte retire cette signature et resigne avec le vrai secret à la sortie. Un script qui ratisse l'environnement à la recherche de matériel AWS repart avec un identifiant de clé et quarante caractères qui n'authentifient rien.

Ensuite, il doit envoyer son butin quelque part, et c'est là que se trouve le pare-feu de sortie du workspace. Mettez Unmatched traffic sur Deny et la VM atteint les hôtes que vous avez listés et rien d'autre. L'application se fait deux fois, à deux endroits que l'invité ne peut pas atteindre : le commutateur réseau virtuel filtre chaque flux par IP de destination et par nom d'hôte capté dans le DNS, tous protocoles confondus, et le proxy filtre à nouveau par nom de serveur TLS. Personne n'a mis un domaine vieux de trois jours sur cette liste. Les modifications de règles atteignent les sessions en cours sans redémarrage, vous pouvez donc ajouter deny any coder-infra[.]com pendant que le build tourne encore.

Si le trafic sort quand même, parce que le workspace tourne dans la posture par défaut qui autorise tout ou parce que la destination avait l'air ordinaire, les leurres font leur second travail. Le proxy inspecte chaque requête sortante, en-têtes et corps, à la recherche d'un identifiant factice à destination d'un endroit pour lequel il n'a jamais été émis. Une chaîne sk-ant-api03-brm-… adressée à coder-infra[.]com n'a rien d'ambigu. Le proxy la bloque avec un HTTP 451 avant qu'un seul octet n'atteigne la destination, met la VM en pause, et lève une alerte nommant l'identifiant et les deux hôtes. Bromure marque le workspace comme compromis, et il ne redémarrera pas tant que vous n'aurez pas effacé son disque et son home.

Vous obtenez la réponse à l'étape cinq pendant qu'elle se produit, au lieu de la reconstituer à partir de journaux une semaine plus tard.

Sur la machine qui a exécuté le moduleterraform apply → dlp.shs'exécute avec les privilèges du buildce qu'il collecteenv et secrets du provisionneurclés SSH, identifiants d'auth externejetons OIDC des utilisateursconfig coderd — mot de passe de la basesortant : un POST HTTPS depuis un hôte debuild qui fait du HTTPS toute la journée« a-t-on été touché ? » → des journaux absentsDe vrais identifiants sont partis, rien n'a noté la requête.Dans un workspace Bromure Agentic Codingterraform apply → dlp.shs'exécute quand même — en ubuntu, dans l'invitéVM jetable · le proxy hôte est la seule sortiechaque variable d'env et jeton est un leurreaucun octet de clé privée — l'hôte signe via vsockAWS : vrai key ID, faux secret, l'hôte resignepare-feu sortant en refus — rappel refuséun leurre hors de sa portée → HTTP 451,VM en pause, workspace marqué compromischaque requête a sa ligne, quel que soit le processusIl a collecté des leurres ; le proxy a refusé et gardé la ligne.
Le même module, le même dlp.sh, sur deux machines. À gauche, il trouve de vraies variables d'environnement, de vraies clés SSH et de vrais jetons, et son rappel est une requête HTTPS ordinaire que rien n'a consignée. À droite, il trouve des leurres, aucun octet de clé privée et un secret de signature qui n'authentifie rien, son rappel rencontre un pare-feu de sortie en refus par défaut, et un leurre adressé à un hôte hors de sa portée arrête la VM.

Une commande répond à l'avis de sécurité

Le proxy hôte écrit un enregistrement de métadonnées pour chaque requête sortant de la VM, quel que soit le processus qui l'a émise : heure, hôte, port, méthode, chemin, statut, latence et octets mesurés avant toute substitution d'identifiant. Mettez le Session trace du workspace sur Activity only et vous obtenez cet enregistrement sans qu'aucun corps de requête ne soit stocké. Ensuite, bromure-cli trace hostnames my-workspace affiche chaque hôte distinct que le workspace a contacté, avec les compteurs. L'étape cinq de Coder, en une ligne, pour la machine où le code a tourné.

Ce que les moteurs ont décidé

La Security Timeline (Window → Security Timeline…) est la vue distincte de ce que les moteurs de Bromure ont décidé : chaque verdict du pare-feu de sortie, chaque identifiant courtié par le proxy, chaque blocage de chaîne d'approvisionnement, avec un code couleur et des filtres. Une connexion refusée vers un domaine enregistré il y a trois jours y apparaît comme une ligne Firewall rouge pendant que le build tourne encore, pas comme un grep dans un mois de journaux de flux après que l'avis de quelqu'un d'autre vous a dit où regarder.

L'invité peut atteindre une seule chose sur votre Mac : un dossier partagé, une fenêtre en lecture-écriture sur votre vrai système de fichiers que l'effacement pour compromission laisse intacte par conception. Partagez le répertoire du projet plutôt que votre répertoire personnel, et un script qui cherche id_rsa et .env parcourt un home Linux plein de checkouts et de caches de paquets.

Quand on ne peut pas vérifier la source, surveillez la sortie

Toutes les vérifications de provenance se ramènent à un seul geste : comparer ce qui est arrivé à quelque chose en quoi vous avez déjà confiance. Un nom, un éditeur, une signature, une empreinte dans un lockfile. Le geste fonctionne tant que l'attaquant doit déposer quelque chose dans le canal pour que vous ayez de quoi comparer. Un attaquant qui tient un tronçon du canal n'y dépose rien, et le pool d'origine de Coder était un tronçon du canal. La compromission de BdThemes en août avait la même forme, où un bucket de stockage détourné a modifié un flux JSON que 350 000 sites WordPress récupéraient, sans aucune publication et sans aucun fichier modifié.

Quatorze heures, c'est une journée de travail. Coder l'a détecté, a livré des correctifs le lendemain matin, publié des empreintes et des indicateurs, et dit à ses lecteurs quoi vérifier. L'avis se termine tout de même en demandant à chaque lecteur d'aller regarder son propre trafic sortant, parce que l'enregistrement de l'autre côté a disparu.

Faites tourner les clés que l'avis liste, purgez les modules en cache, prenez le correctif. Décidez ensuite ce que la prochaine attaque aura le droit d'atteindre, parce que la prochaine arrivera par un canal que vous n'aviez aucune raison de mettre en doute. Installez Bromure Agentic Coding, donnez au build sa propre machine, et ayez la réponse prête quand l'avis arrivera.