Como manter suas chaves SSH seguras no Linux, macOS e Windows

· 17 min de leitura

  • linux
  • security
  • ssh

Antes de começar

Este artigo é a parte prática de Chaves SSH por dentro: como funcionam, os tipos e os riscos. Lá estão o porquê de cada recomendação: o que a passphrase protege, o risco do agent forwarding, por que oferecer todas as chaves a todo servidor é ruim. Aqui está o que fazer, sistema por sistema.

Os comandos de Linux foram rodados com o OpenSSH 10.2p1 no Ubuntu 26.04, e os outputs são reais, com o nome de usuário trocado por um genérico, user. Os de macOS e Windows seguem a documentação da Apple, da Microsoft e dos projetos citados, mas não foram capturados aqui: compare cada passo com o que o seu sistema mostrar.

O que vale em todo sistema

  1. Use Ed25519, ou uma chave em hardware (ed25519-sk) se você tem uma chave FIDO2.
  2. Ponha passphrase em toda chave de uso pessoal. Sem ela, o arquivo é a credencial inteira. O outro artigo mostra como conferir as suas.
  3. Use uma chave por dispositivo. Notebook, desktop e servidor de CI, cada um com a sua.
  4. Deixe o agente guardar a chave por tempo limitado, como um dia de trabalho, e peça confirmação a cada uso se agentes de IA rodam comandos no seu terminal.
  5. Diga qual chave usar em cada servidor e não faça agent forwarding.
  6. Confira a host key na primeira conexão e quando ela mudar.
  7. Criptografe o disco: LUKS, FileVault ou BitLocker.
  8. Revise e revogue: as chaves cadastradas no GitHub e no GitLab, e o authorized_keys dos servidores.

Um ~/.ssh/config de partida

texto
Host github.com
  User git
  IdentityFile ~/.ssh/id_ed25519

Host servidor
  HostName 203.0.113.10
  User deploy
  IdentityFile ~/.ssh/id_ed25519

Host interno
  HostName 10.0.0.5
  ProxyJump servidor

Host *
  IdentitiesOnly yes
  ForwardAgent no
  AddKeysToAgent 8h
  • IdentitiesOnly yes faz o cliente oferecer só a chave configurada para aquele servidor (ou as padrão, se nenhuma for), em vez de todas as que estão no agente.
  • ForwardAgent no já é o padrão, mas deixar escrito evita que um bloco anterior ligue sem você perceber.
  • ProxyJump servidor faz o ssh interno atravessar o servidor sem levar o seu agente até ele.
  • AddKeysToAgent 8h põe a chave no agente na primeira vez que você digita a passphrase, e tira depois de oito horas: você digita uma vez por dia de trabalho, e a chave não passa a noite no agente.

O Host * fica no fim porque, no ssh_config, vale o primeiro valor encontrado para cada opção: os blocos específicos vêm antes, o genérico completa o que faltou. Para ver o resultado final de um host, use ssh -G:

bash
ssh -G servidor | grep -E '^(hostname|user|identityfile|identitiesonly|forwardagent|addkeystoagent) '
output
user deploy
hostname 203.0.113.10
identitiesonly yes
identityfile ~/.ssh/id_ed25519
addkeystoagent 28800
forwardagent no

Uma chave por dispositivo, e nada de backup

Diferente de uma chave de criptografia, perder uma chave SSH não faz você perder dados, só acesso. Por isso, em vez de fazer backup da chave (e espalhar mais cópias dela), cadastre uma segunda chave nos lugares importantes: a de outro computador, ou uma chave em hardware guardada numa gaveta. Se o notebook for roubado, você remove só a chave dele de todo lugar e entra com a outra.

Para servidores, garanta um segundo caminho: o console web do provedor, por exemplo. Ficar trancado do lado de fora é o risco real de quem tem uma chave só.

Disco criptografado

A passphrase protege o arquivo da chave, mas o disco tem mais coisa: o known_hosts (onde você conecta), o config, o histórico do shell. Com o disco criptografado, um notebook perdido desligado não entrega nada disso. Linux: LUKS, oferecido pelo instalador da maioria das distribuições. macOS: FileVault. Windows: BitLocker, ou a "Criptografia do dispositivo" nas edições Home.

Linux

Permissões

bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config ~/.ssh/authorized_keys
chmod 644 ~/.ssh/id_ed25519.pub
ls -la ~/.ssh
output
total 20
drwx------ 2 user user 140 Oct 15 13:14 .
drwxrwxr-x 3 user user  60 Oct 15 13:14 ..
-rw------- 1 user user 101 Oct 15 13:14 authorized_keys
-rw------- 1 user user  28 Oct 15 13:14 config
-rw------- 1 user user 464 Oct 15 13:14 id_ed25519
-rw-r--r-- 1 user user 101 Oct 15 13:14 id_ed25519.pub
-rw-r--r-- 1 user user  37 Oct 15 13:14 known_hosts
ArquivoPermissãoQuem acessa
~/.ssh/drwx------ (700)Só você entra na pasta
id_ed25519, config, authorized_keys-rw------- (600)Só você lê e escreve
id_ed25519.pub, known_hosts-rw-r--r-- (644)Você escreve, todos leem

Os três grupos de letras são o que o dono, o grupo e os outros podem fazer: r ler, w escrever, x executar (numa pasta, entrar). O outro artigo explica como ler isso em detalhe, e mostra o aviso de "UNPROTECTED PRIVATE KEY FILE" que o cliente dá quando a chave privada está aberta demais.

O known_hosts não tem segredo, mas mostra onde você conecta. Se a máquina tem outros usuários, um chmod 600 nele também não faz mal. No servidor, o sshd recusa um authorized_keys, ou uma pasta ~/.ssh, em que outros usuários possam escrever.

Qual agente está rodando

Em quase todo desktop Linux já tem um agente. Descubra qual:

bash
echo $SSH_AUTH_SOCK
output
/run/user/1000/gcr/ssh

Esse é o do GNOME (gcr-ssh-agent), que usa o ssh-agent do OpenSSH por baixo e pede a passphrase numa janela. No KDE, costuma ser o ssh-agent com o ksshaskpass. Se a variável vier vazia, não tem agente, e você pode iniciar um na sessão do terminal com eval "$(ssh-agent -s)".

A passphrase no chaveiro do sistema

No macOS, o Keychain guarda a passphrase e devolve a chave ao agente sozinho depois do login. No Linux, dá para ter o mesmo de algumas formas, todas com a troca da seção do macOS: a passphrase deixa de ser digitada e passa a ser protegida pela senha do seu login, ou do seu cofre.

  • GNOME: quando o gcr-ssh-agent pede a passphrase, a janela tem a opção Automatically unlock this key whenever I'm logged in (o texto pode aparecer traduzido). Marcada, a passphrase vai para o chaveiro de login do GNOME Keyring, que é destravado junto com a sua sessão. Para ver ou apagar depois, use o app Senhas e chaves (Seahorse).
  • KDE: o ksshaskpass pode guardar a passphrase no KWallet, que também é destravado no login.
  • KeePassXC: a integração com o agente SSH adiciona a chave quando você destrava o banco e a remove quando você trava, com a opção de pedir confirmação a cada uso. Veja gerenciadores de senha como agente.
  • O programa keychain: apesar do nome, não guarda nada. É um script, disponível no pacote keychain da maioria das distribuições, que reaproveita um único ssh-agent entre os terminais, então a passphrase é pedida uma vez por boot. Serve para servidores e para o WSL, onde não tem um desktop com agente.

E se a chave não tem passphrase

Aí não tem o que guardar: o agente carrega uma chave sem passphrase sem perguntar nada, e o chaveiro nunca entra em jogo. É justamente o caso em que o arquivo sozinho já é a credencial. O caminho é pôr uma passphrase primeiro e deixar o chaveiro guardá-la:

  1. Confira se a chave tem passphrase, com o teste do outro artigo.
  2. Adicione uma. Numa chave sem passphrase, o ssh-keygen -p só pede a nova, duas vezes:
bash
ssh-keygen -p -f ~/.ssh/id_ed25519
output
Key has comment '[email protected]'
Enter new passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved with the new passphrase.

A chave continua a mesma, só passa a ficar criptografada no disco, então não precisa mudar nada no GitHub nem nos servidores.

  1. Tire do agente a versão sem passphrase, que continua carregada: ssh-add -D. O gcr-ssh-agent volta a oferecer as chaves de ~/.ssh sozinho, agora pedindo a passphrase.
  2. Use a chave normalmente, com um git push ou um ssh servidor. O GNOME mostra a janela pedindo a passphrase: digite e marque Automatically unlock this key whenever I'm logged in. Daí em diante, ela vem do chaveiro.

A janela só aparece quando é o próprio agente que precisa da passphrase. Se você rodar ssh-add no terminal, a passphrase é pedida ali mesmo, e a opção de guardar não aparece. Mantenha também o .pub ao lado da chave: é por ele que o gcr-ssh-agent encontra as chaves de ~/.ssh. No KDE e no KeePassXC, o passo 2 é o mesmo; depois, quem guarda a passphrase é o ksshaskpass ou o cofre.

Com a passphrase no chaveiro, o tempo de vida do agente perde força: a chave volta sozinha no próximo uso. O que continua protegendo durante o uso é a confirmação, abaixo.

Tempo de vida e confirmação

O AddKeysToAgent 8h do config de partida resolve o tempo de vida. Na mão, o equivalente é:

bash
ssh-add -t 8h ~/.ssh/id_ed25519
output
Identity added: /home/user/.ssh/id_ed25519 ([email protected])
Lifetime set to 08:00:00

Por que oito horas, e não uma: o tempo de vida protege o momento em que você esquece a chave carregada, como o computador que fica ligado à noite. Enquanto você está usando o computador, ele não protege nada, e uma hora só faz você digitar a passphrase várias vezes por dia. Um dia de trabalho é o meio-termo: você digita de manhã, e a chave não passa a noite no agente. Quem protege durante o uso é a confirmação, ou o toque numa chave em hardware.

Para pedir confirmação a cada uso:

bash
ssh-add -c -t 8h ~/.ssh/id_ed25519
output
Identity added: /home/user/.ssh/id_ed25519 ([email protected])
Lifetime set to 08:00:00
The user must confirm each use of the key

Cada assinatura abre uma janela perguntando se você permite. Negando, quem pediu recebe isto:

output
sign_and_send_pubkey: signing failed for ED25519 "[email protected]" from agent: agent refused operation
user@localhost: Permission denied (publickey).

A janela vem de um programa ssh-askpass: no GNOME, o próprio gcr-ssh-agent traz o dele; no KDE, é o ksshaskpass. Sem nenhum, a confirmação não tem como acontecer e todo uso é negado, então em servidores e no WSL sem interface gráfica fique só com o tempo de vida. No config, o equivalente é AddKeysToAgent confirm 8h.

Para tirar tudo do agente ao sair de perto do computador:

bash
ssh-add -D

Chaves em hardware (FIDO2)

Com uma chave FIDO2, como um YubiKey (série 5) ou um Nitrokey 3, a chave privada é gerada dentro do dispositivo e não sai de lá. O OpenSSH das distribuições principais já vem com suporte:

bash
ssh-keygen -t ed25519-sk -O verify-required -C "[email protected]"
  • O dispositivo pede um toque a cada uso, por padrão.
  • O -O verify-required pede também o PIN do dispositivo a cada uso, então um dispositivo roubado não serve sozinho.
  • O arquivo gerado em ~/.ssh/id_ed25519_sk não é a chave, só uma referência a ela: sem o dispositivo, ele não serve para nada.

Se você alterna entre computadores, o -O resident guarda essa referência no próprio dispositivo, e o ssh-keygen -K a recupera em qualquer máquina. O manual avisa da troca: com tudo no dispositivo, quem o rouba fica mais perto de usá-lo, e por isso chaves residentes normalmente exigem um PIN configurado.

Não tenho um dispositivo FIDO2 neste laboratório, então os outputs desta seção não foram capturados. Algumas chaves mais antigas não suportam Ed25519: nesse caso, use -t ecdsa-sk.

Se você também administra o servidor

Quando todas as suas chaves estiverem funcionando, desligue a entrada por senha no servidor. Crie um arquivo em /etc/ssh/sshd_config.d/:

texto
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no

O nome começa com 10- de propósito. No sshd_config também vale o primeiro valor encontrado, os arquivos dessa pasta são lidos em ordem alfabética, e imagens de nuvem costumam trazer um 50-cloud-init.conf com PasswordAuthentication yes. Confira o valor efetivo com sshd -T. Num laboratório com os dois arquivos:

bash
sudo sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin)'
output
permitrootlogin prohibit-password
passwordauthentication no
kbdinteractiveauthentication no

prohibit-password é o padrão: o root só entra com chave. Se você nunca entra como root, troque por PermitRootLogin no.

Valide a sintaxe com sudo sshd -t e reinicie o serviço (sudo systemctl restart ssh no Debian e no Ubuntu, sshd no Fedora e no Arch). Mantenha a sessão atual aberta e teste uma conexão nova em outro terminal antes de sair: se algo deu errado, é por essa sessão que você conserta.

macOS

A passphrase no Keychain

O macOS já traz o OpenSSH e um agente iniciado pelo sistema. A integração própria da Apple guarda a passphrase no Keychain:

bash
ssh-add --apple-use-keychain ~/.ssh/id_ed25519

E no ~/.ssh/config:

texto
Host *
  IgnoreUnknown UseKeychain
  UseKeychain yes
  AddKeysToAgent yes

Com isso, a passphrase fica no Keychain, que é destravado quando você faz login, e a chave volta ao agente sozinha depois de reiniciar. É cômodo, mas troca a passphrase pela senha da sua conta do macOS: quem estiver com a sua sessão aberta usa a chave. Bloqueie a tela ao sair de perto.

O IgnoreUnknown UseKeychain é para o caso de você usar também o OpenSSH do Homebrew: o UseKeychain só existe no OpenSSH da Apple, e o do Homebrew recusa o config inteiro com "Bad configuration option" sem essa linha.

Chaves no Secure Enclave

Macs com chip Apple ou com o chip T2 têm o Secure Enclave, um chip separado de onde chaves não podem ser extraídas. O Secretive é um app de código aberto que cria chaves SSH lá dentro e funciona como agente, com Touch ID por uso, se você quiser. É o equivalente a ter uma chave em hardware sem comprar um dispositivo, com uma diferença: a chave fica presa àquele Mac, então ela é a chave daquele dispositivo, e você precisa de outra para os demais.

O Secure Enclave só gera chaves ECDSA P-256, não Ed25519. Tudo bem: a fragilidade do ECDSA está no número aleatório de cada assinatura, e quem gera esse número é o próprio chip.

Chaves FIDO2 no macOS

O OpenSSH que vem com o macOS é compilado sem suporte a chaves FIDO, e o ssh-keygen -t ed25519-sk falha. O caminho mais comum é instalar o OpenSSH do Homebrew (brew install openssh), que inclui esse suporte, e conferir com which ssh e ssh -V qual dos dois está sendo usado. O Secretive costuma ser a opção mais simples no Mac.

FileVault

Em Ajustes do Sistema > Privacidade e Segurança > FileVault. O assistente de configuração do Mac oferece ligá-lo, mas vale conferir.

Windows

O OpenSSH já vem no sistema

O Windows 10 (desde a versão 1809) e o Windows 11 já trazem o cliente OpenSSH. No PowerShell:

powershell
ssh -V

As chaves ficam em C:\Users\VOCÊ\.ssh, ou $env:USERPROFILE\.ssh. O ssh-keygen é o mesmo, e o padrão também é Ed25519:

powershell
ssh-keygen -t ed25519 -C "[email protected]"

Permissões

No Windows, as permissões são ACLs, e o OpenSSH do Windows também recusa uma chave privada que outros usuários conseguem ler. Para deixar o arquivo acessível só para você:

powershell
icacls $env:USERPROFILE\.ssh\id_ed25519 /inheritance:r
icacls $env:USERPROFILE\.ssh\id_ed25519 /grant:r "$($env:USERNAME):F"

O primeiro comando remove as permissões herdadas da pasta; o segundo dá controle total só ao seu usuário.

O serviço ssh-agent

O agente do Windows é um serviço, que vem desativado. Num PowerShell como administrador:

powershell
Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent

E, como o seu usuário:

powershell
ssh-add $env:USERPROFILE\.ssh\id_ed25519

Aqui o Windows é bem diferente do Linux e do macOS: o agente guarda a chave de forma persistente, associada à sua conta do Windows, e ela continua lá depois de reiniciar. A documentação da Microsoft chega a sugerir apagar o arquivo da chave depois de adicioná-la. Na prática, você digita a passphrase uma vez, e daí em diante a proteção da chave é a proteção da sua conta do Windows. Então:

  • Proteja a conta: Windows Hello ou uma senha forte, e tela bloqueada ao sair de perto.
  • Use BitLocker.
  • Remova chaves que não usa mais com ssh-add -D, ou ssh-add -d com o arquivo.
  • Para uma chave mais sensível, prefira uma chave em hardware ou um gerenciador de senhas como agente (abaixo).

Git for Windows

O Git for Windows traz o próprio ssh.exe, que não conversa com o serviço ssh-agent do Windows. O sintoma é o Git pedir a passphrase toda vez, mesmo com a chave no agente. Aponte o Git para o OpenSSH do sistema:

powershell
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"

PuTTY

Se você usa o PuTTY, ou programas que o embutem (FileZilla, WinSCP, TortoiseGit), atualize: as versões 0.68 a 0.80 vazavam chaves ECDSA P-521 (CVE-2024-31497). Se você usou uma chave P-521 nessas versões, troque a chave, porque atualizar não desfaz as assinaturas já feitas. O PuTTY usa o formato .ppk e o próprio agente, o Pageant. O PuTTYgen converte chaves entre os formatos do PuTTY e do OpenSSH.

Chaves FIDO2 no Windows

O OpenSSH do Windows usa a API WebAuthn do próprio sistema, então chaves físicas FIDO2 funcionam sem instalar nada:

powershell
ssh-keygen -t ed25519-sk -O verify-required -C "[email protected]"

O Windows mostra a própria janela pedindo o PIN e o toque. Usar o Windows Hello do computador como autenticador, em vez de um dispositivo físico, é instável: há relatos de a janela trocar o Windows Hello por "celular ou chave USB" depois de atualizações.

WSL

O WSL é um Linux à parte, com o próprio ~/.ssh: siga a seção de Linux lá dentro. Não guarde as chaves em /mnt/c/...: por padrão, os arquivos do disco do Windows aparecem no WSL com permissões abertas, e o ssh se recusa a usá-los.

Agentes de IA e as suas chaves

Agentes de IA que trabalham no seu terminal, como o Claude Code, o Codex CLI ou o modo agente dos editores, rodam comandos com o seu usuário e herdam as suas variáveis de ambiente. Isso inclui o SSH_AUTH_SOCK: se a sua chave está no agente, um git push, um ssh servidor ou um commit assinado que eles rodem funciona sem pedir nada. É o mesmo risco do agent forwarding: ninguém copia a chave, mas ela é usada sem você ver. E um agente de IA pode ser levado a isso por um texto que ele leu, como um README, uma issue ou uma página com instruções escondidas (prompt injection).

Não é motivo para tirar as chaves do agente, e sim para decidir o que um comando que você não digitou pode fazer:

  1. Confirmação a cada uso, com AddKeysToAgent confirm 8h ou a aprovação do 1Password, do Bitwarden ou do KeePassXC. Cada git push do agente de IA abre uma janela na sua tela, e você vê o que ia acontecer. Negado, ele recebe o agent refused operation mostrado acima.
  2. Toque numa chave em hardware. Um processo não aperta um botão físico.
  3. Chaves separadas e limitadas para o que o agente de IA faz sozinho. No GitHub, uma deploy key dá acesso a um repositório só, e um token fine-grained, usado via HTTPS, pode ser limitado a alguns repositórios e permissões. Se for mal usada, o estrago fica ali.
  4. A chave de produção fora do alcance, ou com confirmação, enquanto um agente de IA trabalha na mesma sessão.
  5. Isolamento. Num container ou numa VM, o agente de IA só vê o que você monta lá dentro: não monte o socket do seu agente nem a pasta ~/.ssh.

Uma combinação que funciona bem: a chave pessoal com confirm 8h e, se o agente de IA precisa fazer push sozinho, uma deploy key restrita àquele repositório.

Gerenciadores de senha como agente

O 1Password, o Bitwarden e o KeePassXC podem funcionar como agente SSH: a chave fica criptografada no cofre, e só fica disponível enquanto o cofre está destravado. O 1Password e o Bitwarden ainda pedem aprovação a cada uso, com Touch ID, Windows Hello ou a senha mestra. Funcionam nos três sistemas.

A troca: o cofre sincroniza entre os seus dispositivos, então a mesma chave passa a existir em todos eles, ao contrário do "uma chave por dispositivo". Para a maioria das pessoas, ter a chave protegida pelo cofre e com aprovação por uso compensa. Se você prefere manter uma chave por dispositivo, crie uma chave para cada um dentro do cofre.

Se uma chave vazar

  1. Remova a chave pública de todo lugar em que ela entra: GitHub, GitLab e o authorized_keys de cada servidor. Para achar a linha, liste os fingerprints do arquivo e compare com o da chave vazada:
bash
ssh-keygen -lf ~/.ssh/authorized_keys
output
256 SHA256:4+ja/K0i3L7pStIZYZVOnEvEd0YEFPjedY/2GlUWgFA [email protected] (ED25519)
256 SHA256:cycriPjeX1MyKaDUPvNH4ySmfg8RVZe+EGnV/EImFnQ [email protected] (ED25519)
3072 SHA256:ypI93OhP6BnAPzWA7Ut6fzAxERVrO8R1Ot19RxPG204 [email protected] (RSA)
256 SHA256:uhpYBP/1S7pAB7Agqm4u9GbKMzEld7i7qIWH5Su8PH0 [email protected] (ECDSA)
  1. Gere uma chave nova e cadastre no lugar da antiga.
  2. Procure se ela foi usada. Nos servidores, cada entrada fica no log com o fingerprint da chave (journalctl -u ssh | grep 'Accepted publickey', ou -u sshd fora do Debian e do Ubuntu). No GitHub, o log de segurança da conta mostra as ações recentes.
  3. Revise o authorized_keys dos servidores em que ela entrava. Quem entrou com uma chave roubada pode ter deixado outra chave lá, para voltar depois.

Checklist

  • Chaves Ed25519 (ou -sk em hardware); nenhuma DSA, nenhuma RSA abaixo de 3072 bits
  • Passphrase em toda chave de uso pessoal (confira com ssh-keygen -y -P '')
  • Uma chave por dispositivo, e uma segunda chave cadastrada nos lugares importantes
  • Agente com tempo de vida (AddKeysToAgent 8h ou ssh-add -t 8h)
  • Confirmação a cada uso, ou chave em hardware, se agentes de IA rodam comandos no seu terminal
  • IdentitiesOnly yes e uma IdentityFile por servidor
  • Sem agent forwarding; ProxyJump para passar por um bastion
  • Host keys conferidas na primeira conexão e quando mudam
  • Permissões corretas (chmod 600, ou icacls no Windows)
  • Disco criptografado (LUKS, FileVault ou BitLocker)
  • Chaves revisadas no GitHub, no GitLab e no authorized_keys dos servidores
  • Entrada por senha desligada nos servidores que você administra
  • PuTTY e derivados na versão 0.81 ou mais nova
  • Um plano para chave vazada: remover, trocar, procurar nos logs

Fontes