← Voltar para todas as publicações
Publicado em · por Renaud Deraison

O token do agente era o do proprietário

Em 23 de outubro, a LiteLLM publicou a causa raiz da sua versão 1.107.0 envenenada. A primeira credencial roubada não veio de um log. Veio da própria plataforma de agentes da LiteLLM, cuja API devolvia o token do GitHub que seus agentes na nuvem usavam para abrir pull requests: o token de um proprietário da organização, sem escopo restrito. A chave dessa API estava havia quatro meses e meio num arquivo de configuração público. No Bromure Agentic Coding, um agente que abre pull requests tem um substituto, o token real fica criptografado no Mac e nenhuma API o devolve.

Os agentes da LiteLLM precisavam de um token do GitHub para abrir pull requests. Receberam um que pertencia a um proprietário da organização. Uma única chamada de API com uma chave vazada o devolveu.

Em 23 de outubro, a LiteLLM publicou a cadeia de ataque por trás da sua versão 1.107.0, a que saiu em 21 de outubro com um ladrão de credenciais dentro. O texto se apresenta como «a fonte canônica da verdade» e corrige a primeira teoria da LiteLLM. Os tokens do GitHub roubados não vieram do tráfego de modelos registrado em log. O primeiro veio de um serviço experimental que a LiteLLM mantinha para seus próprios agentes de programação.

Cobrimos a versão em si em A versão tinha duas horas, e ali seguimos a primeira teoria. O relatório final começa em outro lugar. A primeira credencial que o atacante levou pertencia aos agentes.

Uma plataforma de agentes com o token de um proprietário

A LiteLLM Agent Platform era, nas palavras da LiteLLM, «um projeto experimental de agentes em nuvem auto-hospedados para abrir PRs em repositórios do GitHub». Para abrir pull requests, a plataforma tinha um token do GitHub. Esse token «não estava associado a uma conta de serviço e não tinha escopo restrito». Era o token OAuth da CLI do GitHub de um dos dois proprietários da organização.

A linha do tempo do relatório, em UTC:

  • 27 de maio, 18:53. Um arquivo opencode.json é commitado no repositório público LiteLLM-Labs/lite-harness. É a configuração de um harness de agentes e contém a chave mestra de uma implantação da Agent Platform. A equipe percebe, sobrescreve o commit com um force-push e marca as chaves expostas para revogação. «A revogação não aconteceu para a chave mestra.»
  • 10 de outubro, 21:27. A partir de 69.48.229.143, o atacante chama a API da plataforma com essa chave. GET /api/v1/managed_agents/agents?limit=100 devolve a configuração de cada agente gerenciado e, com ela, o token do proprietário.
  • 10 de outubro, 22:11. Com o token do proprietário, o atacante envia um branch descartável, ci-tmp-a351cc, para BerriAI/litellm. O workflow dele envia seis segredos do repositório para webhook.site. Um deles, GH_TOKEN, é o token da CLI do GitHub de um segundo mantenedor. Vinte e quatro segundos depois, um segundo branch em BerriAI/litellm-auto envia o token de um terceiro mantenedor para o mesmo endpoint. As duas execuções são apagadas em menos de três minutos.
  • 12 de outubro. Mais workflows em repositórios privados enviam todos os segredos de repositório que conseguem alcançar.
  • 21 de outubro, 10:08. Um push para o repositório de release com o token do segundo mantenedor é rejeitado pela proteção de branch. Cinco segundos depois, o token do proprietário desativa a proteção e força o push da mudança, que aponta o workflow de release para o imitador Berri-AI/litellm.
  • 21 de outubro, 11:11. O token do segundo mantenedor aprova o portão de produção. Uma regra que exigia a aprovação de uma conta vinculada a um dispositivo no escritório da LiteLLM tinha sido afrouxada naquela semana, de modo que dois engenheiros quaisquer podiam publicar.
  • 21 de outubro, das 11:39 às 11:46. Sete wheels chegam ao PyPI, depois as imagens no Docker Hub e no GHCR.
  • 21 de outubro, 11:53. O token do terceiro mantenedor apaga os registros da execução de release. Às 12:24, o token do proprietário deixa a main limpa com um force-push.

O resumo da própria LiteLLM: «Este incidente forneceu uma ilustração contundente dos riscos de não rotacionar uma chave potencialmente exposta.» A empresa agora está revogando todos os tokens ligados a cada sistema interno e a cada plataforma de fornecedor.

Toda frota de agentes tem um token assim

A LiteLLM tem uma equipe de segurança competente, e sua configuração era comum. Um agente que abre pull requests precisa de um token que possa fazer push. O token mais à mão é o seu próprio login do gh, e foi esse que os agentes da LiteLLM receberam.

Depois, o token precisa morar em algum lugar onde os agentes possam lê-lo: uma variável de ambiente, um arquivo de configuração, uma linha de banco de dados atrás de uma API que configura os agentes. Quem consegue ler esse lugar tem o token. Aqui bastou uma chave de API que vazou uma vez e nunca foi rotacionada.

Tokens da CLI do GitHub pioram a situação. A LiteLLM observa que eles «são emitidos após o 2FA e não estão sujeitos aos fluxos de reautenticação 2FA de “superusuário” do GitHub associados a ações sensíveis na organização ou no repositório». Com um deles, o atacante podia desligar a proteção de branch, forçar um push, aprovar um release e apagar as evidências.

Procure as mesmas peças na sua própria configuração: um GH_TOKEN no ambiente do agente, uma configuração MCP com um token bearer dentro, uma configuração de harness que alguém um dia vai commitar. O opencode.json da LiteLLM continha mais do que a chave da plataforma. Também trazia tokens bearer para seus servidores MCP do GitHub, do Linear e do Slack e a chave do seu gateway de modelos, tudo em texto puro.

A mesma frota numa máquina Bromure

O Bromure Agentic Coding roda Claude Code, Codex, Grok, Kimi e omp em VMs Linux no seu Mac. Suas automações fazem o que a plataforma de agentes da LiteLLM fazia: um agente dispara num horário ou a partir de uma nova issue do GitHub, trabalha num worktree novo e envia um branch ou abre um pull request. Rode a cadeia da LiteLLM contra uma máquina que executa essas automações.

O agente tem um substituto

Adicione o token do GitHub à máquina em Credentials. Na inicialização, o Bromure grava um falso na VM: um valor ghp_ com o comprimento certo, em GH_TOKEN, na configuração do gh e em ~/.git-credentials. Os gh pr create e git push do agente funcionam normalmente. Quando uma requisição sai da VM para github.com, o proxy no Mac troca o falso pelo token real, e só para esse host.

O token real fica criptografado no Mac, sob uma chave guardada nas Chaves do macOS. Ele nunca é gravado na VM, em nenhum arquivo nem variável. O manual diz sobre o canal entre o host e a VM: «Não existe requisição pela qual a VM possa pedir um token real de volta.»

Então o passo que abriu a organização da LiteLLM não tem nada a devolver. Despeje o ambiente do agente, leia a configuração dele, liste todas as automações pela API: o valor do GitHub em todas elas é o substituto. Cole-o no seu próprio gh e o GitHub o rejeita, porque só o proxy daquela máquina sabe o que ele representa.

A configuração commitada também tem substitutos

O vazamento começou com uma configuração de harness num repositório público. Numa máquina Bromure, os arquivos de configuração que um agente lê são gravados com substitutos. O token bearer de um servidor MCP, definido no painel MCP da máquina, chega ao agente como um valor brm-mcp_…, trocado em trânsito nas requisições para o host desse servidor. Uma chave de modelo definida em Preferences › Models nunca entra na VM; o agente vê um falso no formato do provedor. Qualquer outra chave de API vai em Other API keys com um filtro de host, e a VM recebe um valor brm_… na variável que você nomear.

Quando um agente commita a configuração por engano, ou um desenvolvedor a copia para um repositório de demonstração, o arquivo leva substitutos. Nada nele funciona fora daquele Mac, então nem um force-push nem uma lista de revogação que ninguém termina decidem se você é invadido.

Não há API de plataforma para chamar

O atacante da LiteLLM chegou à API da plataforma pela internet. As automações do Bromure rodam no seu Mac. Nas palavras do manual: «não há nenhum serviço em nuvem nem nenhuma superfície de rede de entrada». Os gatilhos do GitHub e do Linear são consultados a partir do Mac a cada três minutos com o token armazenado da máquina, que fica no host. Não há endpoint de webhook, nem chave mestra, nem rota /managed_agents para uma chave vazada abrir.

Um substituto roubado dispara um alarme

Os workflows de 10 de outubro enviavam segredos para webhook.site. Imagine o mesmo movimento contra uma máquina Bromure: uma dependência envenenada ou uma instrução injetada num prompt lê GH_TOKEN e o envia para webhook.site.

O proxy examina cada requisição de saída, cabeçalhos e corpo, em busca de substitutos indo para um host diferente daquele para o qual foram emitidos. Um substituto ghp_ a caminho de webhook.site é bloqueado com HTTP 451 antes que um único byte saia. A VM pausa, e um alerta no Mac mostra This environment may have been compromised, com a prévia do token, o nome da credencial, o host ao qual ela pertence e o host para o qual foi enviada. Você pode desligar a máquina ou guardar o disco e a pasta pessoal dela para investigação.

Com Unmatched traffic em Deny na política de sandbox da máquina, webhook.site nem chega a ser alcançável, e a tentativa aparece como uma linha vermelha na Security Timeline.

Um push espera por você

Máquinas Bromure novas começam com a política de escrita do GitHub em Prompt before write. Leituras passam. Um git push ou qualquer outra escrita em github.com para num diálogo no seu Mac:

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

Os movimentos do atacante eram todos escritas: os branches ci-tmp-, a mudança no workflow de release, o force-push na main, a exclusão dos registros de execução. A partir de uma máquina Bromure, cada um teria parado nesse diálogo. Allow once não concede nada além daquela única escrita, então o próximo push pergunta de novo. Um force-push continua sendo um push em trânsito, e Prompt before write o controla.

Uma automação sem supervisão não pode esperar um clique, então dê a ela o token mais restrito que faça o trabalho, sob uma política própria daquela máquina. O token que você adiciona a uma máquina release-bot pode ser um token de granularidade fina para um único repositório. Seu próprio login do gh, com direitos de proprietário sobre tudo, não tem motivo para estar naquela máquina.

O registro está no seu Mac

A LiteLLM cruzou hashes de tokens no log de auditoria do GitHub para saber qual token roubado fez o quê. Numa máquina Bromure, cada troca de uma credencial em trânsito aparece como uma linha Credential brokering na Security Timeline, com o token mascarado e o host. Cada escrita que você recusa é uma linha vermelha Guardrails com o método e o caminho. Num Mac registrado no bromure.io, os mesmos eventos chegam à sua organização, então um token usado a partir de uma máquina inesperada aparece sem que ninguém precise vasculhar logs de auditoria.

De um arquivo de configuração a um release assinadoLiteLLM, de 27 de maio a 21 de outubro1 · Chave numa configopencode.json num repopúblico, nunca revogada2 · API devolve o tokena config do agente tem otoken gh de um proprietário3 · Segredos enviadosbranches ci-tmp enviam maisdois tokens a webhook.site4 · Pushes e releaseproteção fora, force-push,portão aprovado, logs apagadosNuma máquina BromureSubstitutos nas configsas chaves MCP, de modelo eAPI na VM são falsas. Umcommit não vaza nada.Nenhum token a darsem API de entrada; o agentetem um substituto ghp_ queo GitHub rejeita fora dali.Bloqueado, VM pausadaum substituto rumo awebhook.site recebe um 451e um alerta.Prompt before writecada push, force-pushe escrita de API esperaum diálogo no Mac.
A cadeia de ataque da LiteLLM, e onde uma máquina Bromure rodando os mesmos agentes interrompe cada passo. Cada verificação roda no Mac, fora da VM.

Agentes recebem substitutos

GH_TOKEN, a configuração do gh e ~/.git-credentials guardam um ghp_ falso. O token real é trocado no Mac, só para os hosts do GitHub.

Nenhuma API entrega tokens

As automações rodam no seu Mac sem superfície de entrada. Nada que a VM possa chamar devolve uma credencial real.

Vazamentos pausam a máquina

Um substituto enviado para qualquer lugar que não seja o próprio host é bloqueado com um 451, e a VM pausa enquanto você decide.

Escritas perguntam antes

Máquinas novas perguntam antes de cada push e de cada escrita no GitHub, force-pushes incluídos, com a requisição exata na tela.

Não dê o token ao agente

A LiteLLM está revogando todos os tokens e vai publicar suas mudanças de processo na semana que vem. Rotação, escopos restritos e contas de serviço ajudam. Cada uma dessas medidas ainda deixa um token funcional em algum lugar onde um agente pode lê-lo, e foi isso que o atacante foi procurar em 10 de outubro.

Numa máquina Bromure, o agente faz o trabalho com um token que só funciona através do próprio proxy, rumo ao único host para o qual foi emitido. Tudo o que o agente pode ler, um atacante que alcança o agente também pode, e numa máquina Bromure isso nunca inclui o token real.

Para ver como os mesmos substitutos resistem a um stealer que mira os arquivos de agentes de programação, leia O stealer levou os substitutos. Para as cópias que um agente deixa nas próprias transcrições, leia A transcrição guardou o token.