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

Eles mudaram o endereço, não o pacote

Durante quatorze horas, em 31 de agosto de 2026, algumas requisições a registry.coder.com foram respondidas por servidores que um invasor havia adicionado ao próprio pool de origem Cloudflare da Coder. O hostname estava certo, o certificado era válido, os módulos tinham os nomes e as versões de sempre, e o código que voltou saiu à procura de chaves SSH, tokens OIDC e senhas de banco de dados. A Coder não tem os logs do invasor, então o comunicado pede que você examine o próprio tráfego de saída. Essa é uma pergunta que a maioria das máquinas rodando um agente de codificação não consegue responder.

O hostname era da Coder. O certificado era válido. O módulo trazia o nome e a versão que o seu template já fixava. Durante quatorze horas, em 31 de agosto, alguns dos servidores que respondiam pertenciam a outra pessoa.

Uma engenheira de plataforma atualiza um template de workspace numa manhã de segunda-feira. O terraform init busca os módulos que o template nomeia (Docker, Zed, aider) em registry.coder.com, o mesmo lugar de onde vieram no mês passado. O Terraform imprime seu muro de verde habitual e o build passa. Nenhuma ferramenta sinaliza nada, nem naquela manhã nem naquele mês.

Em 1º de setembro, a Coder publicou o GHSA-vx42-ghc9-gw65, classificado como crítico, CVSS 9.0. Nas palavras dele: “Um agente malicioso não identificado obteve acesso à infraestrutura Cloudflare da Coder e adicionou endereços IP não autorizados ao pool usado pelo registro de módulos da Coder.” Entre 07:35 e 21:45 UTC da segunda-feira, 31 de agosto, algumas requisições por módulos da Coder foram servidas por máquinas sob controle do invasor. A Coder constrói ambientes de desenvolvimento em nuvem auto-hospedados; a BleepingComputer lista Dropbox, Palantir, Square, Mercedes-Benz, KKR, EnBW, órgãos do governo dos EUA e empreiteiras de defesa entre seus usuários.

Ninguém tocou no pacote

Todo hábito de cadeia de suprimentos que um time cuidadoso tem é uma checagem do artefato. O nome está escrito certo. A publicadora é quem diz ser. Esta versão foi publicada ontem. O hash do lockfile ainda bate. Leia o diff antes de subir a versão.

O invasor mudou quais máquinas respondiam quando o seu cliente perguntou pelo endereço, e deixou o artefato em paz. Ele não publicou pacote nenhum, não assumiu conta de mantenedor nenhuma, não registrou nome sósia nenhum e não editou nada no registro da Coder. O nome resolveu do jeito que sempre resolve. O handshake TLS se completou contra um certificado válido para o hostname verdadeiro. O módulo se chamava docker, e zed, e aider, porque esses são os módulos que um template da Coder pede, e trazia o número de versão que o template já fixava.

O que voltou era diferente. As cópias envenenadas embarcavam um script de shell chamado dlp.sh, com um build próprio para os módulos docker, aider, rstudio-server, windows-rdp e zed, e um hash próprio para cada um no comunicado. O comunicado descreve o payload como código “projetado para identificar uma série de credenciais e exfiltrá-las para um servidor remoto com um nome de domínio parecido com um que a Coder usa regularmente.” Esse domínio é coder-infra[.]com, registrado em 28 de agosto, três dias antes de alguém usá-lo, resolvendo para 199.91.220.205, com o callback em http://www[.]coder-infra[.]com/cli/check.

A lista do que ele procurou parece um inventário da confiança de uma plataforma de desenvolvimento: variáveis de ambiente e segredos do provisionador, tokens OIDC de usuários, chaves SSH, credenciais de autenticação externa e variáveis de configuração do coderd, incluindo senhas de banco de dados e credenciais do provedor de autenticação.

Você não precisava fazer nada fora do comum para estar dentro da janela. Segundo o comunicado: “Usuários que usam o registro principal de módulos da Coder (registry.coder.com) como origem dos seus templates e que atualizaram componentes durante o período em que este código esteve disponível podem ter instalado os pacotes maliciosos.” Criar um template, subir a versão de um template, rodar um dry run ou construir um workspace com o cache de módulos desligado é trabalho comum de segunda-feira.

31 de agosto, 07:35–21:45 UTC — a janela de quatorze horaso build pedeterraform inittemplate novo, bump deversão ou um dry runo nome resolveregistry.coder.comCloudflare, a porta deentrada verdadeiraum IP no pooladicionado pelo invasor,servindo módulo alteradodlp.sho callbackcoder-infra[.]comregistrado em 28/08,usado em 31/08O que o cliente checou, e o que encontrouo hostnameo da própria Coder, nãoum domínio sósiao certificadoválido, para o nome real,na borda verdadeirao nome do módulodocker, zed, aider —os que você pediua versãoa que já estava notemplate. Nada novoAs quatro checagens passam. Qual servidor respondeu é a única coisa que nenhuma checagem no cliente olha.
O invasor adicionou os próprios endereços IP ao pool de origem por trás de registry.coder.com, então o hostname, o certificado, o nome do módulo e a versão fixada eram todos genuínos enquanto os bytes vinham de outro lugar. Um build envenenado difere de um limpo em uma única coisa observável: uma conexão de saída para um domínio registrado três dias antes.

“Revise os logs de firewall, proxy, DNS e fluxo da VPC”

Esse é o passo cinco da remediação da Coder, e a linha mais instrutiva do comunicado. Os passos de um a quatro são coisas com que a Coder pode ajudar: SQL para achar módulos em cache da janela, SQL para expurgá-los, releases corrigidas (2.37.0, 2.36.4, 2.35.7 e 2.34.9, todas no ar em 1º de setembro) e uma lista de credenciais para rotacionar. O passo cinco vai além de tudo isso, porque os servidores que diriam quem foi atingido pertencem ao invasor. A Coder não consegue vê-los, e diz exatamente isso: não pode identificar de forma conclusiva cada implantação comprometida.

Então a pergunta volta para você, e é uma pergunta sobre evidência. Alguma coisa na máquina que rodou aquele módulo se conectou a coder-infra[.]com? Para uma VPC de produção, a maioria dos times responde dentro de uma hora, porque alguém do time de plataforma já é dono dos logs de fluxo.

Agora faça a mesma pergunta sobre a máquina onde o seu agente de codificação trabalha. Um agente roda terraform init, npm install, make, um build de contêiner, uma suíte de testes, um script que ele achou no repositório, um binário que um módulo baixou. Cada um desses abre conexões, e ninguém está anotando essas conexões em algum lugar onde você pudesse buscá-las uma semana depois. O transcript do próprio agente guarda as chamadas de ferramenta que o modelo fez. Ele não tem linha nenhuma para o que um script de shell dentro de um módulo Terraform fez com a rede, porque o modelo nunca viu aquilo acontecer.

Você não consegue comprar uma resposta para o passo cinco depois do fato. Ou alguma coisa registrou a requisição no momento em que ela saiu da máquina, ou não há nada para buscar.

O que o script teria encontrado

Leve o payload a sério e deixe ele rodar. Ele varre o ambiente atrás de credenciais, lê as chaves SSH, pega os tokens OIDC e de autenticação externa, recolhe as variáveis de configuração com a senha do banco dentro e posta tudo para coder-infra[.]com.

O Bromure Agentic Coding dá a cada workspace sua própria VM Ubuntu sob o hipervisor da Apple, com um proxy do lado do host como única rota do guest para a rede. Rode a mesma varredura ali e ela volta com um conjunto completo de credenciais, todas falsas.

ANTHROPIC_API_KEY guarda uma isca sk-ant-api03-brm-…, GH_TOKEN uma string ghp_ do tamanho certo, e ~/.git-credentials, ~/.docker/config.json e ~/.kube/config guardam substitutos correspondentes. Os formatos sobrevivem, então gh, docker e kubectl aceitam tudo sem reclamar. O proxy mantém seus valores reais criptografados no seu Mac e os troca no fio depois que a requisição saiu da VM, só quando ela vai para o host para o qual aquela credencial foi emitida. Nenhuma chave desliga isso; é assim que as credenciais funcionam no aplicativo.

As chaves SSH são o caso mais limpo: não há nenhuma na VM para ler. O SSH_AUTH_SOCK aponta para uma ponte sobre um socket virtual até um agente no host, e aquele protocolo tem uma requisição que significa “assine este desafio” e nenhuma requisição que signifique “me entregue a chave”. ls ~/.ssh/id_* volta vazio enquanto o git push continua funcionando.

A AWS merece parágrafo próprio, porque é contra ela que o Terraform roda. O ~/.aws/config da VM aponta para um helper que entrega o seu access key ID real pareado com uma secret key falsa. Terraform, os SDKs da AWS, o boto3 e a CLI aws leem esse helper sem nenhuma configuração, depois assinam suas requisições com a falsa e produzem uma assinatura bem formada que a AWS rejeitaria. O host remove essa assinatura e reassina com o segredo real na saída. Um script raspando o ambiente atrás de material da AWS vai embora com um key ID e quarenta caracteres que não autenticam nada.

Aí ele precisa mandar o butim para algum lugar, e é onde fica o firewall de saída do workspace. Ponha Unmatched traffic em Deny e a VM alcança os hosts que você listou e mais nada. A aplicação acontece duas vezes, em dois lugares que o guest não alcança: o switch de rede virtual casa cada fluxo por IP de destino e por hostname observado no DNS, em todos os protocolos, e o proxy casa de novo pelo server name do TLS. Ninguém pôs um domínio de três dias nessa lista. Edições de regra chegam a sessões em execução sem reboot, então você pode adicionar deny any coder-infra[.]com com o build ainda rodando.

Se o tráfego sair mesmo assim, porque o workspace roda na postura padrão que permite tudo ou porque o destino pareceu comum, as iscas fazem seu segundo trabalho. O proxy varre cada requisição de saída, cabeçalhos e corpo, atrás de uma credencial falsa indo para um lugar para o qual ela nunca foi emitida. Uma string sk-ant-api03-brm-… endereçada a coder-infra[.]com não é ambígua. O proxy a bloqueia com HTTP 451 antes de um byte chegar ao destino, pausa a VM e levanta um alerta nomeando a credencial e os dois hosts. O Bromure marca o workspace como comprometido, e ele não sobe de novo até você apagar o disco e o home dele.

Você recebe a resposta do passo cinco enquanto ele acontece, em vez de reconstruí-la a partir de logs uma semana depois.

Na máquina que rodou o móduloterraform apply → dlp.shroda com os privilégios do próprio buildo que ele coletaenv vars e segredos do provisionadorchaves SSH, credenciais de auth externatokens OIDC de usuáriosconfig do coderd — senha do bancosaída: um POST HTTPS de um host de buildque faz requisições HTTPS o dia inteiro“fomos atingidos?” → logs que talvez não existamCredenciais reais saíram, e nada no host anotou a requisição.Num workspace do Bromure Agentic Codingterraform apply → dlp.shainda roda — como ubuntu, dentro do guestVM descartável · o proxy do host é a única saídatoda env var e token de config é uma iscanenhum byte de chave privada — o host assina via vsockAWS: key ID real, secret falso, o host reassinafirewall de saída nega por padrão — callback recusadouma isca fora do seu escopo → HTTP 451,VM pausada, workspace marcado comprometidotoda requisição vira uma linha, seja qual for o processoColetou iscas, o proxy recusou o callback e guardou a linha.
O mesmo módulo, o mesmo dlp.sh, em duas máquinas. À esquerda ele encontra variáveis de ambiente reais, chaves SSH reais e tokens reais, e seu callback é uma requisição HTTPS comum que ninguém anotou. À direita ele encontra iscas, nenhum byte de chave privada e um segredo de assinatura que não autentica nada, seu callback encontra um firewall de saída que nega por padrão, e uma isca endereçada a um host fora do seu escopo para a VM.

Um comando responde ao comunicado

O proxy do host escreve um registro de metadados para cada requisição que sai da VM, seja qual for o processo que a fez: hora, host, porta, método, caminho, status, latência e bytes medidos antes de qualquer troca de credencial. Ponha o Session trace do workspace em Activity only e você recebe esse registro sem nenhum corpo de requisição armazenado. Aí bromure-cli trace hostnames my-workspace imprime cada host distinto que o workspace contatou, com contagens. O passo cinco da Coder, em uma linha, para a máquina onde o código rodou.

O que os motores decidiram

A Security Timeline (Window → Security Timeline…) é a visão separada do que os motores do Bromure decidiram: cada veredito do firewall de saída, cada credencial que o proxy intermediou, cada bloqueio de cadeia de suprimentos, com cores e filtros. Uma conexão recusada para um domínio registrado três dias atrás aparece ali como uma linha vermelha de Firewall enquanto o build ainda roda, não como um grep em um mês de logs de fluxo depois que o comunicado de outra pessoa te diz onde olhar.

O guest alcança uma única coisa no seu Mac: uma pasta compartilhada, uma janela de leitura e escrita ao vivo para o seu sistema de arquivos real que o apagamento por comprometimento deixa em paz por design. Compartilhe o diretório do projeto em vez do seu diretório home, e um script caçando id_rsa e .env percorre um home Linux cheio de checkouts e caches de pacotes.

Quando não dá para checar a origem, vigie a saída

Toda checagem de procedência se reduz a um único movimento: comparar o que chegou com algo em que você já confia. Um nome, uma publicadora, uma assinatura, um hash no lockfile. O movimento funciona enquanto o invasor tiver que colocar alguma coisa no canal para você comparar. Um invasor que segura um trecho do canal não coloca nada nele, e o pool de origem da Coder era um trecho do canal. O comprometimento da BdThemes em agosto tinha o mesmo formato, em que um bucket de armazenamento sequestrado mudou um feed JSON que 350.000 sites WordPress buscavam, sem release nenhuma e sem arquivo modificado.

Quatorze horas é um dia de trabalho. A Coder pegou, entregou patches na manhã seguinte, publicou hashes e indicadores e disse aos leitores o que checar. O comunicado ainda assim termina pedindo que cada leitor vá olhar o próprio tráfego de saída, porque o registro do outro lado sumiu.

Rotacione as chaves que o comunicado lista, expurgue os módulos em cache, aceite o patch. Depois decida o que a próxima vai poder alcançar, porque a próxima vai chegar por um canal que você não tinha razão nenhuma para duvidar. Instale o Bromure Agentic Coding, dê ao build sua própria máquina, e tenha a resposta pronta quando o comunicado aparecer.