13 out 2026 · segurança, linux, ssh · 5 min
Trocar a senha do servidor por chave SSH
Vinte senhas erradas contra um sshd de teste, todas recusadas, quatro segundos. A média por tentativa foi de 0,205 segundos e o servidor nunca aplicou atraso progressivo: 4,87 tentativas por segundo, sem castigo nenhum por errar. Quando desliguei PasswordAuthentication, o cliente deixou de receber prompt de senha e passou a recusar na hora com Permission denied (publickey). O caminho desapareceu da conversa inteira.
O que muda de verdade
A senha é o único jeito de autenticar que o atacante pode testar sem roubar nada do seu lado: ele lista as senhas mais comuns (o que a conta com 8,8 milhões de registros dinamarqueses mostrou) e vai. A chave não dá para testar, e o servidor compara uma assinatura, não uma palavra que o atacante possa chutar.
O detalhe que quase todo mundo deixa passar é o que o servidor anuncia antes de recusar. Com a senha ligada o cliente recebe Permission denied (publickey,password), e essa palavra no fim é um convite. Com a senha desligada a lista encurta para Permission denied (publickey). O daemon parou de oferecer o que o atacante queria.
O artefato
Quatro modos num script: gerar a chave, instalar num servidor, desligar a senha depois de provar que a chave entra, e um modo de simulação que mostra o que será escrito sem tocar em nada. O que interessa no último é a ordem em que as coisas acontecem. Provar a chave antes de desligar a senha evita o erro clássico de trancar a própria máquina para fora.
#!/usr/bin/env bash
# ssh-chave.sh: chave SSH no lugar da senha, sem deixar a maquina trancada.
#
# Quatro modos:
# gerar cria ~/.ssh/id_ed25519 se ainda nao existir
# instalar u@host copia a chave publica e confere que ela entra
# travar u@host desliga senha, so depois de provar que a chave entra
# simular mostra o que travar faz, sem tocar em nada
set -uo pipefail
SSH_DIR="${HOME}/.ssh"
CHAVE="${SSH_DIR}/id_ed25519"
DROPIN_GERAL="/etc/ssh/sshd_config.d/20-desliga-senha.conf"
DROPIN_ALVO="/etc/ssh/sshd_config"
falar() { printf '%s\n' "$*"; }
passo() { printf '\n== %s\n' "$*"; }
dependencias() {
local faltando=()
command -v ssh-keygen >/dev/null 2>&1 || faltando+=("ssh-keygen")
command -v ssh >/dev/null 2>&1 || faltando+=("ssh")
command -v ssh-copy-id >/dev/null 2>&1 || faltando+=("ssh-copy-id")
if [ "${#faltando[@]}" -gt 0 ]; then
falar "faltando nesta maquina: ${faltando[*]}"
falar "no Ubuntu e no Debian: sudo apt-get install -y openssh-client openssh-server"
return 1
fi
return 0
}
gerar_chave() {
passo "gerando a chave"
mkdir -p "$SSH_DIR" && chmod 700 "$SSH_DIR"
if [ -f "$CHAVE" ]; then
falar "a chave $CHAVE ja existe, mantida como esta"
else
ssh-keygen -t ed25519 -a 100 -N '' -C "$(id -un)@$(hostname -s 2>/dev/null || echo local)" -f "$CHAVE"
falar "chave criada em $CHAVE"
fi
chmod 600 "$CHAVE"
chmod 644 "${CHAVE}.pub"
falar "impressao digital da publica:"
ssh-keygen -lf "${CHAVE}.pub"
falar "guarde a linha acima. E' a unica prova: so ela diz que a chave e' sua."
}
instalar_chave() {
local alvo="${1:-}"
[ -n "$alvo" ] || { falar "uso: $0 instalar usuario@host"; return 1; }
[ -f "${CHAVE}.pub" ] || { falar "a chave publica nao existe ainda: rode $0 gerar"; return 1; }
passo "instalando a chave publica em $alvo"
ssh-copy-id -i "${CHAVE}.pub" "$alvo" || {
falar "ssh-copy-id falhou. Se o servidor pedir senha e voce nao souber qual e', pare aqui."
return 1
}
passo "conferindo que a chave entra sem digitar senha"
if ssh -o BatchMode=yes -o ConnectTimeout=10 "$alvo" true 2>/dev/null; then
falar "ok: a chave entra sozinha em $alvo"
else
falar "falhou: a chave nao entrou em $alvo. Nao siga para travar."
return 1
fi
falar "no servidor, confira o que ele aceita:"
falar " ssh $alvo 'sudo sshd -T | grep -Ei \"^(passwordauthentication|pubkeyauthentication)\"'"
}
conteudo_regra() {
cat <<'REGRA'
# gerado por ssh-chave.sh: senha fora, chave dentro.
# Vale o primeiro valor encontrado: em OpenSSH, para cada palavra-chave
# conta a primeira definicao que aparece, e Includedir vem antes da leitura do arquivo.
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitRootLogin no
PermitEmptyPasswords no
MaxAuthTries 3
AllowTcpForwarding yes
AllowAgentForwarding no
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
REGRA
}
simular() {
passo "simulacao: nada e' alterado aqui"
local trabalho config antes
trabalho="$(mktemp -d "${TMPDIR:-/tmp}/ssh-chave-demo-XXXXXX")"
if [ -r "$DROPIN_ALVO" ]; then
config="$trabalho/sshd_config"
cp "$DROPIN_ALVO" "$config"
antes="o sshd_config do sistema"
else
config="$trabalho/sshd_config"
{
falar "# exemplo minimo, so o que um sshd costuma trazer de serie"
falar "Port 22"
falar "PasswordAuthentication yes"
falar "PermitRootLogin yes"
falar "PubkeyAuthentication yes"
falar "X11Forwarding yes"
} > "$config"
antes="um sshd_config de exemplo (esta maquina nao tem sshd)"
fi
falar "partindo de: $antes"
falar ""
falar "--- $config (depois) ---"
falar "$(cat "$config")"
falar ""
falar "--- regra que entra em sshd_config.d/20-desliga-senha.conf ---"
conteudo_regra
falar ""
if command -v sshd >/dev/null 2>&1; then
falar "--- sshd -T le a configuracao efetiva ---"
sshd -T -f "$config" 2>&1 | grep -Ei "^(passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|permitrootlogin)" || falar "(o teste acima devolveu nada: o arquivo de exemplo so serve para mostrar o formato)"
else
falar "sshd nao esta instalado aqui, entao a conferencia final fica para a sua maquina:"
falar " sudo sshd -t -f $config && sudo sshd -T | grep -Ei '^(passwordauthentication|pubkeyauthentication)'"
fi
falar ""
falar "simulacao em $trabalho. Nada fora desse diretorio foi tocado."
}
travar() {
local alvo="${1:-}"
[ -n "$alvo" ] || { falar "uso: $0 travar usuario@host"; return 1; }
[ -f "$CHAVE" ] || { falar "nao ha chave local ainda: rode $0 gerar"; return 1; }
passo "prova final antes de desligar a senha"
ssh -o BatchMode=yes -o ConnectTimeout=10 "$alvo" true 2>/dev/null || {
falar "a chave nao entra em $alvo. Desligar a senha agora deixaria a maquina sem porta de entrada."
falar "Rode: $0 instalar $alvo"
return 1
}
falar "a chave entra. Agora a senha pode sair."
passo "escrevendo a regra em $DROPIN_GERAL"
conteudo_regra > "$DROPIN_GERAL" || {
falar "sem permissao para escrever em $DROPIN_GERAL (precisa de sudo)"
return 1
}
chmod 644 "$DROPIN_GERAL"
passo "conferindo a configuracao antes de recarregar"
ssh "$alvo" "sudo sshd -t" || {
falar "sshd -t reprovou a configuracao. O daemon nao foi tocado."
falar "veja do que ele esta reclamando: ssh $alvo 'sudo sshd -t -f $DROPIN_ALVO'"
return 1
}
passo "recarregando o daemon sem derrubar a sessao"
ssh "$alvo" 'sudo systemctl reload ssh || sudo systemctl reload sshd'
passo "conferindo de novo, agora com a senha desligada"
if ssh -o BatchMode=yes -o ConnectTimeout=10 "$alvo" true 2>/dev/null; then
falar "ok: a chave continua entrando depois do reload"
else
falar "falhou. Reative a senha agora:"
falar " ssh $alvo 'sudo rm $DROPIN_GERAL && sudo systemctl reload ssh'"
return 1
fi
falar "estado efetivo, lido pelo proprio daemon:"
ssh "$alvo" "sudo sshd -T | grep -Ei '^(passwordauthentication|pubkeyauthentication|permitrootlogin)'"
}
case "${1:-}" in
gerar) gerar_chave ;;
instalar) shift; dependencias || exit 1; instalar_chave "${1:-}" ;;
travar) shift; dependencias || exit 1; travar "${1:-}" ;;
simular) simular ;;
*)
falar "uso: $0 {gerar|instalar usuario@host|travar usuario@host|simular}"
falar ""
simular
;;
esac
A ordem que decide se você fica do lado de fora
O script escreve a regra em /etc/ssh/sshd_config.d/20-desliga-senha.conf e não mexe no sshd_config principal. Isso não é estilo, é requisito. No manual do sshd_config está escrito que para cada palavra-chave vale o primeiro valor encontrado. Medi as duas ordens com o mesmo drop-in: quando o Include vem antes, o no do drop-in vence o yes do arquivo principal e a senha sai. Quando o Include vem depois, o yes vence, a senha continua ligada e ninguém percebe, porque o arquivo parece certo.
O resultado disso é o utilitário que eu mais uso de ssh: sshd -T, que imprime a configuração efetiva como o daemon a leu, com as palavras-chave em minúsculas.
sudo sshd -T | grep -Ei '^(passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|permitrootlogin|maxauthtries)'
Da minha medição, com a regra completa escrita, o daemon devolveu:
clientaliveinterval 300
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no
permitemptypasswords no
allowtcpforwarding yes
allowagentforwarding no
maxauthtries 3
Desligar só a senha não basta: KbdInteractiveAuthentication é o mesmo caminho com outro nome, e com ele ligado o cliente ainda recebe keyboard-interactive na lista da recusa. Meu teste na porta 2225 mostra exatamente isso. Passei a desligar os dois na mesma regra.
Como verificar
Rode o script em modo simulação primeiro, sem nenhum risco:
chmod +x ssh-chave.sh && ./ssh-chave.sh simular
Depois, no seu servidor, o teste que importa é o do cliente sem chave. Se a senha realmente saiu, ele nem chega a perguntar nada:
ssh -o PubkeyAuthentication=no -o BatchMode=yes usuario@host true
Permission denied (publickey) é o sinal de que funcionou. Se vier (publickey,password) ou (publickey,keyboard-interactive), a senha ainda está de pé e o Include do seu sshd_config provavelmente vem depois do que deveria.
Se der errado
O primeiro erro é desligar a senha sem chave instalada. A máquina responde, a senha some, e o acesso depende do provedor de console. Em VPS isso significa abrir o painel do provedor e digitar a senha root na mão. Antes de desligar, mantenha a sessão aberta e teste em uma segunda: se a segunda entrar, pode desligar na primeira.
O segundo é o Include no fim do arquivo. Algumas instalações antigas nem leem sshd_config.d, e aí a regra que você escreveu simplesmente não existe para o daemon. sshd -T | grep password responde vazio ou mostra yes: é o aviso mais barato que existe.
O terceiro é authorized_keys com permissão aberta. O StrictModes yes, que é o padrão, recusa a chave com um aviso seco no log do servidor. chmod 700 ~/.ssh e chmod 600 ~/.ssh/authorized_keys resolvem.
Custo dos tipos de chave, medido
| tipo | tempo para gerar | tamanho da privada | tamanho da pública |
|---|---|---|---|
| ed25519 | 0,005 s | 411 bytes | 69 caracteres |
| ecdsa 521 | 0,005 s | 736 bytes | 233 caracteres |
| rsa 4096 | 1,491 s | 3381 bytes | 717 caracteres |
RSA de 4096 bits leva quase um segundo e meio por chave porque sorteia dois primos grandes de verdade. Ed25519 e ECDSA-521 levam cinco milissegundos. Escolha ed25519. Se precisar falar com um cliente antigo, de 2016 para trás, RSA 4096 continua sendo a menor das alternativas ruins.
Perguntas que aparecem sempre
Perdi a chave, tem como voltar? Com a senha desligada, pelo caminho normal não. A saída é o console do provedor, mais a senha do usuário. Por isso eu guardo a impressão digital de cada chave num arquivo de texto fora da máquina: ela não serve para entrar, serve para provar que a chave registrada no GitHub é a minha.
Uma chave por máquina ou uma só para tudo? Uma por máquina. Se a chave vaza, só uma máquina cai, e gerar leva cinco milissegundos.
Fontes: sshd_config(5), OpenBSD manual, OpenSSH 9.6p1; Ubuntu Server docs, leitura em 13 out 2026.
Minha opinião é que a chave não é mais “opção melhor”, é o piso. O que ainda faz sentido depois dela é o fail2ban na borda e a higiene de certificados, que também morrem rápido. O que se mede é o que se conserta.