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

Le jeton de l'agent était celui du propriétaire

Le 23 octobre, LiteLLM a publié l'analyse de la cause de sa version 1.107.0 piégée. Le premier identifiant volé ne venait pas d'un journal. Il venait de la propre plateforme d'agents de LiteLLM, dont l'API renvoyait le jeton GitHub que ses agents cloud utilisaient pour ouvrir des pull requests : le jeton d'un propriétaire de l'organisation, sans restriction de portée. La clé de cette API traînait dans un fichier de configuration public depuis quatre mois et demi. Dans Bromure Agentic Coding, un agent qui ouvre des pull requests détient un substitut, le vrai jeton reste chiffré sur le Mac, et aucune API ne le renvoie.

Les agents de LiteLLM avaient besoin d'un jeton GitHub pour ouvrir des pull requests. Ils en ont reçu un qui appartenait à un propriétaire de l'organisation. Un seul appel d'API avec une clé fuitée l'a renvoyé.

Le 23 octobre, LiteLLM a publié la chaîne d'attaque derrière sa version 1.107.0, celle qui est sortie le 21 octobre avec un voleur d'identifiants à l'intérieur. Le billet se présente comme « la source de vérité canonique » et corrige la première hypothèse de LiteLLM. Les jetons GitHub volés ne venaient pas du trafic des modèles journalisé. Le premier venait d'un service expérimental que LiteLLM faisait tourner pour ses propres agents de code.

Nous avons couvert la version elle-même dans La version avait deux heures, en suivant alors la première hypothèse. Le rapport final commence ailleurs. Le premier identifiant que l'attaquant a pris appartenait aux agents.

Une plateforme d'agents avec le jeton d'un propriétaire

LiteLLM Agent Platform était, selon LiteLLM, « un projet expérimental d'agents cloud auto-hébergés chargés de déposer des PR sur des dépôts GitHub ». Pour ouvrir des pull requests, la plateforme détenait un jeton GitHub. Ce jeton « n'était pas associé à un compte de service et n'avait pas de portée restreinte ». C'était le jeton OAuth de la CLI GitHub de l'un des deux propriétaires de l'organisation.

La chronologie du rapport, en UTC :

  • 27 mai, 18 h 53. Un fichier opencode.json est commité dans le dépôt public LiteLLM-Labs/lite-harness. C'est la configuration d'un harnais d'agent, et il contient la clé maîtresse d'un déploiement de l'Agent Platform. L'équipe s'en aperçoit, écrase le commit par un force-push et marque les clés exposées pour révocation. « La révocation n'a pas eu lieu pour la clé maîtresse. »
  • 10 octobre, 21 h 27. Depuis 69.48.229.143, l'attaquant appelle l'API de la plateforme avec cette clé. GET /api/v1/managed_agents/agents?limit=100 renvoie la configuration de chaque agent géré, et avec elle le jeton du propriétaire.
  • 10 octobre, 22 h 11. Avec le jeton du propriétaire, l'attaquant pousse une branche jetable, ci-tmp-a351cc, sur BerriAI/litellm. Son workflow envoie six secrets du dépôt à webhook.site. L'un d'eux, GH_TOKEN, est le jeton de CLI GitHub d'un deuxième mainteneur. Vingt-quatre secondes plus tard, une seconde branche dans BerriAI/litellm-auto envoie le jeton d'un troisième mainteneur au même point de terminaison. Les deux exécutions sont supprimées en moins de trois minutes.
  • 12 octobre. D'autres workflows sur des dépôts privés envoient tous les secrets de dépôt qu'ils peuvent atteindre.
  • 21 octobre, 10 h 08. Un push vers le dépôt de publication avec le jeton du deuxième mainteneur est refusé par la protection de branche. Cinq secondes plus tard, le jeton du propriétaire désactive la protection et force le push du changement, qui fait pointer le workflow de publication vers le sosie Berri-AI/litellm.
  • 21 octobre, 11 h 11. Le jeton du deuxième mainteneur approuve la porte de production. Une règle qui exigeait l'approbation d'un compte lié à un appareil dans les bureaux de LiteLLM avait été assouplie cette semaine-là, si bien que deux ingénieurs quelconques pouvaient publier.
  • 21 octobre, de 11 h 39 à 11 h 46. Sept wheels arrivent sur PyPI, puis les images Docker Hub et GHCR.
  • 21 octobre, 11 h 53. Le jeton du troisième mainteneur supprime les traces de l'exécution de publication. À 12 h 24, le jeton du propriétaire remet main au propre par un force-push.

Le résumé de LiteLLM : « Cet incident illustre de façon frappante les risques qu'il y a à ne pas renouveler une clé potentiellement exposée. » L'entreprise révoque désormais chaque jeton lié à chaque système interne et à chaque plateforme de fournisseur.

Chaque flotte d'agents détient un jeton comme celui-là

LiteLLM dispose d'une équipe sécurité compétente, et sa configuration était courante. Un agent qui ouvre des pull requests a besoin d'un jeton qui peut pousser. Le jeton le plus facile à fournir est votre propre session gh, et c'est celui que les agents de LiteLLM avaient reçu.

Ensuite, le jeton doit vivre quelque part où les agents peuvent le lire : une variable d'environnement, un fichier de configuration, une ligne de base de données derrière une API qui configure les agents. Quiconque peut lire cet endroit a le jeton. Ici, une clé d'API qui avait fuité une fois, et n'avait jamais été renouvelée, a suffi.

Les jetons de la CLI GitHub aggravent les choses. LiteLLM note qu'ils « sont post-2FA et ne sont pas soumis aux flux de réauthentification 2FA “super-utilisateur” de GitHub associés aux actions sensibles sur l'organisation ou le dépôt ». Avec l'un d'eux, l'attaquant pouvait couper la protection de branche, forcer un push, approuver une publication et effacer les preuves.

Cherchez les mêmes pièces dans votre propre installation : un GH_TOKEN dans l'environnement de l'agent, une configuration MCP contenant un jeton bearer, une configuration de harnais que quelqu'un commitera un jour. Le opencode.json de LiteLLM contenait plus que la clé de la plateforme. Il portait aussi les jetons bearer de ses serveurs MCP GitHub, Linear et Slack, ainsi que la clé de sa passerelle de modèles, le tout en clair.

La même flotte sur une machine Bromure

Bromure Agentic Coding fait tourner Claude Code, Codex, Grok, Kimi et omp dans des VM Linux sur votre Mac. Ses automatisations font ce que faisait la plateforme d'agents de LiteLLM : un agent se déclenche selon un horaire ou à l'arrivée d'une nouvelle issue GitHub, travaille dans un worktree neuf, et pousse une branche ou ouvre une pull request. Rejouez la chaîne de LiteLLM contre une machine qui fait tourner ces automatisations.

L'agent détient un substitut

Ajoutez le jeton GitHub à la machine sous Credentials. Au démarrage, Bromure écrit un faux dans la VM : une valeur ghp_ de la bonne longueur, dans GH_TOKEN, dans la configuration de gh et dans ~/.git-credentials. Les commandes gh pr create et git push de l'agent fonctionnent normalement. Quand une requête quitte la VM vers github.com, le proxy du Mac remplace le faux par le vrai jeton, et uniquement pour cet hôte.

Le vrai jeton est stocké chiffré sur le Mac, sous une clé conservée dans le trousseau macOS. Il n'est jamais écrit dans la VM, ni dans un fichier ni dans une variable. Le manuel dit du canal entre l'hôte et la VM : « Il n'existe aucune requête par laquelle la VM puisse redemander un vrai jeton. »

L'étape qui a ouvert l'organisation de LiteLLM n'a donc rien à renvoyer. Videz l'environnement de l'agent, lisez sa configuration, listez chaque automatisation par l'API : partout, la valeur GitHub est le substitut. Collez-la dans votre propre gh et GitHub la rejette, car seul le proxy de cette machine sait ce qu'elle remplace.

La configuration commitée contient aussi des substituts

La fuite a commencé par une configuration de harnais dans un dépôt public. Sur une machine Bromure, les fichiers de configuration que lit un agent sont écrits avec des substituts. Le jeton bearer d'un serveur MCP, défini dans le panneau MCP de la machine, parvient à l'agent sous la forme d'une valeur brm-mcp_…, remplacée sur le fil pour les requêtes vers l'hôte de ce serveur. Une clé de modèle définie dans Preferences › Models n'entre jamais dans la VM ; l'agent voit un faux au format du fournisseur. Toute autre clé d'API va sous Other API keys avec un filtre d'hôte, et la VM reçoit une valeur brm_… dans la variable que vous nommez.

Quand un agent commite sa configuration par erreur, ou qu'un développeur la copie dans un dépôt de démonstration, le fichier contient des substituts. Rien de ce qu'il contient ne fonctionne hors de ce Mac : ce n'est donc pas un force-push ni une liste de révocations que personne ne termine qui décident si vous êtes compromis.

Il n'y a pas d'API de plateforme à appeler

L'attaquant de LiteLLM a atteint l'API de la plateforme depuis Internet. Les automatisations de Bromure tournent sur votre Mac. Selon le manuel : « il n'y a aucun service cloud et aucune surface réseau entrante ». Les déclencheurs GitHub et Linear sont interrogés depuis le Mac toutes les trois minutes avec le jeton enregistré de la machine, qui reste sur l'hôte. Il n'y a pas de point de terminaison webhook, pas de clé maîtresse et pas de route /managed_agents qu'une clé fuitée pourrait ouvrir.

Un substitut volé déclenche une alarme

Les workflows du 10 octobre envoyaient des secrets à webhook.site. Imaginez le même geste visant une machine Bromure : une dépendance piégée ou une instruction injectée dans un prompt lit GH_TOKEN et l'envoie à webhook.site.

Le proxy inspecte chaque requête sortante, en-têtes et corps, à la recherche de substituts dirigés ailleurs que vers l'hôte pour lequel ils ont été émis. Un substitut ghp_ en route vers webhook.site est bloqué avec un HTTP 451 avant qu'un seul octet ne sorte. La VM se met en pause, et une alerte sur le Mac affiche This environment may have been compromised, avec l'aperçu du jeton, le nom de l'identifiant, l'hôte auquel il appartient et l'hôte vers lequel il a été envoyé. Vous pouvez éteindre la machine, ou conserver son disque et son dossier personnel pour enquête.

Avec Unmatched traffic réglé sur Deny dans la politique de sandbox de la machine, webhook.site n'est même pas joignable, et la tentative apparaît comme une ligne rouge dans la Security Timeline.

Un push vous attend

Les nouvelles machines Bromure démarrent avec la politique d'écriture de GitHub réglée sur Prompt before write. Les lectures passent. Un git push ou toute autre écriture vers github.com s'arrête sur une boîte de dialogue sur votre Mac :

Allow write on "GitHub" from workspace "release-bot"?
POST /repos/<owner>/<repo>/git/refs

Les gestes de l'attaquant étaient tous des écritures : les branches ci-tmp-, la modification du workflow de publication, le force-push sur main, la suppression des traces d'exécution. Depuis une machine Bromure, chacun se serait arrêté sur cette boîte de dialogue. Allow once n'accorde rien au-delà de cette seule écriture, donc le push suivant redemande. Un force-push reste un push sur le fil, et Prompt before write le contrôle.

Une automatisation sans surveillance ne peut pas attendre un clic : donnez lui donc le jeton le plus étroit qui fasse son travail, sous une politique propre à cette machine. Le jeton que vous ajoutez à une machine release-bot peut être un jeton à granularité fine limité à un seul dépôt. Votre propre session gh, avec des droits de propriétaire sur tout, n'a aucune raison de se trouver sur cette machine.

L'historique est sur votre Mac

LiteLLM a rapproché des empreintes de jetons dans le journal d'audit de GitHub pour savoir quel jeton volé avait fait quoi. Sur une machine Bromure, chaque substitution d'un identifiant sur le fil apparaît comme une ligne Credential brokering dans la Security Timeline, avec le jeton masqué et l'hôte. Chaque écriture que vous refusez est une ligne rouge Guardrails avec la méthode et le chemin. Sur un Mac inscrit auprès de bromure.io, les mêmes événements remontent à votre organisation : un jeton utilisé depuis une machine inattendue apparaît sans que personne ait à fouiller des journaux d'audit.

D'un fichier de configuration à une version signéeLiteLLM, du 27 mai au 21 octobre1 · Clé dans une configopencode.json dans un dépôtpublic, jamais révoquée2 · L'API rend le jetonla config d'agent contient lejeton gh d'un propriétaire3 · Secrets exfiltrésdes branches ci-tmp envoientdeux jetons à webhook.site4 · Pushs et publicationprotection coupée, force-push,porte validée, traces effacéesSur une machine BromureSubstituts en configles clés MCP, modèle et APIde la VM sont fausses. Uncommit ne fuite rien.Aucun jeton à renvoyerpas d'API entrante ; l'agenta un substitut ghp_ queGitHub rejette ailleurs.Bloqué, VM en pauseun substitut destiné àwebhook.site reçoit un 451et une alerte.Prompt before writechaque push, force-pushet écriture d'API attendun dialogue sur le Mac.
La chaîne d'attaque de LiteLLM, et l'endroit où une machine Bromure faisant tourner les mêmes agents arrête chaque étape. Chaque contrôle s'exécute sur le Mac, hors de la VM.

Les agents reçoivent des substituts

GH_TOKEN, la configuration de gh et ~/.git-credentials contiennent un faux ghp_. Le vrai jeton est substitué sur le Mac, uniquement pour les hôtes de GitHub.

Aucune API ne distribue de jetons

Les automatisations tournent sur votre Mac sans surface entrante. Rien de ce que la VM peut appeler ne renvoie un vrai identifiant.

Les fuites mettent la machine en pause

Un substitut envoyé ailleurs que vers son propre hôte est bloqué avec un 451, et la VM se met en pause le temps que vous décidiez.

Les écritures demandent d'abord

Les nouvelles machines demandent avant chaque push et chaque écriture vers GitHub, force-push compris, avec la requête exacte à l'écran.

Ne donnez pas du tout le jeton à l'agent

LiteLLM révoque tous ses jetons et publiera ses changements de processus la semaine prochaine. Le renouvellement, les portées étroites et les comptes de service aident tous. Chacun laisse pourtant un jeton fonctionnel quelque part où un agent peut le lire, et c'est ce que l'attaquant est allé chercher le 10 octobre.

Sur une machine Bromure, l'agent fait son travail avec un jeton qui ne fonctionne qu'à travers son propre proxy, vers le seul hôte pour lequel il a été émis. Tout ce que l'agent peut lire, un attaquant qui atteint l'agent peut le lire aussi, et sur une machine Bromure, cela n'inclut jamais le vrai jeton.

Pour voir comment ces mêmes substituts résistent à un voleur qui cible les fichiers des agents de code, lisez Le stealer a emporté les substituts. Pour les copies qu'un agent laisse dans ses propres transcriptions, lisez Le jeton est resté dans la transcription.