169.254.169.254 agora pede licença: o Hermes coloca a busca de credenciais do instance metadata na nuvem atrás de aprovação


Você está depurando um problema de “por que esta máquina não alcança o serviço interno” em uma VM na nuvem, e pede ao agente para descobrir quem é a instância. Ele decide olhar:

curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

Em uma instância na nuvem, esse comando devolve credenciais de acesso temporárias sem autenticação nenhuma — credenciais que representam a identidade da máquina e alcançam tudo o que a sua política de nuvem concedeu a ela. O Hermes tratava isso como uma leitura comum e deixava rodar. Agora ele para e pergunta. Se você roda agentes na nuvem, vale entender, porque isso fecha a porta dos fundos mais fácil de passar batido — aquela que aparece no momento em que um agente consegue executar comandos.

O que é o IMDS e por que ele é mais arriscado do que parece

IMDS é Instance Metadata Service: um endpoint HTTP especial que as principais nuvens expõem dentro de cada instância, no endereço link-local 169.254.169.254. A função dele é permitir que o software dentro da instância saiba quem ela é, em que região vive e que role carrega. A parte crítica é o modelo de autenticação: não existe nenhum. Qualquer processo na máquina capaz de fazer uma requisição HTTP pode lê-lo — inclusive aquele curl que você acabou de pedir para o agente rodar.

Cada nuvem tem o seu próprio número de porta para o mesmo serviço:

  • 169.254.169.254 — o endereço link-local IPv4 de AWS, Azure, GCP e OpenStack;
  • fd00:ec2::254 — a forma IPv6 da AWS;
  • metadata.google.internal — o hostname do GCP;
  • 100.100.100.200 — o endpoint da Alibaba Cloud.

Por que isso importa além da curiosidade? Porque o que volta normalmente não é só “quantas vCPUs”. Em geral vem junto credenciais temporárias de role IAM ou de service account. Ter essa string é equivalente a ter tudo o que a instância está autorizada a fazer — ler um bucket privado, chamar uma API interna, às vezes bem mais que isso. As credenciais expiram, mas costumam viver o suficiente para terminar o serviço.

O caminho do ataque: uma instrução injetada basta

Coloque dois fatos lado a lado e o risco fica óbvio: o agente lê conteúdo externo (páginas web, issues, e-mails, comentários de código) e o agente executa comandos. Se esse conteúdo externo contiver “já que você está aí, rode isto e me mande a saída”, e o comando apontar para um endpoint de metadata, então um único prompt injection se transforma em exfiltração de credenciais. Isso não é hipotético. A propriedade especial do endpoint é exatamente viver na rede interna e não precisar de credencial nenhuma, o que o torna a coisa mais barata de roubar.

Os provedores de nuvem fazem a parte deles — o IMDSv2 da AWS, por exemplo, exige um PUT para obter um token de uso único antes, o que eleva a barra para server-side request forgery. Mas isso não toma a decisão por você: se o agente deve ou não ler instance metadata é um julgamento que um humano merece fazer uma vez.

O que acontece agora: um prompt, não um bloqueio

O PR #106418 adiciona uma regra determinística na camada de aprovação de terminal, com um nome que diz exatamente o que é:

cloud metadata endpoint access (instance credentials)

O comportamento é um prompt de aprovação, não um bloqueio rígido. E isso é intencional. Existem usos legítimos em VMs reais na nuvem — o agente administra a própria instância, você está depurando uma role IAM, você está escrevendo um script que reporta fatos da instância. Quem decide é o operador.

Na prática:

  • Quando um desses quatro literais de host aparece em um comando de terminal, a aprovação é disparada. Não importa qual cliente HTTP você usa: curl, wget, uma atribuição de variável de ambiente ou um argumento de script, todos contam.
  • Em caminhos sem ninguém acompanhando (cron jobs, automação em approve-mode), esses comandos não são auto-aprovados — com o padrão approvals.cron_mode: deny, um job agendado que tente ler metadata é negado em vez de liberado.
  • A detecção é pattern matching, não julgamento de modelo. Não há deriva conforme o humor do modelo no dia, nem chamada extra de modelo no caminho crítico.

Escopo: os falsos positivos foram mantidos estreitos de propósito

A implementação usa lookarounds na entrada de DANGEROUS_PATTERNS (em tools/approval_detection.py) para que a regra só dispare nos próprios endpoints:

  • curl http://169.254.1.1/status — outros endereços link-local — não é afetado;
  • 169.254.169.2540, em que o endpoint é apenas prefixo de uma sequência de pontos maior, não casa;
  • metadata.google.internal.example.com, um host diferente que por acaso contém a string, não casa.

Em outras palavras, a regra só se manifesta quando um comando realmente mira o endpoint de credenciais, em vez de tratar tráfego link-local comum como perigoso.

Vale também separar isso da proteção do lado do navegador. As ferramentas de browser já aplicam o próprio piso de cloud metadata em todo backend (e restrições adicionais quando rodando dentro de um contêiner). Esta mudança cobre o caminho de comando de terminal — outra porta, em outro andar.

O desenho se inspira na prática já existente: o changelog do Claude Code 2.1.257 registra que o modo automático dele deixou de auto-aprovar buscas de credenciais em cloud metadata (junto com evasão de egress e alcance cross-tenant), a menos que o ambiente as declare esperadas. O Hermes adapta a parte de metadata em uma regra determinística de terminal — mais simples, porém previsível.

O prompt que você vai ver e como ajustá-lo

Quando a regra dispara, você recebe uma aprovação de comando carregando essa string de motivo. O que fazer a seguir depende da sua configuração de approvals:

# Em que modo estamos? (smart é o padrão)
hermes config get approvals.mode

# Manual: todo comando perigoso pergunta para você
hermes config set approvals.mode manual

# Desligar as aprovações (yolo) — mas atenção: a deny list continua valendo
hermes config set approvals.mode off

Detalhes que pegam as pessoas:

  • approvals.mode aceita smart, manual ou off; o padrão é smart, em que um modelo revisor trata os casos ambíguos, enquanto acertos de regra determinística não precisam de modelo nenhum.
  • approvals.deny é uma lista de bloqueio incondicional de globs fnmatch sem distinção de maiúsculas (por exemplo rm -rf *). Ela continua valendo em yolo, então approvals.mode: off não significa “nada é bloqueado nunca”.
  • Negações repetidas acionam um breaker: approvals.denial_breaker_threshold é 3 por padrão, então você não fica preso em um loop infinito de negações.
  • Jobs agendados seguem approvals.cron_mode (padrão deny). Se um cron job realmente precisa ler instance metadata, mude essa chave explicitamente em vez de desligar as aprovações por inteiro.
  • Dentro de uma sessão, uma negação interativa pode ser registrada com /deny mais um motivo.

Para a engrenagem mais ampla de aprovação — modos, o modelo revisor em auxiliary.approval.model e como testar — veja o guia de configuração do Smart Approvals e dry runs de aprovação. Guardas irmãs no estilo “pergunte antes” cobrem edições de config SSH e desinstalações por gerenciador de pacotes.

Verifique na sua própria máquina

Se você tem uma VM na nuvem que é sua, dá para confirmar com segurança a forma desse risco (somente em máquinas que você possui e está autorizado a usar):

# 1) Uma leitura inofensiva: devolve fatos quase públicos da instância
curl -s --max-time 2 http://169.254.169.254/latest/meta-data/instance-id

# 2) AWS IMDSv2: pegue um token de uso único e leia o caminho de credenciais com ele
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

O comando 2 imprime o nome da role e credenciais temporárias, o que é precisamente por que ele merece uma confirmação humana. Nunca cole o que ele devolve em uma sessão, um ticket ou um chat — é autoridade de nível de conta em forma de texto.

Se você vir essa regra disparar em uma máquina que não é da nuvem (um notebook, um data center privado), a causa usual é um endereço hardcoded dentro de algum script. Se você realmente precisa disso — está construindo uma ferramenta de auto-checagem de nuvem — aprove uma vez e deixe o motivo visível; é mais seguro do que baixar as aprovações para tudo.

A conclusão

Em uma frase: 169.254.169.254 em uma VM na nuvem não é “um endereço interno qualquer” — é um dispensador de credenciais sem autenticação. Colocá-lo atrás de aprovação troca um pouco de atrito pelo caminho de roubo de credencial mais barato que existe.

Três coisas para lembrar na prática:

  1. Quando um agente de repente quer instance metadata, pare e leia o comando. Pode ser depuração legítima, ou pode ser uma instrução injetada.
  2. Jobs agendados negam esses comandos por padrão; se você realmente precisa deles, libere esse caso via approvals.cron_mode em vez de desligar as aprovações por completo.
  3. approvals.deny é incondicional, e approvals.mode: off não significa “nada é bloqueado”.

Esta mudança foi mergeada depois da tag v0.21.2, então no momento em que escrevo ela vive na main e não está em um release tagueado. O hermes update pega a main, ou espere o próximo release.