# Acesso SSH para agentes de IA: comandos remotos sem chaves privadas

Entregar uma chave privada SSH a um agente de programação de IA é a abstração errada. O agente não precisa de uma credencial duradoura que possa autenticar a partir de qualquer máquina a qualquer momento. Ele precisa de permissão para executar uma ação remota específica, enquanto você ainda consegue enxergar, interromper e explicar essa ação.

Isso parece uma pequena mudança de arquitetura. Não é. A mudança separa um segredo que pode viajar para sempre de uma solicitação SSH que tem um solicitante, um destino, um comando, um resultado e um responsável.

Já vi equipes passarem dias reforçando regras de prompt para um agente e, ao mesmo tempo, deixarem `~/.ssh/id_ed25519` legível pelo mesmo processo. As regras do prompt viram decoração quando a chave privada está disponível. Um comando de shell pode copiar o arquivo, imprimi-lo, arquivá-lo ou enviá-lo para algum lugar que você só descobrirá semanas depois.

Uma chave privada não é um token de API com um nome mais curto. Ela é um token de autoridade reutilizável para todos os servidores SSH que aceitam sua parte pública.

## Dê ao agente uma ação, não uma identidade

O agente deve solicitar uma operação SSH, enquanto um componente confiável separado mantém a identidade e estabelece a conexão. Esse componente recebe dados estruturados, como destino, conta remota, comando, argumentos e tempo limite. Ele verifica a solicitação, autentica no servidor, captura o resultado e retorna apenas o resultado ao agente.

O segredo permanece em uma única máquina. Mais importante, o direito de usá-lo deixa de acompanhar o processo do agente por onde ele passar.

Essa distinção muda o modo como as falhas acontecem. Se um agente for comprometido ou seguir instruções hostis de um repositório, ele ainda poderá solicitar um comando ruim. Mas não conseguirá exportar a chave privada silenciosamente e reutilizá-la em uma VM descartável no mês seguinte. Você reduz um problema aberto de roubo de credenciais a um problema de autorização e controle de comandos.

O segundo problema ainda merece respeito. A diferença é que agora você consegue operá-lo.

Um formato de solicitação útil deve ser deliberadamente sem graça:

```json
{
  "host": "deploy-01.internal.example",
  "user": "release",
  "command": "/usr/local/libexec/release-service",
  "args": ["api", "2025.03.08-4f2c1a7"],
  "timeout_seconds": 120
}
```

Não envie uma string de shell como `ssh deploy-01 'cd /srv/api && git pull && sudo systemctl restart api'` por cinco camadas e chame isso de controle. As regras de análise do shell passam a fazer parte do seu limite de segurança, e quase ninguém as revisa dessa forma. Mantenha o caminho do comando fixo, passe os argumentos como valores separados e faça o wrapper remoto rejeitar valores fora da gramática esperada.

Prefiro um contrato de ação um pouco incômodo de usar a um terminal remoto flexível que ninguém consegue limitar. O incômodo aparece durante a configuração. O dano de um terminal sem restrições aparece mais tarde.

O broker deve retornar um resultado com estrutura suficiente para que o agente continue o trabalho sem precisar adivinhar:

```json
{
  "exit_code": 0,
  "stdout": "released api version 2025.03.08-4f2c1a7\n",
  "stderr": "",
  "duration_ms": 1842
}
```

Não retorne credenciais, sockets de agente, conteúdo de `known_hosts` nem um TTY interativo. Esses são detalhes de implementação do lado confiável.

## Posse, assinatura e execução são poderes diferentes

As equipes costumam usar «acesso SSH» como se isso descrevesse uma única coisa. Na prática, há pelo menos três poderes materialmente diferentes: possuir uma chave privada, pedir que um agente crie uma assinatura e fazer com que um broker execute um comando remoto nomeado.

A posse da chave privada é o poder mais amplo. Quem lê o arquivo pode copiá-lo indefinidamente e tentar usá-lo em todos os servidores acessíveis. Permissões de arquivo ajudam, criptografia de disco ajuda e senhas ajudam, mas nada disso responde à pergunta central: por que um processo de IA precisou de uma credencial portátil em primeiro lugar?

Um `ssh-agent` elimina a necessidade de que cada cliente leia um arquivo de chave privada. O processo pode solicitar uma assinatura por meio de `SSH_AUTH_SOCK`. Isso melhora a higiene da estação de trabalho local, mas a assinatura ainda concede poder de autenticação. O manual `ssh-agent(1)` do OpenSSH diz que o agente mantém as chaves privadas fora do tráfego de rede durante o encaminhamento, enquanto o solicitante encaminhado recebe o resultado das operações de identidade. É uma propriedade útil, mas não um modelo completo de autorização.

A execução de ações é mais restrita quando você a constrói dessa forma. Um broker pode recusar um host desconhecido, impedir outra conta remota, exigir uma decisão humana, limitar o tempo de execução, recusar um TTY e manter um registro da chamada. Ele também pode usar uma identidade SSH diferente para cada classe de operação.

Esse último detalhe importa mais do que as equipes imaginam. Uma identidade que pode ler logs, implantar código, editar `/etc/sudoers` e abrir túneis cria um limite de autorização do tamanho de toda a sua frota. Quatro identidades com restrições estreitas no servidor criam quatro problemas menores.

Prefiro rotacionar quatro identidades com escopo restrito depois de um erro a passar um fim de semana tentando provar que uma identidade de administrador não chegou a todos os hosts.

Esse padrão não torna um comando prejudicial inofensivo. Ele oferece pontos onde você pode capturá-lo: antes da conexão, durante a autenticação no servidor, dentro do comando remoto forçado e no registro posterior.

## O encaminhamento do agente SSH resolve outro problema

O encaminhamento do agente SSH evita copiar uma chave privada para um host de salto, mas não torna seguro entregar uma máquina remota a um agente. O host remoto recebe acesso a um socket que pode solicitar operações ao agente de autenticação local.

O OpenSSH afirma isso claramente em `ssh_config(5)`: o encaminhamento do agente deve ser ativado com cautela, porque um usuário capaz de contornar as permissões no host remoto pode usar o agente local pela conexão encaminhada. O invasor não consegue extrair os bytes da chave privada por essa interface, mas pode pedir que as identidades carregadas façam a autenticação.

Essa ressalva fica ainda mais séria com ferramentas autônomas. Um agente pode se conectar a um host porque uma tarefa pediu que ele inspecionasse um log. Uma instrução maliciosa do repositório, uma conta remota comprometida ou um comando mal isolado pode então encontrar `SSH_AUTH_SOCK` e usar a capacidade de assinatura encaminhada. A chave privada continua tecnicamente secreta, enquanto a autoridade de autenticação é abusada. Essa distinção não trará muito conforto à vítima.

Não ative `ForwardAgent yes` globalmente. Coloque `ForwardAgent no` na configuração básica do cliente e adicione uma exceção explícita apenas para um fluxo de trabalho humano nomeado que realmente precise desse recurso.

```sshconfig
Host *
    ForwardAgent no
    AddKeysToAgent no
    IdentitiesOnly yes
    StrictHostKeyChecking yes

Host legacy-bastion
    HostName bastion.internal.example
    User ops
    ForwardAgent yes
```

A exceção acima ainda precisa de uma justificativa, um responsável e uma data para ser removida. Uma exceção permanente de encaminhamento tende a ficar invisível porque o SSH faz tudo funcionar com muita facilidade.

Identidades limitadas ao destino melhoram essa situação, mas não substituem um limite de ação. O manual `ssh-add(1)` do OpenSSH explica que as restrições de destino verificam todo o caminho da conexão quando um cliente e um servidor compatíveis encaminham o agente. O manual também alerta que alguém com um `SSH_AUTH_SOCK` remoto pode encaminhar esse socket novamente, embora o uso continue limitado aos destinos permitidos.

Use restrições de destino quando fizer sentido. Não as confunda com uma política de comandos. Elas dizem onde uma identidade pode se autenticar, não se `rm -rf /srv/release-cache` deve ser executado depois que ela chegar ao destino.

## Acompanhe a falha por todo o caminho do comando

Um incidente plausível começa com um atalho bem-intencionado: um desenvolvedor coloca uma chave privada de implantação em uma variável de ambiente porque o agente de programação precisa executar um único comando de release. O agente lê uma issue do repositório que inclui um comando de diagnóstico sugerido. O comando imprime o ambiente para solucionar o problema, e a transcrição da execução acaba em um log de sessão local.

A primeira perda aconteceu antes do SSH. A chave privada entrou em um processo que não precisava mantê-la.

Agora o agente tem várias formas de vazá-la ou reutilizá-la. Ele pode gravar o valor em um arquivo temporário. Pode enviá-lo a um host remoto com `scp`. Pode colocá-lo em um commit do Git. Pode entregá-lo a um subprocesso fora dos limites que você imaginava. Uma chave privada copiada uma vez não expira apenas porque você encerrou aquela execução do agente.

Suponha que a equipe tenha usado o encaminhamento do agente. A instrução do repositório manda o agente se conectar a `build-02` e depois executa um comando que expõe o socket encaminhado a um processo local naquela máquina remota. O invasor não consegue imprimir a chave privada, mas pode pedir ao socket que se autentique em outro host usando uma identidade carregada no agente do desenvolvedor. O encaminhamento nunca foi pensado como um mecanismo geral de delegação, mas acabou se tornando um.

Agora considere o projeto com broker. O agente envia uma solicitação para `release-service api 2025.03.08-4f2c1a7` em `deploy-01`. O broker percebe que essa é a primeira solicitação de um novo processo de agente e pede autorização. A pessoa que aprova vê a autoridade de assinatura de código do processo solicitante, a identidade remota e o destino. O broker se conecta apenas ao host nomeado. O servidor aceita somente uma identidade de implantação restrita e invoca um único wrapper do lado do servidor.

A instrução hostil ainda pode solicitar um release. Ela não consegue transformar essa solicitação em um shell de login, um encaminhamento de porta, uma chave privada copiada ou uma passagem por servidores sem relação com a tarefa.

Isso reduz muito o raio de impacto, mas custa flexibilidade. Um agente genérico pode improvisar diante de estados incomuns em produção. Uma interface de ações restrita não pode. Você precisará adicionar operações deliberadas para logs, status de serviço, rollback, verificações de migração e limpeza de artefatos. Alguém precisa ser responsável por essas interfaces. Esse trabalho não é opcional; o SSH irrestrito apenas o esconde.

A resposta correta é fazer com que o caminho seguro cubra as tarefas que as pessoas realmente executam, em vez de reabrir um shell bruto sempre que o caminho seguro não tiver um recurso.

## Coloque o cliente SSH atrás de um ponto local de decisão

Um ponto local de decisão deve ser responsável por três coisas: a identidade privada criptografada, a permissão para usá-la e as evidências produzidas quando ela é usada. O agente não deve ser responsável por nenhuma delas.

O Sallyport segue esse padrão para agentes no macOS: seu shim `sp mcp` incluído aceita chamadas MCP comuns, enquanto o aplicativo da barra de menus mantém as identidades SSH em seu cofre criptografado e usa `sp-ssh` para realizar a conexão. O agente recebe o resultado do comando, não a credencial.

A estrutura é mais importante que a implementação específica:

```text
Processo do agente de IA
    -> solicitação de ação local
        -> decisão de autorização
            -> auxiliar SSH usando identidade protegida
                -> sshd remoto e conta restrita
                    -> stdout, stderr, status de saída
```

Mantenha o ponto de decisão na máquina gerenciada do desenvolvedor quando uma pessoa precisar supervisionar o trabalho. Não o transforme em um proxy de rede que assina silenciosamente todas as solicitações de todos os processos. O limite de processo local permite atribuir a solicitação ao chamador e rejeitar um executável inesperado antes que qualquer conexão de rede comece.

A identidade do processo não é um campo decorativo em um cartão de aprovação. Uma solicitação do aplicativo assinado que você esperava é diferente de uma solicitação de um binário não assinado iniciado a partir de `/tmp`, mesmo que ambos afirmem ser «o agente de programação». No macOS, a autoridade de assinatura do código oferece ao responsável pela aprovação algo concreto para avaliar.

Quero que um novo processo conquiste uma sessão antes de poder usar uma identidade SSH. Também quero que essa sessão desapareça quando o processo terminar. Concessões de longa duração são fáceis de operar e difíceis de explicar depois de um comprometimento.

O bloqueio do cofre deve negar todas as ações SSH. Não deve haver um fallback parcial para uma chave privada em cache, um arquivo PEM exportado ou um processo auxiliar com sua própria cópia oculta. Se o seu caminho de emergência contornar a proteção, ele acabará se tornando o caminho normal.

## Torne a conta remota deliberadamente sem graça

O servidor remoto precisa limitar o que uma autenticação SSH bem-sucedida pode fazer. Uma chave privada protegida é apenas metade do controle; o servidor deve tratar essa identidade como algo criado para uma finalidade específica, não como uma conta de administrador humano.

Crie uma conta Unix separada para a ação, como `release`, `diagnostics` ou `backup`. Não dê a ela um perfil pessoal de shell, um diretório home amplo nem login por senha. Não coloque sua identidade pública ao lado da identidade pessoal irrestrita de um engenheiro no mesmo arquivo `authorized_keys` e chame as contas de separadas.

Para uma operação de release, uma entrada em `authorized_keys` pode ter esta aparência:

```text
restrict,command="/usr/local/libexec/release-service" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleOnlyReplaceThis release-broker
```

A opção `restrict` desativa o encaminhamento de portas, de agente e de X11, a alocação de TTY e a execução de `~/.ssh/rc`. O manual `sshd(8)` do OpenSSH documenta `restrict` especificamente para esse fim e mostra seu uso com um comando forçado.

O comando forçado não deve entregar a linha de comando original a `sh -c`. Ele deve ignorar `SSH_ORIGINAL_COMMAND`, a menos que você tenha um analisador restrito com testes. O wrapper deve aceitar um protocolo fixo, validar cada campo, gravar uma entrada de auditoria e invocar a operação real com limites explícitos para os argumentos.

Um pequeno wrapper de shell é aceitável se continuar pequeno:

```sh
#!/bin/sh
set -eu

service=${1:-}
version=${2:-}

case "$service" in
  api|worker) ;;
  *) echo "unsupported service" >&2; exit 64 ;;
esac

case "$version" in
  *[!0-9A-Za-z._-]*|"") echo "invalid version" >&2; exit 64 ;;
esac

exec /usr/bin/sudo /usr/local/sbin/deploy-approved-release "$service" "$version"
```

Chame esse wrapper com argumentos fixos a partir do broker. Não permita que o cliente escolha o caminho. Se precisar de mais operações, crie mais wrappers ou um pequeno dispatcher de comandos que aceite subcomandos nomeados e rejeite todo o resto.

É aqui que estabeleço um limite rígido: não conecte um agente autônomo a uma conta compartilhada `ops` com um shell normal e tente compensar isso com um prompt melhor. Uma conta de shell compartilhada torna voluntária toda proteção do lado do servidor.

Para trabalhos com estado, use operações idempotentes. Um wrapper de release deve verificar se o artefato solicitado já está ativo, se o serviço está saudável e se existe um ponto de rollback antes de alterar qualquer coisa. O agente pode repetir uma solicitação depois de um timeout; sua operação remota não deve interpretar a repetição como permissão para executar a ação duas vezes.

## A verificação do host e a gramática dos comandos exigem o mesmo cuidado

Uma identidade privada protege a autenticação do cliente. Ela não prova que o agente chegou ao servidor pretendido. Se sua automação aceitar qualquer nova impressão digital de host, um erro de DNS, um arquivo hosts envenenado ou um invasor ativo na rede poderá redirecionar a conexão para um servidor que receberá seu comando e conhecerá sua saída.

Use `StrictHostKeyChecking yes` para identidades de automação. Distribua impressões digitais de host revisadas por meio do gerenciamento de configuração ou mantenha um arquivo `known_hosts` cuidadosamente controlado. Quando um host trocar sua chave de host SSH, faça uma alteração explícita com uma etapa de verificação fora de banda. Não ensine o agente a responder «yes» ao aviso da chave do host.

Um perfil mínimo de cliente para uma ação não interativa costuma ter esta aparência:

```sshconfig
Host deploy-01.internal.example
    HostName deploy-01.internal.example
    User release
    IdentityAgent /path/to/broker.sock
    IdentitiesOnly yes
    StrictHostKeyChecking yes
    UserKnownHostsFile /Library/Application Support/agent-ssh/known_hosts
    BatchMode yes
    RequestTTY no
    ForwardAgent no
```

`IdentitiesOnly yes` impede que o SSH envie ao destino todas as identidades disponíveis por meio de um agente. Isso reduz tentativas de autenticação desnecessárias e evita o uso acidental de uma identidade pessoal quando a identidade da ação pretendida falhar. O manual `ssh_config(5)` do OpenSSH descreve essa opção como uma forma de usar os arquivos de identidade configurados, em vez das identidades extras oferecidas por um agente ou provedor.

Não permita que o agente escolha os nomes dos hosts se puder evitar. Uma solicitação deve se referir a um destino lógico aprovado, como `production-api-release`, e o lado confiável deve mapear esse nome para um hostname, uma conta remota, uma impressão digital de host, uma identidade e uma família de comandos. O mapa é uma política no sentido comum, mas deve ser composto por dados que uma pessoa possa inspecionar, não por uma linguagem engenhosa que incentive brechas.

A gramática dos comandos exige a mesma contenção. Permitir argumentos arbitrários para um binário fixo ainda pode resultar em execução arbitrária se o binário aceitar caminhos de arquivos, nomes de plugins, importações de configuração ou flags `--exec`. Inspecione a CLI real. Escreva quais tokens são permitidos. Teste entradas malformadas, espaços em branco, metacaracteres do shell, surpresas de Unicode e flags repetidas.

Vinte minutos tentando abusar do seu próprio wrapper com argumentos estranhos revelarão mais do que outro aviso de aprovação.

## Aprove a autoridade no momento em que ela se torna útil

A aprovação humana deve ficar no limite em que um processo ganha uma capacidade SSH pela primeira vez, não depois de cada comando inofensivo e nem apenas depois que um comando destrutivo já chegou ao servidor.

Uma aprovação por sessão é um padrão prático para uma execução de agente. A primeira ação SSH tentada por um novo processo dispara uma decisão. A aprovação deve informar qual processo assinado fez a solicitação, qual identidade deseja usar e qual classe de acesso remoto receberá. Se aprovada, essa mesma execução poderá fazer solicitações permitidas até terminar.

Isso evita transformar diagnósticos rotineiros em uma pilha de cliques. Também evita a alternativa pior: uma aprovação que se aplica a todos os processos futuros chamados `node`, `python` ou `claude` na máquina.

Algumas identidades merecem uma segunda barreira a cada uso. Implantação em produção, reinicialização de emergência, exportação de banco de dados, alteração de infraestrutura e comandos que podem mudar a acessibilidade da rede são candidatos razoáveis. O custo é a interrupção. O benefício é que uma sessão comprometida não consegue usar silenciosamente a identidade mais perigosa depois que você aprovou uma verificação de status de baixo risco.

O Sallyport pode exigir aprovação por chamada para uma identidade SSH específica, enquanto sua autorização normal de sessão associa a concessão a um processo de agente determinado. A barreira do cofre também nega ações enquanto ele está bloqueado, em vez de permitir que um agente em segundo plano continue operando depois que a pessoa se afasta.

O aviso de aprovação não deve pedir que os usuários leiam um parágrafo de JSON serializado. Mostre um resumo conciso da ação e deixe os detalhes disponíveis: destino, conta remota, nome fixo do comando, argumentos fornecidos e tempo limite. As pessoas identificam rapidamente `production-db` e `staging-db`. Elas não conseguem reconhecer o perigo de forma confiável em um pipeline completo de shell depois do décimo aviso.

Uma negativa deve ser uma saída de primeira classe. O agente precisa receber um resultado explícito de recusa para poder informar que não tinha permissão, em vez de tentar novamente com outra conta, outro host ou uma solução alternativa. Tentativas repetidas depois de uma recusa devem deixar um registro visível.

## Um log de auditoria precisa sobreviver ao solicitante comprometido

Os logs gerados pelo agente não são suficientes. Se o processo do agente puder escrever ou apagar o registro de suas próprias chamadas SSH, ele poderá deixar para trás a versão agradável da história.

Registre as atividades fora do processo do agente. No mínimo, mantenha a identidade do processo solicitante, o identificador da sessão, o horário, o destino, a conta remota, o nome da ação, os argumentos depois das regras de redação, o resultado da autorização, o código de saída, a duração e uma captura limitada de stdout e stderr. Capture as primeiras e as últimas 50 linhas de comandos barulhentos ou armazene um hash do conteúdo junto de um artefato completo protegido quando a saída puder conter segredos.

Não registre chaves privadas brutas, tokens bearer nem variáveis de ambiente arbitrárias. Um sistema de auditoria que cria um segundo vazamento de credenciais é pior que a ausência de um sistema de auditoria, porque as pessoas confiarão nele.

A evidência contra adulteração muda a forma como você investiga. Um log encadeado pode revelar que alguém removeu ou reordenou registros depois do fato, enquanto um arquivo de texto normal, mesmo somente para anexação, geralmente prova apenas que um arquivo existe. A verificação deve funcionar sem exigir que o cofre esteja desbloqueado; caso contrário, você não conseguirá examinar o registro justamente quando o sistema estiver no estado em que mais precisa de evidências.

A orientação de avaliação de red team da CISA oferece um motivo concreto para levar a contenção de credenciais a sério. A avaliação publicada descreve uma equipe obtendo dezenas de chaves SSH privadas em arquivos acessíveis e usando identidades privilegiadas para se mover entre sistemas Linux. A lição não é que todo agente se tornará um invasor. A lição é que material SSH reutilizável se transforma em um inventário de movimentação lateral quando chega a lugares que não precisam dele.

Quando um comando inesperado aparecer, revogue primeiro a sessão do agente. Depois, desative a identidade afetada no servidor, analise o registro de atividades verificado e revise os logs do `sshd` e do sistema no host de destino. Faça a rotação da identidade privada se houver qualquer possibilidade de ela ter saído do limite protegido. Um modelo com broker torna essa sequência mais rápida, porque revogar uma sessão interrompe futuras solicitações de ação sem esperar que uma chave privada copiada seja encontrada.

## Use quatro caminhos estreitos em vez de um shell geral

A implementação prática começa com um inventário das ações remotas que seus agentes já tentam executar, classificadas por consequência. Não comece perguntando de quais chaves privadas eles precisam. Essa pergunta começa pelo lado errado.

A maioria das equipes encontra um pequeno conjunto de operações recorrentes:

- Ler o status limitado de um serviço ou um trecho recente do log.
- Implantar um artefato já criado em um ambiente.
- Executar uma verificação de migração que não altera dados.
- Reiniciar um serviço nomeado depois de um release.
- Coletar diagnósticos para um job que falhou.

Crie um caminho de ação para cada uma. Atribua uma conta ou identidade remota dedicada quando os servidores forem diferentes. Force o comando no lado do servidor. Fixe a identidade do host. Decida se a ação precisa de aprovação por sessão ou a cada uso. Registre as chamadas permitidas e recusadas.

Mantenha um caminho SSH humano para trabalhos de reparo incomuns. Às vezes, pessoas precisam de um shell interativo durante um incidente, e fingir o contrário as empurra para saídas inseguras. Esse caminho deve usar uma identidade pessoal separada, MFA quando disponível, um host de salto quando apropriado e os controles normais de incidentes. Ele não deve compartilhar a identidade de automação apenas porque isso economiza uma linha em `authorized_keys`.

O padrão mais seguro exige mais engenharia inicial: wrappers precisam de testes, alguém precisa ser responsável pelas impressões digitais dos hosts e o catálogo de ações precisa de manutenção. Assuma esse custo. A alternativa é entregar a um gerador probabilístico de texto uma credencial que pode sobreviver a todas as barreiras que você colocou em torno da tarefa atual.

Comece pela chave privada SSH mais próxima da produção. Remova-a do alcance do agente, substitua um comando de shell amplo por uma ação restrita e analise o registro depois de uma execução real. Esse primeiro limite mostrará exatamente onde o restante do trabalho está escondido.
