Hardening do Gateway — ferramentas de execução com política de segurança
As ferramentas de execução do GarraIA (
bash,file_read,file_write,web_fetch,schedule_heartbeat) já vêm ativas por padrão — não há flag para "ligá-las". O que existe, e este guia cobre, é a política em volta delas: quem alcança o gateway, o que exige confirmação, o que é bloqueado sempre, e como adicionar novas capacidades (via MCP) sem abrir a máquina.
Complementa (não substitui): docs/security.md, checklist de segurança e .env.example.
1. Os dois perfis de exposição
Perfil A — loopback (default, recomendado)
O bind vem da linha de comando: --host/HOST e --port/PORT, senao
127.0.0.1:3888 — so a propria maquina alcanca. gateway.host e
gateway.port no arquivo estao deprecados e nunca foram lidos pelo
garraia start (#1261); nao os escreva.
Com bind em loopback, a superfície de rede é zero para terceiros. Os canais de mensageria (Telegram/Discord/…) continuam funcionando — eles fazem egress para as APIs dos provedores; ninguém precisa alcançar a sua porta 3888. Se você não tem um motivo concreto para expor o gateway, este perfil encerra o assunto.
Atenção: a env var HOST define o bind (runtimes de container
costumam setar HOST=0.0.0.0). Desde a v0.4.5 um bind nao-loopback sem
credencial de gateway recusa o boot (#1261) — se o garraia start saiu
com refusing to start, procure por essa variavel no ambiente.
Perfil B — exposto na rede (0.0.0.0) — requisitos mínimos
Um gateway exposto sem autenticação é execução remota de comandos
aberta: qualquer um que alcance a porta usa o tool bash. Por isso,
desde a v0.4.5, o garraia start recusa um bind nao-loopback sem
credencial (#1261; exit 78, com a mensagem de como corrigir). Suba com
HOST=0.0.0.0 garraia start (ou --host 0.0.0.0) e TODOS os itens abaixo:
gateway:
# Valor LITERAL no arquivo — ou, sem editar o arquivo, a env
# GARRAIA_GATEWAY_API_KEY, que vence o arquivo e nunca e gravada nele.
# Gere com `openssl rand -hex 32` e proteja o arquivo com chmod 0600:
api_key: "<token-forte-aqui>"
# NÃO adicione `session_tokens_required: true`: o flag NÃO está implementado
# (nenhuma rota HTTP valida token de sessão hoje) e o gateway RECUSA subir
# com ele ligado, em vez de fingir proteção. Ver "Limitações conhecidas" §2.
session_ttl_secs: 86400 # validade do token (1 dia)
session_idle_secs: 3600 # corte por inatividade (1h)
# vazio = nenhuma origem cross-origin (default seguro desde #1182). Se o
# console é alcançado por NOME DNS — reverse proxy com domínio próprio,
# mDNS `.local`, Tailscale, nome de serviço Docker, ingress —, liste a
# origem aqui; sem isso, POST/PATCH/DELETE e o WebSocket do chat vindos do
# navegador contra esse nome são recusados com 403 pela guarda anti-CSRF
# (`origin_guard`), e a resposta não carrega `Access-Control-Allow-Origin`.
# Por IP ou localhost nada é necessário. `*` não é aceito.
allowed_origins:
- "https://seu-dominio.exemplo"
rate_limit:
per_second: 1
burst_size: 60
- TLS: os binários release atuais não incluem a feature
tls(compilável a partir do código comcargo build -p garraia --features garraia-gateway/tls). O caminho suportado hoje é um reverse proxy com TLS (Caddy/nginx/Traefik) na frente do gateway em loopback — ou seja, muitas vezes o Perfil A + um proxy exposto resolve melhor que0.0.0.0direto. Interação com a guarda anti-CSRF (#1182), quebra conhecida e deliberada: a guarda ancora o anti-DNS-rebinding em "IP literal,localhostou nome declarado emallowed_origins" — então qualquer acesso ao console por nome DNS exige o nome na lista: reverse proxy com domínio próprio, mas também mDNS (http://nas.local:3888), Tailscale MagicDNS, nome de serviço Docker/Compose e ingress do Kubernetes. No reverse proxy há um agravante: o navegador mandaOrigin: https://seu-dominio.exemplo, o proxy repassaHost: seu-dominio.exemplo, mas o gateway por baixo falahttp— a comparação de esquema recusa antes mesmo da âncora. A correção é listar a origem emallowed_origins, com o esquema que o navegador vê: uma origem declarada ali é aceita como tal e destrava CORS, guarda e o handshake do WebSocket do chat (/ws) de uma vez. Sem isso,POST/PATCH/DELETEdo navegador voltam 403 e o chat do console não conecta — e note quegateway.api_keynão substitui a lista: a guarda roda igual com a chave configurada, de propósito (um bearer válido não dispensa a origem certa, senão o CSRF voltaria contra um dono autenticado). É o preço de fechar o CSRF do perfil default (loopback, sem chave), que é a instalação da esmagadora maioria. As receitas de proxy dodocs/src/production-runbook.md§2 já trazem a linha. - Métricas: mantenha
GARRAIA_METRICS_BIND=127.0.0.1:9464e useGARRAIA_METRICS_TOKEN/GARRAIA_METRICS_ALLOWse precisar raspar de fora (ver.env.example). - Verifique com
garraia config checkapós editar — ele valida o schema e reporta a precedência efetiva. Desde a v0.4.5 o mesmo check roda em todogarraia start/restart/start -d(#1247): os achados vão para o log, e TLS configurado pela metade (sótls_cert_pathou sótls_key_path) recusa o boot com exit 78 em vez de servir HTTP puro em silêncio. Escotilha consciente:GARRAIA_ALLOW_INVALID_CONFIG=1(exatamente1).
2. Confirmação humana para comandos arriscados
O tool bash tem dois tiers de proteção
(crates/garraia-common/src/safety_gate.rs):
| Tier | Config | Exemplos | Comportamento |
|---|---|---|---|
| DENY_LIST (perigoso) | sempre ativo, sem opt-out | rm -rf /, fork bomb, mkfs, dd if=, shutdown, git push --force origin main, curl … | sh | Bloqueado sempre, mesmo com confirmação desligada |
| CONFIRM_LIST (arriscado) | agent.tool_confirmation_enabled: true | rm -r, git reset --hard, drop table, truncate, kill, taskkill | Pausa e pede "sim"/"yes" apenas se a flag estiver ligada |
O default é false — o hard-block continua valendo, mas comandos
"arriscados" executam sem perguntar. Para um agente com bash em
máquina pessoal, ligue:
agent:
tool_confirmation_enabled: true
max_tool_calls: 50 # teto de chamadas de tool por tarefa
Como o "sim" e retomado (#1343). Antes da v0.4.5 a pausa era terminal em todo canal de producao: o historico e guardado como texto, o pedido pausado nao voltava no turno seguinte e o "sim" nunca aprovava. Agora o gateway guarda o pedido em memoria e o "sim" (a mensagem inteira) da mensagem seguinte roda o pedido uma vez, dentro de 5 minutos, e so se vier do mesmo remetente, na mesma sessao e no mesmo canal:
| Caminho | Quem pode aprovar |
|---|---|
Web Console (/ws) e desktop (/ws/parrot) | a mesma conexao WebSocket — reconectou (ou retomou a sessao em outra aba), pergunta de novo |
/v1/chat/completions | o dono da allowlist com o mesmo Authorization e a mesma X-Session-Id nos dois requests — sem X-Session-Id cada request e uma sessao nova e o "sim" nao retoma; sem dono reivindicado, a pausa e terminal. Vale igual com e sem "stream": true. A X-Session-Id so alcanca sessao das superficies locais do operador (api, vscode, web, parrot): o id de uma sessao de canal ou do mobile responde 404 e nao e tocado (#1462) |
App mobile (POST /chat) | o sub do JWT |
| Telegram, Discord, Slack, WhatsApp Cloud, Matrix, IRC, Signal, LINE, Teams, Google Chat, iMessage, WhatsApp pessoal | o id do usuario na plataforma; em grupo, o "sim" de outro membro nao aprova e encerra o pedido |
garraia chat | o proprio terminal, na mesma sessao |
Continuam sem retomada, e ali a pausa e terminal de proposito: A2A,
OpenClaw, POST /api/sessions/{id}/messages, a resposta do agente no chat
do workspace (rest_v1), as tarefas agendadas (process_heartbeat),
garraia ask e o garra_agent do garraia mcp-server — ou quem fala e
outro agente, ou nao ha remetente que o servidor possa provar. Qualquer mensagem no meio encerra o pedido, e
reiniciar o gateway cancela todos. Detalhes em
security/threat-model.md §5.16.
3. Restringir tools por contexto: modos (ToolPolicy)
Cada modo de execução carrega uma allow/deny-list de tools
(crates/garraia-agents/src/modes.rs) — este é o mecanismo legítimo
para "menos poder por padrão":
| Modo | Tools permitidas |
|---|---|
ask (default nos messengers) | nenhuma — só conversa |
search | file_read, repo_search (bash read-only) |
review | file_read, git_diff |
edit | file_read, file_write (sem bash) |
code | file_read, file_write, bash |
debug | file_read, bash, repo_search (sem write) |
auto / orchestrator | sem restrição de lista |
Precedência: header X-Agent-Mode > comando /mode > default do canal
(Telegram/Discord/WhatsApp → ask; web/API → auto) > ask. Na
prática: nos canais de chat o agente nasce sem tools e só ganha
poder quando alguém pede um modo explicitamente — mantenha assim; evite
fixar code/auto como modo permanente em canal público.
4. Quem pode falar com o agente: allowlist + pareamento
Usuários desconhecidos nos canais precisam de código de pareamento
(deny-by-default). O arquivo é único e global —
<config_dir>/allowlist.json (ex.: ~/.garraia/allowlist.json ou
~/.config/garraia/allowlist.json) — e vale para todos os canais:
um usuário aprovado no Telegram também está aprovado no Discord. O
primeiro usuário a parear vira owner. Revise a lista periodicamente
(/users no chat).
5. Secrets: cofre para MCP e chaves de provider
- Exporte
GARRAIA_VAULT_PASSPHRASEno ambiente do gateway. Com ela, env vars sensíveis dos servidores MCP (key,token,secret,password,auth,credential,passno nome) são movidas para o cofre AES-256-GCM no primeiro save e omcp.jsonguarda apenas referênciasvault:mcp.<server>.<KEY>. Sem a passphrase, caem em plaintext com warning. - O default do wizard
garra initpara chaves de provider éconfig.yml(modo 0600). Escolha a opção do cofre no wizard e mantenha a passphrase exportada a cada start para ter criptografia em repouso.
6. Adicionando capacidades via MCP — com política
Novas capacidades (navegar em browser real, banco de dados, filesystem
com escopo) entram como servidores MCP, cada um atrás da mesma
política. Receitas prontas e regras por servidor (allowed_tools,
memory_limit_mb, secrets via vault) em
docs/integrations/mcp-capacidades.md.
Regra de ouro: não adicione um segundo shell via MCP — o bash
nativo já existe e passa pelo safety_gate; um shell MCP contornaria os
dois tiers.
7. Config de referência endurecido
Arquivo completo comentado, validado com garra config check:
config.hardened.example.yml.
Limitações conhecidas (honestas)
-
gateway.api_keynão tem interpolação de env no arquivo, mas desde a v0.4.5 a envGARRAIA_GATEWAY_API_KEYentrega a credencial sem editar o arquivo (vence o arquivo, nunca é gravada nele; #1261). O.env.exampletraz a linhaGARRAIA_GATEWAY_API_KEY=vazia de proposito (placeholder seria chave conhecida por todos; vazia conta como ausente e o container recusa subir): preencha comopenssl rand -hex 32antes do primeirodocker compose up. Deploy aberto de proposito atras de proxy que autentica:gateway.allow_unauthenticated_network_bind: true(so no arquivo, sem env nem flag; aviso alto em todo boot). -
Auth do gateway local: com
gateway.api_keydefinido, o gate cobre os WebSockets/wse/ws/parrote o REST/api/*(#1045; o/ws/parrotdesde a auditoria R4 do PR #1251). Sem a chave definida, nada é exigido — é o comportamento de sempre, e é o adequado para um gateway em loopback.Três rotas de
/api/continuam abertas mesmo com a chave, por necessidade:/api/healthe/api/capabilities, que o onboarding do app consulta antes de o usuário ter digitado a chave, e/api/auth-check, que é como o console web descobre que precisa pedi-la. As três são secret-free.Desde a #1240 o gate também cobre o plano de conversa e o A2A:
POST /v1/chat/completions,POST /v1/messages,POST /v1/messages/count_tokense todo o/a2a/*(por prefixo). Eram a lacuna mais cara que existia: montadas no mesmo router cru que/api/*, sem resolução de identidade nenhuma, e é por elas que o runtime executa as tools do GarraIA na máquina do dono. Quem configurava a chave acreditava tê-las fechado.E o socket do papagaio (
/ws/parrot), desde a auditoria R4 do PR #1251. Ele roda um turno completo do agente — com as tools e a chave de LLM do dono — sobre a sessão persistente do desktop, e até então a única guarda da rota era o anti-CSRF da #1182, que passa de propósito quando não há headerOrigin: cliente não-navegador (app, CLI,curl) não manda um. Com a chave configurada e o gateway em0.0.0.0— o cenário do §2 —, umwebsocat ws://host:3888/ws/parrotconectava sem credencial nenhuma e dirigia o agente. O irmão/ws, montado na linha de cima dorouter.rs, já checava a chave.A checagem do
/ws/parrotmora dentro do handler, e não na lista de caminhos do middleware, por um motivo que não é óbvio: o middleware só lê header, e a webview Tauri abre o overlay comnew WebSocket(...), que não consegue mandar header nenhum. Gatear a rota no middleware fecharia o Garra Desktop em vez de autenticá-lo. Como no/ws, a chave é aceita por?token=/?api_key=— e também porAuthorization: Bearer, para o cliente de CLI que consegue mandá-lo.Continuam abertas, por serem descoberta e não execução:
/v1/modelse/.well-known/agent.json, além de/healthe/ping. As rotas/v1/*do workspace (rest_v1) e o/v1/auth/*seguem fora deste eixo — têm autenticação JWT própria, e/admin/*tem cookie de sessão.Sessão por id — duas leituras, duas credenciais (#1462). Um id de sessão que o cliente escolhe (
X-Session-Id, o{id}de/api/sessions/{id}/*, oresumesem token do/ws) só alcança sessão das superfícies locais do operador (api,vscode,web,parrot); sessão de canal (WhatsApp, Telegram, Discord…) ou do mobile responde o mesmo404de id inexistente — na leitura e na escrita, em memória e nosessions.db— eGET /api/sessionssó lista as locais. Quem precisa ler qualquer sessão (o Export do Web Console, por exemplo) usaGET /admin/api/sessions/{id}/history, com o cookie de sessão do/admine a permissãomanage_sessions(admineoperator;viewerrecebe 403), que lê sem hidratar e deixa registro na auditoria. A chave degateway.api_keycontinua sendo a fronteira de rede do/api/*, mas não substitui esta regra: é uma chave única compartilhada por toda a LAN, e os ids de canal são adivinháveis por construção. A politica de acesso do WhatsApp pessoal segue o mesmo desenho:GET|POST /admin/api/whatsapp/accesseGET /admin/api/whatsapp/access/audit(cookie + CSRF; leituraChannels/Read, mutacaoChannels/Update), pelo mesmo motor e com o mesmo audit da CLI (ADR 0025). A API nunca revela identidade. OGET /admin/api/whatsapp/doctor(#1420,Channels/Read) roda o motor dogarraia doctor whatsappem processo e devolve so contagens, origens e nomes — nunca chave de sessao, chave de API, numero ou LID.A #1240 não fechou nada por default. Sem a chave configurada, todas essas rotas respondem exatamente como antes.
A chave vai só no header
Authorization: Bearerno REST — com uma alternativa nas duas rotas compat Anthropic (/v1/messages,/v1/messages/count_tokens), que aceitam tambémx-api-key, porque o Claude Code e o SDK da Anthropic nunca mandam bearer. Na query string ela é aceita apenas pelos dois handshakes de WebSocket (/wse/ws/parrot), porque o handshake WebSocket de um navegador não permite header; no REST, chave em query acabaria em log de acesso e no span de tracing.session_tokens_requirednão está implementado: o middleware nunca foi ligado ao router, e desde a investigação da issue #930 o gateway recusa subir com o flag emtrueem vez de afirmar uma proteção inexistente. -
"Controle de aplicativos" (GUI/automação de apps) não existe como tool — ver a nota em mcp-capacidades.md.
-
TLS embutido exige build com
--features garraia-gateway/tls; binários release atuais servem HTTP puro (use reverse proxy).