Corrigindo desconexões SSH: Keepalive e Timeouts

Corrigindo desconexões SSH: Keepalive e Timeouts

Por que o SSH desconecta sozinho?

Conclusão: Na maioria das vezes, um NAT/firewall no caminho ou o sshd silenciosamente mata uma conexão ociosa (sem tráfego). Se morre com Broken pipe após uma breve pausa, o keepalive -- pequenos pacotes periódicos -- não está ativo. Configurar ServerAliveInterval no cliente ou ClientAliveInterval no servidor resolve a maioria dos casos.

Se o SSH "funciona enquanto estou digitando mas morre quando me afasto", timeout ocioso é quase sempre o culpado. Uma conexão TCP sobrevive ao silêncio por um tempo, mas dispositivos NAT, firewalls stateful e balanceadores de carga no caminho removem sessões que ficam quietas por muito tempo. Após a remoção, sua próxima tecla não tem para onde ir, e a sessão morre com mensagens como estas:

client_loop: send disconnect: Broken pipe
Write failed: Broken pipe
packet_write_wait: Connection to 192.0.2.10 port 22: Broken pipe
Timeout, server not responding.

Por outro lado, se cai enquanto você está trabalhando ativamente, ou no mesmo tempo decorrido exato a cada sessão, suspeite de um limite de sessão explícito no servidor ou de um link instável (coberto abaixo). O primeiro passo mais rápido é determinar se morre ocioso ou morre em uso.

Pré-requisitos

  • Cliente e servidor ambos Ubuntu / Linux típico (OpenSSH)
  • Edite ~/.ssh/config no cliente; /etc/ssh/sshd_config no servidor
  • Aplicar mudanças no servidor precisa de sudo e um reload do sshd

Queda ociosa ou queda em uso -- como distinguir?

Conclusão: Se morre enquanto fica ocioso, é um timeout ocioso (NAT / firewall / ClientAliveInterval). Se morre mesmo enquanto você mantém a tela atualizando (ex.: executando top), suspeite de qualidade do link ou um limite de sessão explícito. O primeiro é corrigido com keepalive; o segundo precisa de uma abordagem diferente.

Para dividir a causa em duas, primeiro teste se tráfego constante mantém a conexão viva.

# Mantem a conexao ocupada com saida periodica leve
$ ssh user@server 'while true; do date; sleep 30; done'
  • Para de cair -> o silêncio era a causa: um timeout ocioso. Keepalive resolve (próxima seção).
  • Continua caindo -> suspeite de link instável (Wi-Fi / movel) ou timeout fixo no servidor (outras configurações do sshd_config, PAM, ou limite rigido de balanceador de carga).

Manter a conexão com ssh -v e ler o log no momento da queda torna isso conclusivo.

$ ssh -v user@server
debug1: client_loop: send disconnect: Broken pipe

Se o tráfego de keepalive parou logo antes do corte, ou server not responding apareceu, indica qual lado desistiu primeiro.

Se o tempo-até-cair é quase constante toda vez, um timeout artificial é provável (conntrack do NAT, ou ClientAliveInterval x ClientAliveCountMax do servidor). Se varia, incline-se para um problema de qualidade do link.

Lado do cliente: como configurar ServerAlive?

Conclusão: Coloque ServerAliveInterval 60 no ~/.ssh/config do cliente primeiro. Mesmo sem tráfego, o ssh envia uma requisição pelo canal criptografado a cada 60 segundos, mantendo a sessão NAT viva e detectando um servidor morto. Funciona apenas do cliente, mesmo quando você não pode mexer no servidor (hosts compartilhados, etc.).

ServerAliveInterval é o número de segundos de silêncio do servidor após o qual o ssh envia uma requisição pelo canal criptografado. O padrão é 0 (desabilitado). ServerAliveCountMax (padrão 3) é quantos probes podem ficar sem resposta antes do ssh desconectar.

# ~/.ssh/config
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

Com isso, um pequeno pacote flui a cada 60 segundos para que a sessão NAT/firewall permaneça viva, prevenindo quedas ociosas. Ao mesmo tempo, se o servidor realmente morrer, o ssh desiste após 60s x 3 = ~180s em vez de deixar uma sessão zumbi.

Para testar ad hoc, use -o.

$ ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@server

Host * aplica a todas as conexões; limite a um bloco Host myserver para restringir. Mantenha ~/.ssh/config no modo 600 -- permissões muito frouxas podem fazer com que seja ignorado.

Definir ServerAliveInterval muito baixo significa que uma breve instabilidade esgota o ServerAliveCountMax e torna quedas mais prováveis. Em links instáveis, não reduza o intervalo agressivamente; aumente o count (ex.: Interval 30 / CountMax 6 dá ~180s de margem).

Lado do servidor: como configurar ClientAlive?

Conclusão: Se você gerencia o servidor, coloque ClientAliveInterval 60 em /etc/ssh/sshd_config. O sshd envia uma requisição pelo canal criptografado para cada cliente, então você previne quedas ociosas sem configurar cada cliente. Os mesmos parâmetros também permitem desconectar deliberadamente clientes ociosos -- a direção depende do CountMax que você combinar.

ClientAliveInterval é o espelho do ServerAliveInterval do cliente: o sshd envia um probe após N segundos de silêncio do cliente. Padrão 0 (desabilitado). ClientAliveCountMax tem padrão 3.

# /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3

Valide a sintaxe antes de aplicar. Quebrar a configuração do sshd aqui pode te trancar para fora, então sempre faça isso com uma segunda sessão aberta.

# Verificacao de sintaxe (captura erros antes de entrarem em vigor)
$ sudo sshd -t

# Recarregar a configuracao (sessoes existentes sao preservadas)
$ sudo systemctl reload ssh
# sshd -t nao imprime nada quando a configuracao e valida

Note que o significado muda com a intenção:

  • Manter conexões vivas: ClientAliveInterval 60 sozinho mantém o keepalive fluindo e supera a remoção do NAT. Um CountMax maior é aceitável.
  • Desconectar clientes ociosos (ex.: requisito de segurança): ClientAliveInterval 300 / ClientAliveCountMax 1 derruba um cliente silencioso após cerca de 5 minutos (note que ClientAliveCountMax 0 em vez disso desabilita a terminação).

Um restart pode derrubar conexões existentes; prefira reload para aplicar mudanças. Execute sshd -t -> reload com uma segunda sessão SSH ainda aberta para se proteger contra bloqueio. Se o ufw bloquear até o SSH, veja Troubleshooting ufw e SSH.

Como ServerAlive difere do TCPKeepAlive?

Conclusão: ServerAliveInterval / ClientAliveInterval operam sobre o canal criptografado SSH -- não podem ser falsificados e permitem ajustar o intervalo em segundos. TCPKeepAlive é keepalive na camada TCP, vinculado ao padrão do SO (tipicamente 2 horas de início ocioso) e falsificável. Para prevenção de queda ociosa, use a família ServerAlive da camada SSH.

As três configurações comumente confundidas, por camada:

Configuração Camada Direção Notas
ServerAliveInterval SSH (criptografado) cliente->servidor Definido no cliente. Não falsificável
ClientAliveInterval SSH (criptografado) servidor->cliente Definido no servidor. Não falsificável
TCPKeepAlive TCP ambas direções Padrão yes. Intervalo depende do SO

Mesmo com TCPKeepAlive yes (o padrão do OpenSSH), a vivacidade é verificada, mas o tempo ocioso antes do keepalive TCP iniciar segue a configuração do SO (no Linux, net.ipv4.tcp_keepalive_time, padrão 7200 segundos = 2 horas). Isso é lento demais para um NAT que corta em minutos. Para superar um timeout ocioso curto, o ServerAliveInterval com granularidade de segundos é a ferramenta certa.

# Para referencia: o inicio ocioso do keepalive TCP (segundos)
$ sysctl net.ipv4.tcp_keepalive_time
net.ipv4.tcp_keepalive_time = 7200

Os manuais oficiais (man ssh_config / man sshd_config) afirmam que mensagens ServerAlive são "enviadas pelo canal criptografado e portanto não falsificáveis", enquanto TCPKeepAlive é falsificável. O keepalive da camada SSH vence tanto em segurança quanto em controle.

E se ainda cair após configurar o keepalive?

Conclusão: Se o keepalive está configurado e ainda cai, é um destes: (1) um timeout ocioso de NAT / LB menor que seu intervalo de keepalive, (2) o link em si oscilando, ou (3) outro timeout do servidor (PAM, limite rigido de balanceador de carga). A correção é tornar o intervalo de keepalive confiavelmente menor que esse valor de ociosidade.

Percorra os suspeitos habituais quando o keepalive não é suficiente.

# Procurar pistas de keepalive / disconnect / timeout no log detalhado
$ ssh -v user@server 2>&1 | grep -iE 'alive|disconnect|timeout'
  • NAT / firewall ocioso é curto: roteadores domésticos e LBs de nuvem podem expirar em aproximadamente 60-350 segundos. Defina ServerAliveInterval claramente abaixo disso (se o LB é 60s, use 30).
  • Link oscilando: links Wi-Fi / moveis caem fisicamente. Keepalive não salva esses -- use tmux / mosh (abaixo) para tolerar desconexões.
  • Outro limite do servidor: um limite de sessão de balanceador de carga ou bastion, ou uma regra PAM, pode cortar em um tempo fixo. Verifique o motivo no servidor com journalctl -u ssh.
# Verificar o motivo da desconexao no lado do servidor
$ sudo journalctl -u ssh -n 100 --no-pager

Atrás de um balanceador de carga na nuvem (AWS NLB / GCP, etc.), o timeout ocioso do próprio LB domina (AWS NLB tem padrão de 350 segundos). Sempre defina o intervalo de keepalive abaixo desse valor.

Um intervalo de keepalive igual ou maior que o timeout ocioso é inútil. "LB é 60s, então ServerAliveInterval 60" ainda escorrega no limite. Mire em metade ou menos (30 ou abaixo neste exemplo).

O que usar para sobreviver a desconexões?

Conclusão: A correção mais profunda é tornar uma queda inofensiva. Execute tmux / screen no servidor e sua sessão persiste mesmo quando o SSH morre -- reconecte e attach para voltar. Em links instáveis, mosh reconecta automaticamente.

Keepalive mantém você conectado; tmux / mosh evitam que uma desconexão cause dano. Combine ambos para robustez.

# Iniciar uma sessao tmux no servidor
$ tmux new -s work

# Apos o SSH cair, reconecte e volte para a sessao
$ ssh user@server
$ tmux attach -t work

Execute processos longos e comandos arriscados dentro de tmux / screen para que uma queda de link não derrube o processo junto. Se transferências grandes com scp / rsync continuam falhando no meio, use rsync retomável.

mosh (mobile shell) é baseado em UDP e recupera automaticamente entre mudanças de IP e breves interrupções -- ideal para links moveis ou instáveis. Requer a instalação do mosh no servidor e a abertura de suas portas UDP.

Resumo e checklist

Conclusão: Se morre ocioso, o primeiro passo é keepalive -- ServerAliveInterval no cliente ou ClientAliveInterval no servidor. Mantenha o intervalo abaixo de qualquer timeout ocioso de NAT / LB, e combine com tmux / mosh para que uma queda seja inofensiva. Isso resolve quase todas as desconexões SSH inesperadas.

  • [ ] Separar queda-ociosa vs queda-em-uso (silêncio vs qualidade do link)
  • [ ] Definir ServerAliveInterval / ServerAliveCountMax no ~/.ssh/config do cliente
  • [ ] Se você gerencia o servidor, definir ClientAliveInterval em /etc/ssh/sshd_config, depois sshd -t -> reload
  • [ ] Tornar o intervalo de keepalive confiavelmente menor que o timeout ocioso do NAT / LB
  • [ ] Manter uma segunda sessão SSH aberta ao alterar configuração para se proteger contra bloqueio
  • [ ] Em links instáveis, usar tmux / screen / mosh para tolerar desconexões
  • [ ] Se ainda cair, verificar o motivo no servidor com journalctl -u ssh

Próximas leituras