Configurar o fail2ban e bloquear ataques de força bruta automaticamente

Publicado a 16 min de leitura

Como configurar o fail2ban de forma limpa através de jail.local, pôr o jail sshd a funcionar nas quatro distribuições LTS atuais, verificar e voltar a levantar bloqueios, e que erros o travam pelo caminho.

Qualquer servidor com endereço IPv4 público recebe tentativas de início de sessão na porta 22 poucos minutos depois do primeiro aprovisionamento. Não se trata de um ataque dirigido, é apenas ruído de fundo. O fail2ban acompanha as linhas de log dessas tentativas falhadas e faz a firewall bloquear o IP de origem durante um período definido. A ferramenta vem empacotada em todas as distribuições aqui tratadas e fica pronta em poucos minutos. As armadilhas estão noutro sítio: no Debian 12, o fail2ban arranca muitas vezes com um erro de configuração; em alguns sistemas parece bloquear sem que um único pacote seja realmente descartado; e uma linha descuidada em jail.local bloqueia o seu próprio acesso.

Este artigo cobre exatamente esses pontos, para Debian 13, Debian 12, Ubuntu 24.04 LTS e Ubuntu 22.04 LTS.

Porquê jail.local e nunca jail.conf

O ficheiro /etc/fail2ban/jail.conf pertence ao pacote. Pode ser substituído em qualquer atualização e, na melhor das hipóteses, o dpkg pergunta primeiro; na pior, durante uma atualização não vigiada, nem sequer pergunta. As suas alterações vivem por isso em /etc/fail2ban/jail.local. Esse ficheiro não existe depois da instalação: tem de o criar, e deve conter apenas os valores que realmente altera.

Importa perceber a ordem pela qual o fail2ban lê os ficheiros. A página de manual jail.conf(5) indica-a de forma explícita:

  1. jail.conf
  2. jail.d/*.conf por ordem alfabética
  3. jail.local
  4. jail.d/*.local por ordem alfabética

Os ficheiros lidos mais tarde ganham. Em concreto: a sua jail.local também sobrepõe os valores de /etc/fail2ban/jail.d/defaults-debian.conf, o ficheiro que vem com o pacote da distribuição. Muitos guias afirmam o contrário e recomendam por isso um ficheiro próprio em jail.d/. Não é necessário. O diretório jail.d/ só faz sentido se quiser distribuir jails individualmente através de gestão de configuração.

O que a sua distribuição já define

A maior diferença entre os quatro sistemas não está no fail2ban em si, mas neste único ficheiro que o pacote coloca em /etc/fail2ban/jail.d/defaults-debian.conf. Comece por o consultar:

cat /etc/fail2ban/jail.d/defaults-debian.conf
Sistemafail2banConteúdo de defaults-debian.conf
Debian 131.1.0banaction = nftables, banaction_allports = nftables[type=allports], para [sshd] ainda backend = systemd, journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd e enabled = true
Debian 121.0.2apenas [sshd] e enabled = true
Ubuntu 24.041.0.2banaction = nftables, banaction_allports = nftables[type=allports], backend = systemd, além de [sshd] com enabled = true
Ubuntu 22.040.11.2apenas [sshd] e enabled = true

Daí decorre quase tudo o resto. O Debian 13 e o Ubuntu 24.04 leem de fábrica o journal do systemd e bloqueiam através de nftables. O Debian 12 e o Ubuntu 22.04 recorrem às predefinições de jail.conf, ou seja, banaction = iptables-multiport e backend = auto. E auto não significa, ao contrário do que o nome sugere, "escolhe o mecanismo certo". O comentário em jail.conf diz que auto experimenta, por esta ordem, pyinotify e polling. Ambos são métodos baseados em ficheiros. O valor auto nunca escolhe o journal. Sem o ficheiro /var/log/auth.log, o jail sshd fica ali sem nada para ler.

Instalação e primeira verificação

apt update
apt install -y fail2ban

No Debian 12 e no Ubuntu 22.04 acresce um pacote, caso queira analisar o journal em vez de um ficheiro de log:

apt install -y python3-systemd

No Debian 13, python3-systemd é uma dependência obrigatória do pacote e já está presente. No Debian 12 é apenas recomendado, o que constitui a origem de erro mais frequente neste sistema. Verifique a seguir a versão e o estado:

apt-cache policy fail2ban
fail2ban-client --version
systemctl status fail2ban --no-pager

Se aqui já aparecer active (running), metade do trabalho está feita. Se aparecer failed, salte diretamente para a secção sobre as mensagens de erro.

Construir a sua própria jail.local

Crie /etc/fail2ban/jail.local. Esta versão funciona nos quatro sistemas, desde que python3-systemd esteja instalado:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.7
bantime  = 1h
findtime = 10m
maxretry = 5

bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 7d

banaction = nftables
banaction_allports = nftables[type=allports]

[sshd]
enabled  = true
backend  = systemd
port     = ssh
maxretry = 4
bantime  = 2h

O significado dos valores em linguagem simples: findtime é a janela de tempo deslizante, maxretry o número de tentativas falhadas dentro dela e bantime a duração do bloqueio. Quatro tentativas falhadas em dez minutos resultam aqui em duas horas de bloqueio. As indicações de tempo aceitam sufixos como m, h, d e w; um número isolado significa segundos. Um bantime de -1 bloqueia de forma permanente.

A parte interessante é bantime.increment. Com ele, o fail2ban duplica o tempo de bloqueio a cada reincidência do mesmo IP, com o limite definido em bantime.maxtime. Um engano pontual de um utilizador real custa duas horas; um bot persistente chega a uma semana ao fim de poucas rondas. Esta função existe desde o fail2ban 0.11, pelo que também está disponível no Ubuntu 22.04. Para que continue a valer depois de reinícios, a base de dados tem de guardar o histórico, o que se controla através de dbpurgeage em /etc/fail2ban/fail2ban.conf, por predefinição um dia. Se definir bantime.maxtime = 7d, aumente também dbpurgeage num ficheiro fail2ban.local.

Duas armadilhas neste bloco. Primeira, port = ssh: isto resolve-se através de /etc/services para a porta 22. Se o seu serviço SSH correr na 2222, tem de constar ali port = 2222, caso contrário cria-se uma regra de firewall para uma porta onde ninguém bate. Segunda, ignoreip: indique ali o IP fixo do seu escritório, mas nunca uma rede inteira de um fornecedor. E não confie apenas nisso. O valor ignoreself está de qualquer forma a true por predefinição e protege os IPs do próprio servidor.

Diferenças consoante o sistema

No Debian 12 existem dois caminhos equivalentes. Ou fica por backend = systemd e instala python3-systemd, ou instala rsyslog, omite backend e trabalha com /var/log/auth.log. Além disso, no Debian 12 deve definir a banaction de forma consciente. O pacote recomenda nftables ou iptables, e o apt costuma instalar apenas nftables. A predefinição de jail.conf, porém, é iptables-multiport. Numa instalação enxuta, o fail2ban recorre então a um binário que nem sequer existe.

No Ubuntu 22.04, o rsyslog faz parte da instalação padrão de servidor, o ficheiro /var/log/auth.log existe e o jail funciona sem qualquer intervenção. Se não quiser mudar nada nesse sistema, deixe simplesmente backend e banaction de fora da sua jail.local. O fail2ban 0.11.2 conhece a ação nftables, mas o caminho através de iptables-multiport é o mais percorrido neste sistema.

No Debian 13 e no Ubuntu 24.04, a sua jail.local coincide com as predefinições do pacote. Isso não faz mal, apenas torna a configuração explícita e, com isso, legível.

Aplicar e verificar se realmente funciona

Ler a configuração sem perder os bloqueios existentes:

fail2ban-client reload

No Debian 12 e no Ubuntu 24.04, o fail2ban 1.0.2 emite no arranque e ao recarregar o aviso 'allowipv6' not defined in 'Definition'. É inofensivo, o serviço continua a trabalhar normalmente.

Um systemctl restart fail2ban raramente é necessário e apaga os bloqueios em curso. A seguir, as consultas de estado:

fail2ban-client ping
fail2ban-client status
fail2ban-client status sshd

A última saída é a que diz alguma coisa. Mostra Currently failed, Total failed, o número de bloqueios e a lista de endereços bloqueados. É exatamente aqui que a maioria dos guias termina e é exatamente aqui que o problema começa: um jail que arranca sem erros e indica "0 banned" pode estar completamente cego. Um jail sem ocorrências tem um aspeto idêntico ao de um jail que lê a fonte de log errada.

Três verificações tornam essa diferença visível. Primeira: o filtro chega sequer a ver as linhas?

fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf

Com um backend baseado em ficheiros, em alternativa:

fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

No fim aparece uma linha como Lines: 4211 lines, 0 ignored, 137 matched, 4074 missed. Se ali constar 0 matched apesar de o log conter tentativas falhadas bem visíveis, o filtro ou a seleção do journal não estão corretos.

Segunda: que filtro de journal usa o jail na realidade?

fail2ban-client get sshd journalmatch

A resposta varia consoante o sistema, e essa diferença é importante. O Debian 13 devolve _SYSTEMD_UNIT=ssh.service + _COMM=sshd, ao passo que o Ubuntu 24.04 devolve _SYSTEMD_UNIT=sshd.service + _COMM=sshd. A unit chama-se ali sshd.service e no Debian ssh.service. No Debian 12 e no Ubuntu 22.04, o estado de origem devolve No journal match filter set, porque nesses sistemas o backend systemd não vem predefinido e o jail lê um ficheiro de log. Não é um erro, é a confirmação de que não existe qualquer filtro de journal. Só com backend = systemd na sua jail.local é que passa a haver um filtro ativo nesses sistemas.

As condições individuais estão separadas por + e são combinadas com um Ou lógico. _SYSTEMD_UNIT=ssh.service + _COMM=sshd corresponde, portanto, a tudo o que vem da unit ssh.service ou a qualquer processo chamado sshd. Para a contraprova no journal, use exatamente o nome de unit que apareceu na saída anterior:

journalctl -u ssh --no-pager -n 50
journalctl _COMM=sshd --no-pager -n 50

No Ubuntu 24.04, a primeira linha passa a ser journalctl -u sshd --no-pager -n 50. Quem usar aqui o nome errado vê uma saída vazia e passa depois a procurar no sítio errado.

Se aqui aparecerem linhas com Failed password for invalid user mas o fail2ban não contar nada, a causa está na seleção do journal. Isso torna-se relevante se correr o OpenSSH a partir dos backports ou se mudar para ativação por socket. Desde o OpenSSH 9.8, o processo filho chama-se sshd-session e já não sshd, e com ativação por socket a unit deixa de se chamar ssh.service. Nesse caso, acrescente na sua jail.local (no Ubuntu 24.04 com _SYSTEMD_UNIT=sshd.service):

[sshd]
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session

Terceira, a prova dura. Bloqueie manualmente um endereço da rede reservada para documentação e verifique se ele chega à firewall:

fail2ban-client set sshd banip 203.0.113.10

O comando responde com 1, ou seja, um endereço bloqueado. A ferramenta com que comprova esse bloqueio depende da banaction em vigor, e essa vem predefinida de forma diferente consoante a distribuição:

Sistemabanaction de fábricaVerificação do bloqueio ativo
Debian 13nftablesnft list table inet f2b-table
Ubuntu 24.04nftablesnft list table inet f2b-table
Debian 12iptables-multiportiptables -n -L f2b-sshd
Ubuntu 22.04iptables-multiportiptables -n -L f2b-sshd

O Debian 11, mais antigo, comporta-se como o Debian 12. A origem da diferença vê-se em /etc/fail2ban/jail.d/defaults-debian.conf: só o Debian 13 e o Ubuntu 24.04 trazem ali um banaction = nftables, os restantes ficam pela predefinição iptables-multiport de jail.conf. Quem não quiser andar a distinguir isso servidor a servidor coloca banaction = nftables na própria jail.local, tal como no exemplo mais acima, e verifica depois em todo o lado com nft.

Com a ação nftables existe uma tabela f2b-table na família inet, dentro dela uma chain f2b-chain com prioridade filter - 1 e um set chamado addr-set-sshd. É ali que 203.0.113.10 tem de constar como elemento. Nestes sistemas não existe nenhuma cadeia f2b-sshd, e iptables -n -L f2b-sshd responde ali com No chain/target/match by that name. Ao contrário, nft list table inet f2b-table devolve no Debian 12 e no Ubuntu 22.04 Error: No such file or directory, enquanto ali estiver a trabalhar iptables-multiport. Nenhuma das duas mensagens prova que a configuração está estragada, provam apenas que o comando não corresponde à ação configurada.

Com a ação iptables, 203.0.113.10 tem de constar, do mesmo modo, na cadeia f2b-sshd. Só quando o IP aparece na firewall é que a corrente fica fechada, do log até ao pacote descartado. Não se esqueça de limpar a seguir.

Levantar os bloqueios

Libertar um único endereço de um jail concreto:

fail2ban-client set sshd unbanip 203.0.113.10

Um endereço em todos os jails de uma só vez:

fail2ban-client unban 203.0.113.10

E, em caso de emergência, tudo a zero:

fail2ban-client unban --all

O último comando é o que precisa quando se bloqueou a si próprio e está sentado à frente de uma consola de emergência do servidor. Levanta todos os bloqueios sem reiniciar o serviço. Se já não conseguir chegar ao servidor de forma nenhuma, resta o caminho pela consola na área de cliente. Nos servidores root KVM e nos servidores dedicados da KernelHost tem ali acesso a uma sessão VNC que funciona de forma independente do SSH. É também por isso que não deve definir bantime = -1 num servidor sem uma segunda via de acesso.

Um bloqueio que levante volta de imediato se as tentativas falhadas ainda estiverem a ser contadas dentro da janela de tempo. Por isso, o unban apaga também as tentativas falhadas correspondentes. Quem deve ficar permanentemente livre pertence a ignoreip, seguido de fail2ban-client reload.

Interação com UFW e nftables

É aqui que surge o estrago mais difícil de notar, porque o fail2ban continua alegremente a anunciar bloqueios enquanto os pacotes passam sem obstáculo.

O UFW é um frontend para o iptables. Ao arrancar ou ao recarregar, reescreve a tabela de filtros através de iptables-restore. Nesse processo, as chains criadas pelo fail2ban desaparecem sem substituição, e o fail2ban não volta a criá-las por iniciativa própria. Depois de cada ufw enable, ufw disable ou ufw reload vale por isso a regra: systemctl restart fail2ban. Até lá, o fail2ban-client status sshd mostra uma longa lista de IPs bloqueados, dos quais nenhum está realmente a ser bloqueado.

Mais limpo é deixar o fail2ban bloquear diretamente através do UFW. A ação está em /etc/fail2ban/action.d/ufw.conf e vem incluída nas quatro distribuições. Na jail.local:

[DEFAULT]
banaction = ufw
banaction_allports = ufw

Assim, os bloqueios ficam como regras dentro do próprio UFW e sobrevivem ao seu recarregamento. Verificação:

ufw status numbered

Os endereços bloqueados aparecem ali como DENY IN, bem no topo da lista. É importante que as regras de bloqueio venham antes da sua autorização para a porta 22, caso contrário é a autorização que atua primeiro. Por isso, a ação fornecida coloca-as no início com insert.

Quem não usa UFW e mantém em vez disso um conjunto de regras próprio em /etc/nftables.conf tem um problema aparentado. Um flush ruleset no início desse ficheiro, tal como o modelo padrão sugere, apaga também a f2b-table em cada recarregamento. Ou prescinde do flush global e limpa apenas a sua própria tabela, ou liga o reinício do fail2ban ao serviço nftables. A vantagem da tabela própria do fail2ban é atuar com prioridade filter - 1, ou seja, antes da tabela de filtros normal e também antes de regras vindas do iptables-nft.

O que não deve fazer é misturar banaction = nftables com um conjunto de regras iptables próprio à espera de que iptables -L mostre os bloqueios. Os dois caminhos funcionam em paralelo, mas cada ferramenta mostra apenas o seu próprio conjunto de regras. Verifique sempre com a ferramenta que corresponde à banaction configurada.

As mensagens de erro na íntegra e o que está por trás delas

Consoante o sistema, encontra os logs em dois sítios:

journalctl -u fail2ban --no-pager -n 100
tail -n 100 /var/log/fail2ban.log

Failed during configuration: Have not found any log file for sshd jail

A mensagem que assina o Debian 12. O jail corre com backend = auto, procura /var/log/auth.log, e o ficheiro não existe porque não está instalado nenhum rsyslog e tudo vai parar ao journal. O serviço nem chega a arrancar, e o systemctl status fail2ban mostra Failed with result 'exit-code'. Duas soluções: ou apt install -y rsyslog e um reinício, ou definir em jail.local, dentro de [sshd], a linha backend = systemd. O segundo caminho é o mais moderno, mas exige o pacote seguinte.

Backend 'systemd' failed to initialize due to No module named 'systemd'

Definiu backend = systemd, mas falta a ligação de Python ao journal. No Debian 12, python3-systemd é apenas recomendado e não é instalado em sistemas configurados sem pacotes recomendados. Solução:

apt install -y python3-systemd
systemctl restart fail2ban

Failed to access socket path: /var/run/fail2ban/fail2ban.sock. Is fail2ban running?

O cliente não chega ao servidor. Na esmagadora maioria dos casos, o serviço simplesmente não está a correr porque falhou num erro de configuração. Leia primeiro systemctl status fail2ban, depois o journal. Raramente, após uma paragem abrupta, fica para trás um ficheiro de socket órfão e o arranque do servidor devolve Server already running. Nesse caso, remova o ficheiro de socket e reinicie.

NOK: ('sshd',)

A resposta a fail2ban-client status sshd quando esse jail não existe em tempo de execução. Ou falta enabled = true, ou houve um erro de escrita no nome, ou um erro de sintaxe mais acima em jail.local engoliu a secção. Quais os jails que estão mesmo a correr, mostra-o fail2ban-client status. O que o fail2ban montou a partir de todos os ficheiros, mostra-o o despejo da configuração:

fail2ban-client -d

Error banning e iptables not found

O jail conta corretamente, mas o bloqueio falha na ação. É típico em instalações enxutas de Debian 12, onde só existe nftables enquanto a predefinição é iptables-multiport. Ou instala depois apt install -y iptables, ou muda em jail.local para banaction = nftables. O segundo caminho é aquele que o Debian 13 e o Ubuntu 24.04 já seguem por si próprios.

Quando nada mais resulta: a reversão ordenada

Como todas as alterações estão em jail.local, o caminho de volta é curto. Ponha o ficheiro de lado, reinicie o serviço e fica de novo com as predefinições do pacote:

mv /etc/fail2ban/jail.local /root/jail.local.bak
systemctl restart fail2ban
fail2ban-client status

Se assim funcionar, o erro está na sua configuração e volta a inserir os blocos um a um. Se mesmo assim não funcionar, a causa está no ambiente, ou seja, na fonte de log em falta ou na ferramenta de firewall em falta. É precisamente por isso que jail.conf tem de ficar intocada: esta reversão em dez segundos só é possível enquanto o ficheiro do pacote estiver no estado de origem.

Para terminar, o enquadramento. O fail2ban reduz o ruído nos logs e trava tentativas de autenticação lentas e repetidas. Um ataque a sério vindo de uma botnet grande usa cada endereço uma única vez e passa ao lado de qualquer rate limit. O passo mais eficaz contra a força bruta em SSH continua a ser desativar a autenticação por palavra-passe a favor de chaves. E contra ataques volumétricos só ajuda mesmo a filtragem na rede, no nosso caso através de 3,2 Tbps de filtragem Arbor em tempo real diretamente no datacenter em Frankfurt am Main e até 17 Tbps de capacidade global de filtragem nos tarifários Professional. O fail2ban é a camada de baixo, no host, e aí cumpre a sua função de forma fiável assim que três coisas estiverem certas: a fonte de log correta, a ação de firewall correta e uma jail.local que, em caso de dúvida, possa voltar a remover de uma só vez.

Perguntas frequentes

Tenho mesmo de deixar a jail.conf sem lhe tocar?
Sim. O ficheiro pertence ao pacote e pode ser substituído nas atualizações. Todas as alterações vão para /etc/fail2ban/jail.local. Esse ficheiro é lido depois de jail.conf e de jail.d/*.conf, pelo que também sobrepõe os valores predefinidos de defaults-debian.conf.
Porque é que o fail2ban não arranca no Debian 12 depois da instalação?
Porque o jail sshd trabalha com backend = auto e espera /var/log/auth.log, um ficheiro que não existe sem o rsyslog instalado. No journal fica então "Failed during configuration: Have not found any log file for sshd jail". Solução: instalar o rsyslog ou definir backend = systemd e instalar a seguir o python3-systemd.
O que significa exatamente backend = auto?
O auto experimenta, por esta ordem, pyinotify e polling. Ambos são métodos baseados em ficheiros. O auto nunca escolhe o journal do systemd. Quem quiser analisar o journal tem de definir backend = systemd de forma explícita.
Como levanto novamente um bloqueio?
Individualmente com fail2ban-client set sshd unbanip endereço-IP, em todos os jails com fail2ban-client unban endereço-IP e, em caso de emergência, tudo de uma vez com fail2ban-client unban --all. Quem deve ficar permanentemente livre pertence a ignoreip, seguido de fail2ban-client reload.
Porque é que o fail2ban bloqueia e o atacante passa à mesma?
Na maioria dos casos, o UFW reescreveu entretanto a tabela de filtros através de iptables-restore e removeu nesse processo as chains do fail2ban. Depois de cada ufw enable, disable ou reload é preciso reiniciar o fail2ban. Mais limpo é banaction = ufw, porque assim os bloqueios vivem dentro do próprio UFW.
Como sei que o fail2ban está mesmo a funcionar?
Não é pelo facto de o serviço estar a correr. Verifique com fail2ban-regex se o filtro acerta em linhas e bloqueie manualmente, a título de teste, um endereço de 203.0.113.0/24. Depois disso ele tem de aparecer na firewall, de acordo com a banaction configurada: no Debian 13 e no Ubuntu 24.04 com nft list table inet f2b-table, no set addr-set-sshd; no Debian 12 e no Ubuntu 22.04 com iptables -n -L f2b-sshd, na cadeia com o mesmo nome. Em sistemas com nftables não existe qualquer cadeia f2b-sshd.
O fail2ban substitui uma proteção DDoS?
Não. O fail2ban atua no host contra tentativas de autenticação repetidas de endereços individuais. Os ataques volumétricos têm de ser filtrados na rede, antes de chegarem ao servidor.

fail2ban SSH Segurança de servidores Força bruta Debian Ubuntu nftables UFW