SSH sem porta 22 aberta: Cloudflare Tunnel e ProxyJump

O fail2ban não deu conta dos bots na porta 22. O que resolveu foi tirar o SSH da internet: entrada pelo Cloudflare Tunnel e ProxyJump para o resto.

Publicado
Leitura
5 min
Rack de 19 polegadas com switches Ethernet e patch panels cheios de cabos
Switches e patch panels num rack. Foto: Dsimic, CC BY-SA 4.0 (original).

Quem deixa a porta 22 aberta para a internet conhece a cena: o log de autenticação enche de tentativas de login vindas do mundo inteiro, o tempo todo. Eu sofri bastante com isso. Instalei o fail2ban, coloquei uma aplicação para monitorar os acessos e gerar relatórios dos bloqueios, e mesmo assim os bots continuavam batendo.

O que resolveu de verdade não foi bloquear melhor, foi parar de expor a porta. Hoje o meu SSH entra só pelo Cloudflare Tunnel e, de lá, eu pulo para as outras máquinas com ProxyJump. Neste post mostro como montar esse esquema.

Por que o fail2ban não basta #

O fail2ban lê os logs e bane, no firewall, os IPs que erram a senha várias vezes. Ele funciona e ajuda, mas tem um limite: ele reage a ataques que já estão chegando. Com a porta aberta, cada IP novo tem direito a algumas tentativas antes do banimento, e existem muitos IPs novos.

A solução mais limpa é não ter porta nenhuma para o bot encontrar.

Como fica o desenho #

O caminho de um acesso meu, de fora de casa, fica assim:

  1. O meu notebook chama ssh normalmente.
  2. Um ProxyCommand no ~/.ssh/config faz o cloudflared levar a conexão até a Cloudflare.
  3. A Cloudflare entrega a conexão pelo tunnel ao cloudflared que roda num container LXC no meu Proxmox.
  4. O cloudflared repassa para a porta 22 do jump server, a única máquina que recebe SSH de fora.
  5. Do jump server, o ProxyJump me leva a qualquer outro container da rede.

Como o tunnel é uma conexão de saída do meu homelab para a Cloudflare, não preciso de nenhum redirecionamento de porta no roteador. Para quem varre a internet atrás da porta 22, o meu IP simplesmente não tem SSH.

Diagrama da Cloudflare: o navegador chega à rede da Cloudflare, que entrega a requisição pelo tunnel ao cloudflared rodando no servidor, que repassa ao serviço local
Como funciona o Cloudflare Tunnel. Diagrama: Cloudflare (CC BY 4.0).

Lado do servidor: a rota no tunnel #

Considerando que você já tem um Cloudflare Tunnel funcionando, o passo é publicar o SSH como uma rota. Segundo a documentação da Cloudflare, no painel:

  1. Abra o tunnel em Networking > Tunnels e vá na aba Routes.
  2. Clique em Add route e escolha Published application.
  3. Defina um subdomínio, por exemplo ssh.seudominio.com.
  4. Em Service, escolha SSH e informe o destino. Use localhost:22 se o cloudflared roda na mesma máquina do SSH, ou IP-do-servidor:22 se roda em outra, como é o meu caso com o cloudflared num container separado.

Lado do cliente: o ProxyCommand #

No computador de onde você acessa, instale o cloudflared e adicione ao ~/.ssh/config:

Host ssh.seudominio.com
  ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h

O caminho do cloudflared muda conforme o sistema e o gerenciador de pacotes. Confira com which cloudflared.

A partir daí, ssh [email protected] funciona como qualquer SSH, só que passando pelo tunnel. Se a rota estiver protegida pelo Access, o cloudflared abre o navegador para você fazer login antes da sessão começar.

ProxyJump: um só ponto de entrada #

Com o jump server acessível pelo tunnel, não faz sentido publicar cada container. O OpenSSH resolve isso com ProxyJump: você pede para entrar numa máquina interna e ele passa primeiro pelo jump server, de forma transparente.

Um ~/.ssh/config nessa lógica fica assim (nomes e IPs são exemplos):

Host jump
  HostName ssh.seudominio.com
  User bruno
  ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h

Host app-*
  User root
  ProxyJump jump

Host app-blog
  HostName 192.168.1.50

Com isso, ssh app-blog sai do meu notebook, atravessa o tunnel até o jump server e de lá chega ao container do blog, tudo num comando só. Para uma conexão pontual, o equivalente na linha de comando é ssh -J jump [email protected].

Esse desenho tem um bônus: toda conexão externa passa pelo mesmo lugar. É no jump server que fica a monitoria de acessos, então tenho um único ponto para olhar.

Endurecendo o jump server #

Mesmo escondido, o ponto de entrada merece cuidado. No sshd_config do jump server, o básico é:

PasswordAuthentication no
PermitRootLogin no

Com chave SSH obrigatória e root bloqueado, mesmo alguém que chegue até o servidor precisa da sua chave privada. Depois de editar, valide a configuração antes de reiniciar, para não se trancar para fora:

sshd -t && systemctl reload ssh

Resumindo #

  • O fail2ban reage; esconder a porta evita. Os dois podem conviver, mas a porta fechada resolve a raiz do problema.
  • O Cloudflare Tunnel publica o SSH sem redirecionamento de porta no roteador.
  • Proteja a rota com Cloudflare Access para exigir login antes do SSH.
  • ProxyJump dá um único ponto de entrada para todas as máquinas internas.
  • Chave obrigatória e root bloqueado no jump server.

Desde que o SSH passou a entrar só pelo tunnel, os bots sumiram do meu log. Não porque eu bloqueio melhor, mas porque não há mais nada para eles encontrarem.

Fontes e documentação