Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

Proxy de saída com injeção de credenciais (iron-proxy)

Quando o Hermes roda seu agente dentro de um sandbox de terminal em Docker, esse sandbox normalmente segura as suas chaves de API reais (OPENROUTER_API_KEY, OPENAI_API_KEY etc.). Um agente com prompt injetado dentro desse sandbox pode dar cat ~/.config/openrouter/auth.json ou printenv | grep -i key e exfiltrar tudo.

O proxy de saída resolve isso: o sandbox segura tokens de proxy opacos, nunca as chaves reais. Todo o tráfego de saída do sandbox passa por um daemon local do iron-proxy (Apache-2.0, Go) no host, que termina o TLS e troca o token de proxy pela credencial real antes de encaminhar a requisição adiante. Comprometa o sandbox e o atacante sai de lá com tokens que só funcionam dentro da fronteira de proxy confiável configurada — a chave privada da CA e a integridade do endpoint do proxy fazem parte dessa fronteira. Se o tráfego puder ser redirecionado para uma infraestrutura de proxy controlada pelo atacante (por exemplo, uma chave privada de CA roubada ou um endpoint de proxy sequestrado), a garantia do token deixa de valer.

Esta versão liga o proxy de saída somente ao backend Docker. Modal, Daytona, SSH e Singularity não recebem variáveis de ambiente de proxy nem montagens de CA ainda.

O que ele é​

  • Um subprocesso iron-proxy gerenciado no host, instalado sob demanda em ~/.hermes/bin/iron-proxy
  • Uma CA local em ~/.hermes/proxy/ca.crt em que o sandbox confia, para o iron-proxy poder fazer MITM no TLS e reescrever cabeçalhos
  • Uma configuração proxy.yaml em ~/.hermes/proxy/proxy.yaml listando os hosts de destino que você permite e o mapeamento da transformação de segredos
  • Um mappings.json registrando qual token de proxy corresponde a qual variável de ambiente real

O sandbox recebe HTTPS_PROXY=http://host.docker.internal:9090, HTTP_PROXY=http://host.docker.internal:9091 e as variáveis de ambiente padrão de provedor, como OPENROUTER_API_KEY, definidas com tokens de proxy opacos. Apelidos HERMES_PROXY_TOKEN_<ENV_NAME> correspondentes também são exportados para diagnóstico. Os SDKs de provedor existentes leem os nomes de variável de sempre, mandam o token de proxy no Authorization, e a transformação secrets do iron-proxy substitui pelo valor real, tirado do ambiente do daemon no lado do host.

O que ele não é​

  • Ele não é o comando de entrada hermes proxy, que é um proxy reverso agregador de OAuth. Comando diferente (hermes egress), direção diferente.
  • Ele não fica entre o seu terminal local e os provedores — só entre o sandbox e os provedores.
  • Ele não reescreve credenciais para as chamadas de LLM feitas dentro do processo do host. Essas continuam usando as chaves do seu .env diretamente. O modelo de ameaça é o sandbox, não o host.

Início rápido​

# 1. Instala o binário do iron-proxy (versão fixada, verificada por SHA-256)
hermes egress install

# 2. Roda o assistente: gera a CA, cunha tokens de proxy para cada chave de
# provedor no seu ambiente, escreve o proxy.yaml.
hermes egress setup

# 3. Sobe o daemon do proxy
hermes egress start

# 4. Confere o estado
hermes egress status

O hermes egress setup descobre as chaves de provedor no seu ambiente. Se suas chaves vivem só no ~/.hermes/.env (sem serem exportadas para o shell), o setup lê esse arquivo automaticamente — você não precisa dar export antes.

Quando você roda o setup de novo depois (novo host na lista de permitidos, tokens rotacionados, fonte de credencial trocada), ele para o daemon em execução, porque a configuração dele fica em memória, e então oferece reiniciá-lo para você, para a mudança valer na hora. Num tty ele pergunta; passe --restart para sempre reiniciar ou --no-restart para deixar desligado. Para aplicar mudanças em qualquer outro momento, o hermes egress restart é o parar-e-subir num comando só.

Uma vez rodando, o backend de terminal Docker automaticamente:

  • Monta o ~/.hermes/proxy/ca.crt dentro do sandbox em /etc/ssl/certs/hermes-egress-ca.crt
  • Define HTTPS_PROXY, HTTP_PROXY, REQUESTS_CA_BUNDLE, SSL_CERT_FILE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS para fazer todo runtime HTTP comum passar pelo proxy e confiar na CA
  • Define NODE_OPTIONS=--use-openssl-ca (acrescentado ao que você já tem em docker_env.NODE_OPTIONS) para o Node.js passar pelo repositório do OpenSSL que as outras variáveis de pacote de CA controlam — veja a ressalva de CA assimétrica no Node.js abaixo para saber da brecha que sobra
  • Acrescenta --add-host=host.docker.internal:host-gateway para o sandbox alcançar o proxy do lado do host no Linux (o Docker Desktop cuida disso automaticamente no macOS/Windows)
  • Exporta o token de proxy sob o nome padrão de variável do provedor (por exemplo, OPENROUTER_API_KEY) mais um apelido de diagnóstico HERMES_PROXY_TOKEN_<ENV_NAME> por mapeamento cunhado

Configuração​

A configuração completa vive em ~/.hermes/config.yaml, na seção proxy:. Os padrões estão documentados ali mesmo; tudo é opcional.

proxy:
# Chave-mestra. Quando false, o recurso é um nada absoluto — nenhum
# binário baixado, nenhuma montagem docker acrescentada, nenhum subprocesso iniciado.
enabled: false

# Porta do ouvinte do túnel. Os sandboxes batem em http://host.docker.internal:<porta>.
tunnel_port: 9090

# Baixa automaticamente o binário fixado do iron-proxy no primeiro uso.
auto_install: true

# Onde o iron-proxy busca os segredos reais de destino na hora da saída.
# env — ambiente do processo (padrão). O que estiver no seu ~/.hermes/.env
# na hora em que o proxy sobe é a fonte da verdade.
# bitwarden — rebusca no Bitwarden Secrets Manager a cada reinício do
# proxy. A rotação no app web do BW se propaga sem
# tocar no .env. Exige `secrets.bitwarden.enabled: true`.
credential_source: env

# Quando true (padrão), o backend Docker se recusa a subir um sandbox se
# o proxy estiver habilitado mas não estiver rodando. Defina como false para cair na
# postura antiga de "credenciais reais dentro do sandbox" quando o proxy
# não estiver disponível.
enforce_on_docker: true

# Quando `credential_source: bitwarden` mas o token de acesso do BWS /
# o project_id está faltando OU a busca do bws não devolve valores para os
# provedores mapeados, o daemon levanta erro por padrão (combina com o espírito de "eu
# pedi rotação — não use valores velhos do env em silêncio"). Defina
# como true para voltar ao recurso antigo de cair no env do host — útil para
# migrações em que você quer começar a mudar para o modo BW mas ainda não
# ligou todos os segredos.
allow_env_fallback: false

# Lista de negação contra SSRF aplicada ao tráfego de saída. Omita / deixe nulo para
# usar o padrão seguro: loopback (v4 + v6), link-local (incl. IPs de
# metadados de nuvem em 169.254.169.254), RFC1918, ULA IPv6, IPv4 mapeado em v6,
# CGNAT e a faixa de benchmark da RFC2544. Defina como um `[]` explícito
# para desistir por completo (só faz sentido em testes herméticos).
upstream_deny_cidrs: null

# Hosts de destino permitidos além dos padrões que já vêm juntos.
# Curingas (`*.foo.com`) são suportados. Os padrões cobrem OpenRouter,
# OpenAI, Anthropic, Google, xAI, Mistral, Groq, Together, DeepSeek
# e Nous Research.
extra_allowed_hosts: []

Hosts de destino permitidos por padrão​

openrouter.ai *.openrouter.ai
api.openai.com api.anthropic.com
generativelanguage.googleapis.com
api.x.ai api.mistral.ai
api.groq.com api.together.xyz
api.deepseek.com inference.nousresearch.com

Se o seu agente precisa de um destino que não está na lista — um endpoint de inferência auto-hospedado, um LLM de nuvem extra, um servidor MCP — acrescente ao proxy.extra_allowed_hosts. Curingas são comparados com o nome de host completo (*.example.com casa com api.example.com e staging.example.com, mas não com o example.com em si).

CIDRs negados por padrão contra SSRF​

Aplicados independentemente da lista de permitidos. Essas faixas são recusadas pelo iron-proxy na fronteira de rede, então um ataque de DNS rebinding via um nome de host permitido não consegue alcançar o IMDS nem a sua rede interna:

CIDRObjetivo
127.0.0.0/8, ::1/128Loopback (v4 + v6)
169.254.0.0/16, fe80::/10Link-local — incl. IMDS da AWS / GCP / Azure em 169.254.169.254
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16RFC1918
fc00::/7ULA IPv6
::ffff:0:0/96IPv6 com IPv4 mapeado — fecha o desvio de IMDS por pilha dupla
100.64.0.0/10CGNAT da RFC6598 (usado por VPC da AWS, redes de pod do K8s)
198.18.0.0/15Faixa de benchmark da RFC2544

Para sobrescrever: defina proxy.upstream_deny_cidrs com a sua própria lista. Para desistir por completo (por exemplo, num teste hermético que precisa alcançar um destino em loopback): defina como uma lista vazia [].

Política de bind​

O proxy nunca faz bind em 0.0.0.0. O bind padrão é específico de plataforma, porque o iron-proxy v0.39 suporta apenas um único bind por processo do daemon:

  • Linux: o gateway da ponte do docker (172.17.0.1:<tunnel_port>, por padrão). Os contêineres alcançam o proxy via host.docker.internal, que o --add-host=host.docker.internal:host-gateway resolve exatamente para esse IP de gateway da ponte — um bind só em loopback seria inalcançável de dentro dos sandboxes. O IP da ponte é um endereço na interface docker0 do host, então não fica exposto para a LAN; ele É alcançável por outros contêineres na rede de ponte padrão, mas as requisições ainda exigem um token de proxy cunhado e um destino permitido. Se nenhuma ponte do docker é detectada (docker não instalado/não rodando), o bind cai para loopback com um aviso.
  • Docker Desktop no macOS / Windows: loopback (127.0.0.1:<tunnel_port>). O VPNkit do Desktop roteia o host.docker.internal para o host, então o loopback é alcançável pelos contêineres e é a escolha menos exposta.

Um par na LAN com um token de proxy vazado não consegue usar o proxy — nenhum dos dois binds é alcançável pela rede externa.

Nós também fixamos metrics.listen: 127.0.0.1:0, para o servidor de métricas embutido do daemon pegar uma porta efêmera em loopback em vez do seu :9090 padrão — senão ele brigaria com o tunnel_port: 9090 pelo mesmo socket e o daemon se recusaria a subir com "address already in use". Note que a porta efêmera do :0 é aleatória a cada partida e não é exposta em lugar nenhum, então as métricas ficam efetivamente desativadas com essa fixação.

Se um shim hostil do ip, mais cedo no PATH, tivesse conseguido injetar um IPv4 não privado como endereço da ponte (0.0.0.0, um endereço público, multicast, link-local etc.), o recuo para loopback ainda se aplica — nós nunca fazemos bind em nada que não conseguimos validar com ipaddress.IPv4Address mais verificações is_*.

Esquemas de autenticação cobertos​

A transformação secrets troca o token de proxy onde quer que ele apareça num local casado — e ela casa com mais do que Authorization: Bearer:

ProvedorVariável de ambienteTrocado em
OpenRouter, OpenAI, Groq, Together, DeepSeek, Mistral, xAI, Nous*_API_KEYCabeçalho Authorization
Anthropic nativoANTHROPIC_API_KEYx-api-key + Authorization
Azure OpenAIAZURE_OPENAI_API_KEYapi-key + Authorization (*.openai.azure.com, *.cognitiveservices.azure.com, *.services.ai.azure.com)
Google AI Studio (Gemini)GEMINI_API_KEY / GOOGLE_API_KEYCabeçalho x-goog-api-key ou parâmetro de consulta ?key=

GEMINI_API_KEY e GOOGLE_API_KEY são tratadas como uma credencial só: um único token de proxy é cunhado e injetado no sandbox sob os dois nomes, e qualquer um dos nomes no ambiente do host satisfaz a descoberta.

Provedores não cobertos​

Esquemas de autenticação que envolvem assinatura de requisição ou OAuth cunhado pelo SDK não podem ser trocados por uma substituição estática de cabeçalho — se as variáveis de ambiente deles estão presentes, o sandbox segura credenciais reais desses provedores e a garantia de isolamento de saída fica incompleta para eles:

Variável de ambienteProvedorMotivo
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYAWS Bedrock / SageMakerRequisições assinadas com SigV4
GOOGLE_APPLICATION_CREDENTIALSGCP Vertex AIOAuth cunhado a partir de um arquivo de conta de serviço

Essas variáveis de ambiente estão presentes na maioria dos notebooks de quem desenvolve, por causa de ferramentas sem relação com isso (terraform, gcloud, CLI da aws, push no ECR). Elas aparecem como avisos no assistente e no hermes egress status, mas nunca impedem o proxy de subir. Se você não usa esses provedores a partir de sandboxes, dê unset nas variáveis para limpar o aviso.

Integração com o Bitwarden​

Se você já usa o Bitwarden Secrets Manager via hermes secrets bitwarden setup, o proxy de saída pode puxar as credenciais reais de lá em vez do os.environ:

hermes egress setup --from-bitwarden

Isso define proxy.credential_source: bitwarden e descobre os nomes de variável de provedor no seu projeto do BW.

Semântica da rotação​

Quando credential_source: bitwarden, o daemon do iron-proxy rebusca os segredos no BWS via bws secret list <project_id> toda vez que sobe. Então o fluxo de rotação é:

  1. Rotacione uma chave no app web do Bitwarden.
  2. hermes egress stop && hermes egress start no host.
  3. Os sandboxes iniciados depois desse ponto trocam os tokens de proxy pelo novo valor.

Sem editar .env. Sem reiniciar o Hermes no host. O daemon do proxy é a única coisa que encosta no novo valor — o seu processo no host e o os.environ ficam intocados.

Falhar alto na partida​

Quando credential_source: bitwarden, o hermes egress start faz uma pré-checagem na camada do assistente E o _build_proxy_subprocess_env refaz a checagem na camada do daemon:

  • A variável de ambiente do token de acesso do BWS está indefinida → recusa a subir, com uma dica para dar unset e rodar de novo, ou usar hermes egress setup --no-bitwarden para voltar ao modo env
  • secrets.bitwarden.project_id está vazio → recusa a subir, com uma dica para rodar hermes secrets bitwarden setup
  • bws secret list não devolve valores para um ou mais provedores mapeados → recusa a subir, listando os nomes que faltam

Isso é de propósito. Cair no env do host no modo BW reintroduz exatamente o bug de valor velho que o caminho do BW existe para derrotar (o operador escolheu o BW pela garantia de rotação; um recuo silencioso quebra essa garantia).

A opção de configuração proxy.allow_env_fallback: true faz você voltar ao comportamento antigo de "cair em silêncio no env do host se o BWS estiver inalcançável", para cenários de migração. Use quando estiver movendo segredos para o BW um a um e quiser que o daemon suba com os valores que estiverem disponíveis.

Trocar a fonte de credencial​

DeParaComando
envbitwardenhermes egress setup --from-bitwarden
bitwardenenvhermes egress setup --no-bitwarden

Rodar o hermes egress setup de novo SEM nenhuma das duas opções preserva o credential_source existente — o assistente se recusa a rebaixar você de volta para env em silêncio. Isso importa porque, uma vez que você configurou o modo bitwarden, a garantia de rotação é o que você contratou; você tem que dizer explicitamente "quero env de novo" para mudar.

Comandos​

A árvore de subcomandos da CLI:

hermes egress install # baixa o binário fixado do iron-proxy
hermes egress install --force # rebaixa mesmo se já existir uma cópia gerenciada

hermes egress setup # assistente interativo
hermes egress setup --tunnel-port N # sobrescreve a porta do ouvinte do túnel
hermes egress setup --from-bitwarden # usa o BWS como fonte de credencial (falha alto)
hermes egress setup --no-bitwarden # volta explicitamente para o modo env
hermes egress setup --rotate-tokens # cunha tokens novos para todo provedor
# (o padrão preserva os existentes)

hermes egress start # sobe o daemon gerenciado do proxy
hermes egress stop # SIGTERM (depois SIGKILL após 5s de tolerância)
hermes egress restart # para (se estiver rodando) e sobe — necessário quando
# os SEGREDOS de destino mudam (rotação, novo provedor)
hermes egress reload # recarrega a quente o conjunto de regras do proxy.yaml pela
# API de gerenciamento — sem reinício, sem conexões
# derrubadas (edições de lista de permitidos / mapeamento)

hermes egress status # binário + config + pid + estado de escuta + mapeamentos
hermes egress status --show-tokens # imprime os tokens de proxy por inteiro
# (padrão: só prefixo + sufixo, o resto oculto)

hermes egress disable # vira proxy.enabled = false
# (não para um proxy em execução)

hermes egress config # imprime o caminho do proxy.yaml, para depuração

Rotação de tokens​

Por padrão, o hermes egress setup preserva os tokens de proxy dos provedores que já têm um. Acrescentar um provedor novo cunha um token novo só para ele; os tokens existentes ficam inalterados. Isso evita que os sandboxes em execução comecem a tomar 401 quando você roda o assistente de novo.

O --rotate-tokens rola todos os tokens:

hermes egress setup --rotate-tokens

Quando existem tokens E a entrada padrão é um tty, o assistente pede confirmação:

⚠ --rotate-tokens will invalidate proxy tokens in every running
Hermes sandbox. They will start 401-ing against upstreams until restarted.
Type 'rotate' to confirm:

Invocações fora de tty (CI, scripts) pulam a pergunta — a opção é tratada como deliberada. Antes de qualquer sobrescrita, o mappings.json atual é copiado para um irmão com marca de tempo, para a recuperação manual ser possível:

backup: ~/.hermes/proxy/mappings.json.rotated-20260524T143012

O hermes egress setup para um daemon em execução quando reescreve a configuração ou os mapeamentos de token, porque o daemon guarda o YAML antigo em memória. Depois de --rotate-tokens:

hermes egress start

Os contêineres que já estão rodando seguram os tokens antigos e vão precisar ser reiniciados para pegar os novos. Contêineres Docker persistentes novos incluem um rótulo de postura de saída, então o Hermes não vai reaproveitar um contêiner de antes da saída ou de antes da rotação para novas sessões.

Layout do diretório de estado​

Tudo que o iron-proxy mantém vive em ~/.hermes/proxy/:

CaminhoModoObjetivo
~/.hermes/proxy/ (dir)0o700Pertence a você e só você atravessa
ca.crt0o644Certificado público da CA distribuído para os sandboxes
ca.key0o600Chave de assinatura da CA — nunca sai do host
proxy.yaml0o600Configuração do iron-proxy; reescrita a cada setup
mappings.json0o600Token de proxy do sandbox → variável de ambiente de destino
mappings.json.rotated-*0o600Backups criados pelo --rotate-tokens
iron-proxy.pid0o600PID do daemon em execução
iron-proxy.nonce0o600Nonce por partida, para defesa contra reciclagem de PID
iron-proxy.log0o600Saída padrão/de erro do daemon — inclui registros por requisição na v0.39
audit.log0o600Reservado para o fluxo dedicado de auditoria por requisição em versões futuras do binário; criado antes da hora para o contrato de privacidade valer quando o projeto original ligar isso

A chave privada da CA é o arquivo mais sensível. Ela é criada com 0o600 desde o primeiro byte (sem janela de TOCTOU por umask) e com O_NOFOLLOW, para um atacante com o mesmo uid não conseguir redirecioná-la com um link simbólico plantado. O arquivo de pid, o arquivo de nonce, o log do daemon e o log de auditoria recebem o mesmo tratamento.

Logs no iron-proxy v0.39​

Na versão de binário fixada hoje (v0.39.0), o iron-proxy escreve TODA a saída — diagnósticos de nível de daemon E registros por requisição — em ~/.hermes/proxy/iron-proxy.log. A estrutura config.Log da v0.39 não tem um campo audit_path separado, então não dá para mandar os registros por requisição para um fluxo dedicado lá.

Mesmo assim criamos o ~/.hermes/proxy/audit.log antes da hora, em 0o600 e com O_NOFOLLOW, porque:

  1. Isso reserva o caminho para a futura subida de versão: quando a versão fixada passar para uma que suporta log.audit_path, os registros por requisição vão começar a fluir para lá sem reconfiguração do lado do operador. Até então o arquivo fica com 0 bytes — não aponte ferramentas de monitoramento, alerta ou perícia para ele ainda. Use o iron-proxy.log para tudo hoje.
  2. A garantia de 0o600 desde o primeiro byte defende contra o dia da correção lá em cima, em que a v0.40+ cria o arquivo sob o umask padrão dela se ele ainda não existir.

Até essa subida de versão chegar, trate o iron-proxy.log como a fonte da verdade para os dois públicos:

  • Eventos de nível de daemon (banner de partida, erros de bind, motivo do desligamento, erros de transformação). Operação e diagnóstico.
  • Registros por requisição (CONNECT para um destino permitido, troca de segredo disparada, negação pela lista de permitidos). Perícia e conformidade.

Os dois arquivos recebem acréscimos por cima dos reinícios. Rotacione com o logrotate se você se importa com uso de disco em hosts de vida longa.

Como funciona​

┌──────────────┐ ┌──────────────┐ ┌─────────────┐
│ Docker │ CONNECT / │ iron-proxy │ HTTPS w/ │ OpenRouter │
│ sandbox ├──────────────▶│ (host:9090) ├───────────────▶│ / OpenAI / │
│ │ HTTP forward │ │ real API key │ Anthropic … │
│ has: │ w/ proxy tok │ mints leaf │ │ │
│ - proxy tok │ in Auth hdr │ cert from CA │ │ │
│ - CA cert │ │ matches token │ │ │
│ - HTTPS_PROXY│ │ swaps secret │ │ │
└──────────────┘ └──────────────┘ └─────────────┘
│
│ daemon + per-request log (combined on v0.39)
▼
~/.hermes/proxy/iron-proxy.log
(~/.hermes/proxy/audit.log reserved for v0.40+ split stream)
  1. O sandbox faz uma requisição HTTPS, por exemplo POST https://openrouter.ai/v1/chat/completions com Authorization: Bearer hermes-proxy-openrouter-… (o token de proxy, não a chave real).
  2. Como o HTTPS_PROXY está definido, a requisição vai para o iron-proxy como um túnel CONNECT.
  3. O iron-proxy confere a lista de permitidos. openrouter.ai está permitido.
  4. O iron-proxy cunha um certificado folha assinado pela nossa CA para openrouter.ai, termina a conexão TLS e inspeciona a requisição.
  5. A transformação secrets casa com a string do token de proxy no cabeçalho Authorization e substitui pelo valor real de OPENROUTER_API_KEY, tirado do ambiente do próprio iron-proxy.
  6. A requisição é recriptografada e encaminhada para o OpenRouter.
  7. A requisição é registrada em ~/.hermes/proxy/iron-proxy.log na v0.39. Quando a versão fixada do binário suportar o fluxo separado (v0.40+), os registros por requisição vão fluir para ~/.hermes/proxy/audit.log e os diagnósticos de nível de daemon vão ficar no iron-proxy.log. Veja Logs no iron-proxy v0.39.

Uma requisição para um host que não está na lista de permitidos (por exemplo, https://attacker.example.com/leak?key=...) é rejeitada com HTTP 403 antes de qualquer byte sair do host. A negação é registrada no iron-proxy.log com o host de destino e o sandbox de origem.

Distribuição da CA dentro do sandbox​

Quando o backend Docker sobe um contêiner com proxy.enabled: true e o daemon está escutando, ele acrescenta estes argumentos ao docker run:

ArgumentoObjetivo
-v ~/.hermes/proxy/ca.crt:/etc/ssl/certs/hermes-egress-ca.crt:roMontagem somente leitura da CA
-e HTTPS_PROXY=http://host.docker.internal:9090httpx do Python / curl / transporte padrão do go / fetch do Node
-e HTTP_PROXY=http://host.docker.internal:9091curl + wget para HTTP puro — o ouvinte de encaminhamento de HTTP puro vive em tunnel_port + 1
-e NO_PROXY=127.0.0.1,localhost,::1Servidores de desenvolvimento em loopback dentro do sandbox pulam o proxy
-e REQUESTS_CA_BUNDLE=…ca.crtrequests do Python
-e SSL_CERT_FILE=…ca.crtMódulo ssl do Python / OpenSSL — substitui o repositório do sistema
-e CURL_CA_BUNDLE=…ca.crtcurl — substitui o repositório do sistema
-e NODE_EXTRA_CA_CERTS=…ca.crtNode.js — acrescenta ao repositório do sistema
-e NODE_OPTIONS="<seu valor> --use-openssl-ca"Node.js — passa pelo repositório do OpenSSL (acrescentado; o seu --max-old-space-size etc. são preservados)
-e HERMES_EGRESS_PROXY=1Sentinela que o agente pode ler para saber que está ciente do proxy
-e OPENROUTER_API_KEY=<token-de-proxy>Os nomes padrão de variável de provedor recebem tokens de proxy, para os SDKs existentes continuarem funcionando
-e HERMES_PROXY_TOKEN_<NAME>=…Apelido de diagnóstico para cada mapeamento; mesmo valor da variável de ambiente padrão do provedor
--add-host=host.docker.internal:host-gatewaySó no Linux; o Docker Desktop mapeia isso automaticamente

Ressalva de CA assimétrica no Node.js​

REQUESTS_CA_BUNDLE / SSL_CERT_FILE / CURL_CA_BUNDLE substituem o repositório de CAs do sistema dentro do sandbox. O NODE_EXTRA_CA_CERTS acrescenta a ele. Um processo Node.js dentro do sandbox poderia, em princípio, driblar o proxy abrindo um net.Socket cru e começando o próprio aperto de mão TLS — o repositório de CAs do sistema ainda confiaria nos certificados reais de destino, então a requisição daria certo onde o Python / curl falhariam na validação.

O NODE_OPTIONS=--use-openssl-ca é acrescentado ao que você já tem em docker_env.NODE_OPTIONS. Isso força o Node a passar pelo repositório do OpenSSL que o SSL_CERT_FILE controla, estreitando a assimetria. Isso NÃO cobre código que passa explicitamente a própria opção ca para tls.connect() ou https.request(), mas fecha o caso fácil.

Esta é uma limitação conhecida da v1. Acompanhe github.com/ironsh/iron-proxy/issues para uma resolução lá em cima; enquanto isso, não rode código Node não confiável que abra sockets crus num sandbox de que você depende para isolamento de saída.

Colisões de docker_env​

Se você define variáveis de ambiente que controlam o proxy no seu bloco de configuração docker_env: (raro, mas possível), o Hermes se recusa a subir o sandbox quando enforce_on_docker: true está definido. Isso inclui os dois casos:

  • Variáveis de controle de saída: HTTPS_PROXY, HTTP_PROXY, NO_PROXY, REQUESTS_CA_BUNDLE, SSL_CERT_FILE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS
  • Variáveis de ambiente reais de provedor: todo nome no mappings.json (por exemplo, OPENROUTER_API_KEY, OPENAI_API_KEY)

Exemplo de erro:

docker_env in config.yaml overrides egress-proxy variables
['HTTPS_PROXY', 'OPENROUTER_API_KEY']; enforce_on_docker is enabled.
Remove these keys from docker_env or disable enforce_on_docker to
opt out of egress isolation.

Com enforce_on_docker: false, a mesma situação aparece como um aviso e os seus valores de docker_env vencem — útil para migrações ou testes, mas você está saindo explicitamente da garantia de isolamento.

Defesa de PID e nonce​

O arquivo de pid do daemon é escrito com O_EXCL + O_NOFOLLOW mais uma verificação de dono. Chamadas concorrentes de hermes egress start produzem um de dois resultados:

  • O arquivo de pid existente aponta para um iron-proxy vivo → a segunda partida recusa com "another start in progress" mais uma dica para rodar hermes egress stop
  • O arquivo de pid existente está velho (daemon caiu) → a segunda partida o apaga e tenta uma vez mais

Além disso, todo start_proxy planta um nonce aleatório novo em dois lugares:

  • HERMES_IRON_PROXY_NONCE=<nonce> no ambiente do daemon
  • ~/.hermes/proxy/iron-proxy.nonce (irmão do arquivo de pid, em 0o600)

Quando o hermes egress stop (ou qualquer outra verificação _pid_alive) quer confirmar que um PID ainda se refere ao nosso daemon — e não a um processo sem relação que ganhou o mesmo PID depois de o iron-proxy cair — ele lê /proc/<pid>/environ e procura o nonce. A cópia em disco é o que faz isso funcionar entre invocações da CLI (o _proxy_nonce em memória é por processo e zera a cada invocação do hermes).

Se a verificação do nonce falha, o código cai para comparar o nome base do argv[0] com iron-proxy. O stop_proxy ainda captura o starttime de /proc/<pid>/stat antes do SIGTERM e reverifica depois da janela de tolerância de 5s — se o starttime mudou, o PID foi reciclado no meio da espera e o SIGKILL é suprimido com um aviso.

Modelo de segurança​

Do que isto protege:

  • Agente com prompt injetado num sandbox Docker lendo printenv / arquivos de credencial e exfiltrando chaves reais.
  • Dependência comprometida dentro do sandbox ligando para casa em um host arbitrário — a lista de permitidos, que nega por padrão, bloqueia destinos desconhecidos.
  • Agente discando para endpoints de metadados de nuvem (169.254.169.254) — o iron-proxy nega esses por padrão, via upstream_deny_cidrs, incluindo a forma IPv4 mapeada em v6 ::ffff:169.254.169.254.
  • DNS rebinding através de um nome de host permitido para um IP privado — os CIDRs negados são conferidos na hora de conectar, não na hora de montar a lista de permitidos.
  • Processos locais com o mesmo uid lendo o ambiente do daemon do iron-proxy para raspar segredos — só os nomes de variável referenciados pelos mapeamentos são encaminhados, não o ambiente inteiro do host.
  • Um par na LAN com um token de proxy vazado do sandbox gastando a sua cota de API — o proxy faz bind no gateway da ponte do docker (Linux) ou em loopback (Docker Desktop), nunca em 0.0.0.0, então é inalcançável pela rede externa.

Do que isto NÃO protege:

  • Um processo comprometido no host. Se o próprio processo do agente é comprometido, as chaves reais no ~/.hermes/.env do host ficam expostas de qualquer jeito. Este é um recurso de defesa em profundidade para o comprometimento do sandbox, não do host.
  • A perda da própria fronteira de proxy confiável. A garantia de troca de token assume que o sandbox confia no certificado de CA montado (/etc/ssl/certs/hermes-egress-ca.crt) e que o tráfego de fato chega ao nosso iron-proxy. Se a chave privada da CA for roubada, ou se a saída do sandbox for redirecionada para uma infraestrutura de proxy controlada pelo atacante, um adversário no meio pode apresentar um certificado folha válido e os tokens de proxy deixam de ser uma fronteira significativa (cf. MITRE ATT&CK T1588.004 — material de certificado TLS obtido, permitindo AiTM). Proteja a chave da CA (ela é 0600, só do host) e o endpoint do proxy do mesmo jeito.
  • Processos do sandbox que driblam o HTTPS_PROXY usando um socket cru. O proxy não intercepta o que não passa por ele. O Node.js é parcialmente mitigado via NODE_OPTIONS=--use-openssl-ca (veja a ressalva acima).
  • Arquivos de credencial montados explicitamente no Docker (terminal.credential_files ou montagens registradas por skills). A saída protege variáveis de ambiente de provedor; ela não inspeciona arquivos montados arbitrários. Não monte credenciais reais de provedor num sandbox com saída forçada.
  • Exfiltração de dados por host permitido. Se o api.openai.com está permitido, um agente poderia embutir dados de exfiltração num corpo de requisição para esse host. O log do daemon registra que a requisição aconteceu, mas não a impede.
  • Provedores não cobertos (SigV4 do AWS Bedrock, OAuth de conta de serviço do GCP Vertex). As variáveis de ambiente deles ficam no sandbox; se você os habilita, essas credenciais driblam o proxy por completo. Veja Provedores não cobertos.
  • Zeragem de segredos em memória no iron-proxy. O binário em Go segura as credenciais reais trocadas na memória do processo; um core dump ou uma leitura de /proc/<pid>/mem por um atacante com o mesmo uid exporia tudo. Fora do escopo desta camada.

Modos de falha​

  • Binário não instalado, auto_install: true — o primeiro hermes egress setup ou hermes egress start baixa. Verificado por SHA-256 contra o checksums.txt do projeto original.
  • Binário não instalado, auto_install: false — o start falha com uma mensagem clara apontando para a instalação manual.
  • enabled: true mas o proxy não está rodando — com enforce_on_docker: true (padrão), a criação do sandbox Docker se recusa a começar, com um erro explicativo. Com enforce_on_docker: false, ele cai na saída direta com credenciais reais e registra um aviso.
  • Colisão de porta — o iron-proxy sai na hora; o hermes egress start reporta as últimas 20 linhas de log e falha com código de saída diferente de zero.
  • Host de destino negado — o sandbox recebe HTTP 403 do proxy, com um corpo explicando qual host não foi permitido. O agente vê o erro e o reporta.
  • IP de metadados de nuvem (169.254.169.254) requisitado — recusado pelo upstream_deny_cidrs, independentemente da lista de permitidos.
  • docker_env colide com uma variável que controla o proxy (forçamento ligado) — a criação do sandbox recusa, com os nomes das chaves em colisão.
  • docker_forward_env tenta encaminhar uma chave protegida de provedor (forçamento ligado) — a criação do sandbox recusa; tire a chave do docker_forward_env ou saia com proxy.enforce_on_docker: false.
  • docker_extra_args sobrescreve controles de env/rede do proxy (forçamento ligado) — a criação do sandbox recusa; argumentos fornecidos pelo usuário, como -e HTTPS_PROXY=..., --env-file ou --network, rodam depois dos argumentos gerados pelo Hermes e podem driblar a saída.
  • Token de acesso do BWS faltando em credential_source: bitwarden — o hermes egress start recusa, com --no-bitwarden como dica de recuperação.
  • O iron-proxy não faz bind em 5 segundos — o processo é morto, o arquivo de pid é apagado, e o erro nomeia a porta mais o final do iron-proxy.log.
  • Chamadas concorrentes de hermes egress start — a segunda chamada recusa com "another start in progress" se o daemon da primeira está de pé; senão, a segunda apaga o arquivo de pid velho e segue.

Diagnóstico de problemas​

"Refusing to start: BWS_ACCESS_TOKEN is not set"​

Você habilitou credential_source: bitwarden, mas a variável de ambiente do token de acesso não está no seu shell. Ou:

export BWS_ACCESS_TOKEN=… # de uma vez só
hermes egress start

Ou mova para o ~/.hermes/.env. Ou volte para o modo env:

hermes egress setup --no-bitwarden

"iron-proxy exited immediately"​

Olhe as últimas 20 linhas de ~/.hermes/proxy/iron-proxy.log. Causas comuns:

  • Porta já em uso → mude o proxy.tunnel_port ou mate o que mais estiver segurando a 9090
  • proxy.yaml inválido → rode hermes egress setup para gerar de novo
  • Permissões erradas no certificado / na chave da CA → chmod 0o600 ~/.hermes/proxy/ca.key

"iron-proxy did not bind <bind-host>:9090 within 5s"​

O daemon subiu mas nunca fez bind no ouvinte. Em geral significa que o binário travou ou está fazendo algo caro na partida. Confira o ~/.hermes/proxy/iron-proxy.log. O processo órfão é morto automaticamente e o arquivo de pid é limpo, então basta tentar hermes egress start de novo.

O sandbox estoura o tempo ao conectar no proxy (Linux)​

O contêiner resolve host.docker.internal para o gateway da ponte do docker e o proxy está com bind lá, mas um firewall do host (em geral o ufw com INPUT negando por padrão) derruba o tráfego de contêiner para host na docker0. Verifique de dentro de um contêiner:

docker run --rm --add-host host.docker.internal:host-gateway busybox \
nc -zv -w 3 host.docker.internal 9090

Se isso estoura o tempo enquanto o hermes egress status mostra listening, libere a sub-rede da ponte no seu firewall, por exemplo para o ufw:

sudo ufw allow in on docker0 to any port 9090 proto tcp
sudo ufw allow in on docker0 to any port 9091 proto tcp

(9091 = o ouvinte de encaminhamento de HTTP puro em tunnel_port + 1.)

O sandbox vê HTTP 403 do proxy​

O agente dentro do sandbox tentou bater num host que não está no proxy.extra_allowed_hosts. O corpo do 403 explica qual host. Se você quer permitir, acrescente à sua configuração:

proxy:
extra_allowed_hosts:
- api.example.com
- "*.staging.example.com"

Depois hermes egress setup (para gerar o proxy.yaml de novo) e hermes egress stop && hermes egress start.

O sandbox vê erros de verificação de SSL​

Ou a CA não está montada no sandbox (raro; o backend do docker faz isso automaticamente quando proxy.enabled: true), ou o cliente HTTP da sua imagem está lendo de uma variável de ambiente fora do padrão.

# Dentro do sandbox:
cat /etc/ssl/certs/hermes-egress-ca.crt | head -1
# Deve imprimir: -----BEGIN CERTIFICATE-----
env | grep -E "^(REQUESTS|CURL|SSL|NODE).*CA"
# Deve listar todas as quatro variáveis de pacote de CA apontando para /etc/ssl/certs/hermes-egress-ca.crt

Se o certificado não está lá, confira que proxy.enabled: true E que o hermes egress status mostra Listening yes. Se as variáveis de ambiente estão faltando, a imagem do sandbox pode estar rodando um entrypoint que as remove — confira a sua configuração de docker_env.

O sandbox vê HTTP 401 dos destinos​

Duas causas comuns:

  1. Tokens atropelados num novo setup. Você rodou hermes egress setup --rotate-tokens (ou rotacionou tokens de outro jeito) e os sandboxes em execução ainda seguram os tokens antigos. Reinicie os sandboxes.
  2. A atualização do Bitwarden falhou em silêncio. Não deveria acontecer com o novo comportamento de falhar alto, mas, se você tem proxy.allow_env_fallback: true definido, o daemon pode ter subido com valores velhos do env. Confira o ambiente do daemon (/proc/<iron-proxy-pid>/environ) atrás da OPENROUTER_API_KEY esperada etc.

"Address in use" depois de o processo pai morrer​

O processo pai do Hermes morreu durante o hermes egress start (Ctrl-C durante a sondagem de escuta, OOM, pânico). A nova lógica de conserto escreve o arquivo de pid logo depois do Popen, então o órfão é recuperável:

hermes egress stop # acha o órfão pelo arquivo de pid e mata
hermes egress start

Se o hermes egress stop diz "iron-proxy was not running" mas você ainda vê o daemon no ps, o arquivo de pid dessincronizou. Recuperação manual:

pkill -TERM iron-proxy
rm -f ~/.hermes/proxy/iron-proxy.pid ~/.hermes/proxy/iron-proxy.nonce
hermes egress start

Inspecionar o comportamento por requisição​

Na versão de binário fixada (v0.39), tanto os eventos de nível de daemon quanto os registros por requisição caem em ~/.hermes/proxy/iron-proxy.log. O formato é JSON delimitado por linha. Filtre por um destino específico:

grep '"upstream":"openrouter.ai"' ~/.hermes/proxy/iron-proxy.log | tail -20

Ou acompanhe em tempo real:

tail -f ~/.hermes/proxy/iron-proxy.log | jq

Quando a versão fixada passar para a v0.40+ (que acrescenta log.audit_path), os registros por requisição vão migrar para ~/.hermes/proxy/audit.log e o iron-proxy.log vai guardar só os eventos de nível de daemon. Até essa subida, o audit.log é um espaço reservado vazio (criado antes da hora em 0o600, para o daemon futuro herdar permissões apertadas) — ligue o seu logrotate / suas ferramentas de monitoramento ao iron-proxy.log hoje e planeje acrescentar o audit.log depois da subida de versão.

Limitações (v1)​

  • Só o backend Docker. A ligação de Modal, Daytona e SSH vem depois, em PRs separados.
  • Provedores com autenticação baseada em assinatura (SigV4 da AWS, OAuth de conta de serviço do GCP) driblam o proxy por completo — veja Provedores não cobertos. Provedores com token em cabeçalho (bearer, x-api-key, api-key, x-goog-api-key) estão todos cobertos.
  • Não existe binário nativo de Windows no projeto original. Rode em Linux / macOS / WSL.
  • A CA é um certificado autoassinado de 10 anos na primeira geração. A rotação exige openssl genrsa ... na mão (ou esperar um trabalho posterior que acrescente hermes egress rotate-ca).
  • Rodar o setup de novo derruba um daemon em execução depois de reescrever configuração ou mapeamentos; reinicie (ou use hermes egress reload para mudanças só no conjunto de regras) e reinicie os sandboxes já em execução depois de uma rotação de tokens.
  • A zeragem de segredos em memória do iron-proxy é controlada lá em cima. Atacantes com o mesmo uid e acesso de leitura a /proc/<pid>/mem podem ler os segredos trocados na memória do daemon.
  • O iron-proxy v0.39 suporta apenas um único bind por daemon (nós fazemos bind no gateway da ponte do docker no Linux, em loopback no Docker Desktop) e combina os registros de daemon e por requisição num único fluxo de log. Quando lá em cima acrescentarem proxy.http_listens (no plural) e log.audit_path, uma subida de versão pode ligar o bind múltiplo e o fluxo dedicado de auditoria.

Veja também​