Três Camadas de Proteção para Servidores: ipset, Auto-Block e CrowdSec
Olá, Habr! Lembra da minha história sobre o Mac mini e Proxmox? Naquela ocasião, eu estava experimentando com hardware. Desta vez, um problema interessante surgiu da rede, e em grande escala.
O projeto, no qual desenvolvi este caso, não tem alta demanda, mas é comercial. E é importante para mim entender que o servidor está gastando recursos em clientes, e não em scanners.
Em um determinado momento, uma dinâmica atípica apareceu nos relatórios: um pico de visitas com crescimento zero na conversão. Ao analisar os logs, ficou claro que a maioria das solicitações vinha de regiões não relacionadas ao público-alvo do site.
- Conversão: 0%
- Taxa de rejeição: 99%
- Objetivo da visita: .env, wp-login.php, config.php e outras "portas dos fundos".
Para um projeto em Bitrix, isso significa:
- Carga extra no PHP-FPM;
- Logs inflados;
- E o risco de perder um ataque real em meio a esse ruído.
Foi assim que começou a evolução do meu próprio WAF (Web Application Firewall).
Por que não um WAF ou CDN externo?
A primeira pergunta lógica é: por que escrever sua própria solução se existem WAFs e CDNs prontos?
A resposta está nas prioridades de arquitetura do projeto.
-
Controle e transparência.
É importante para mim ver qual IP está bloqueado, por qual regra e por quanto tempo. Sem uma "caixa preta" entre mim e o servidor.
-
Bloqueio antes da aplicação.
Qualquer proteção no nível do PHP já é tardia. Se um bot chega ao Nginx ou PHP-FPM, os recursos já foram gastos.
No meu caso, o bloqueio ocorre no nível do kernel (iptables + ipset), antes que a solicitação seja passada para a aplicação.
-
Flexibilidade para a especificidade do projeto.
As assinaturas típicas nem sempre levam em conta as características de um projeto Bitrix específico. Às vezes, é necessário adicionar rapidamente uma regra para um padrão específico e fazê-lo em poucos minutos.
-
Simplicidade da arquitetura.
O projeto é local, sem um público global. Adicionar uma camada externa para varreduras em segundo plano levará a uma complicação excessiva.
É importante ressaltar que as decisões são tomadas com base na análise de logs HTTP (L7), mas o descarte de pacotes em si é realizado no nível L3/L4 no kernel do Linux. Isso permite remover o lixo antes do Nginx e do PHP.
Eu não estou opondo essa abordagem aos WAFs externos. Para projetos grandes ou internacionais, CDNs e soluções em nuvem são uma escolha absolutamente lógica. No meu caso, uma filtragem cuidadosamente construída do meu lado é suficiente.
Arquitetura: Defesa em Camadas
O sistema resultante é multicamadas.
Como funciona:
-
Primeira linha (bloqueio de países).
Pacotes de países onde não temos interesses são descartados imediatamente. O HTTP nem sequer é analisado.
-
Segunda linha (bloqueio automático de IPs suspeitos).
Se um IP passou pelo geobloqueio, mas começou a procurar por .env, .git ou wp-login.php, ele é banido por 2 horas.
-
Terceira linha (lista negra do CrowdSec).
Se um IP já "se destacou" em algum lugar do mundo, ele pode ser bloqueado preventivamente.
Evolução do Sistema
O sistema não surgiu em sua forma final. Foi um refinamento sequencial.
| Etapa | Arquitetura | Gerenciabilidade | Risco de bloqueios falsos | Manutenção |
|---|---|---|---|---|
| Até v1 | Regras iptables dispersas | Baixa | Aumentado | Edições manuais |
| v1 | iptables + ipset + suspicious | Média | Controlado | Automação parcial |
| v2 | Lógica modular (country / suspicious) | Alta | Mais baixo | Módulos independentes |
| v2 + CrowdSec | Camada de assinatura + custom | Máxima | Minimizado | Base de dados autoatualizável |
A principal mudança na segunda versão é a arquitetura modular. O bloqueio por país, a filtragem de IPs suspeitos e a integração com o CrowdSec agora podem ser ativados ou desativados separadamente. Isso simplificou a depuração e reduziu o risco de bloqueios falsos, adicionando mais gerenciabilidade.
Por que ipset é a base da filtragem
Se você simplesmente adicionar IPs ao iptables, cada regra é uma linha separada na cadeia.
Quando um pacote chega, o kernel percorre as regras de cima para baixo até encontrar uma correspondência. Com 10 regras, isso é praticamente imperceptível, mas com 10.000, começa uma busca linear que já afeta o desempenho.
Em outras palavras:
- Mais IPs → cadeia mais longa → mais comparações;
- Complexidade da verificação - O(n).
Sob carga leve, isso pode ser tolerável. Sob uma onda de varredura, não.
ipset funciona de forma diferente. Em vez de milhares de regras separadas, o iptables tem apenas uma:
bash-m set --match-set blocked_ips src
E os próprios IPs estão dentro de um conjunto (hash:ip ou hash:net). A verificação é feita através de uma tabela hash, e não de uma busca na lista. Isso significa:
- 1 IP no conjunto ou 100.000 - quase não há diferença;
- A verificação é feita em O(1);
- Tudo acontece no nível do kernel.
É por isso que o ipset é a base de todo o esquema. Sem ele, não seria escalável.
Nas primeiras versões, eu atualizava os conjuntos assim:
bashipset flush blocked_ips ipset add blocked_ips
E rapidamente me deparei com um problema. Descobri que, nesse momento, o servidor está efetivamente aberto: entre o flush e o preenchimento, a lista está vazia.
Resolvi isso atualizando o conjunto atomicamente, através de uma lista temporária e swap. Primeiro, criamos um conjunto temporário, depois o preenchemos - o ipset swap é executado e o conjunto antigo é excluído.
Como o swap é executado instantaneamente no nível do kernel, não há intervalo com uma lista vazia. A filtragem não é interrompida - ficamos felizes.
É mais ou menos assim:
bashipset create "${country}_temp" hash:net 2>/dev/null || ipset flush "${country}_temp" if [[ -s "$temp_file" ]]; then while read network; do ipset add "${country}_temp" "$network" done < "$temp_file" ipset swap "${country}_temp" "$country" fi ipset destroy "${country}_temp"
Capturando Scanners por Intenção
auto-block-suspicious.sh não bane por 404s comuns. Ele procura intenções suspeitas do visitante. Exemplos de padrões pelos quais eu determino isso:
- wp-login.php em um projeto Bitrix
- shell.php, shoha.php
- .env, .git, .aws/credentials
Os logs são lidos usando tail -n, e não relidos por completo. O script determina a localização dos logs de acesso ou permite que você especifique o caminho para os logs necessários durante a instalação.
Lógica de Operação
O IP é colocado em um banimento temporário por 2 horas e, em seguida, é automaticamente removido. Um caso real: no início deste ano, uma onda de solicitações para /wp-login.php atingiu um site executando o Bitrix. Mais de 200 tentativas de dezenas de IPs em poucos minutos. Depois de ativar o módulo de verificação de IPs suspeitos, os endereços IP começaram a ser banidos em 20-30 segundos, e a carga retornou ao normal quase imediatamente.
text=== Estatísticas do Sistema de Bloqueio === Bloqueios por país: China: 8802 IPs Total bloqueado por país: 8802 IPs Bloqueio automático de IPs suspeitos: Status: ✓ ATIVADO Total: 10 IPs Exemplos de IPs bloqueados: 4.193.97.168 timeout 4972 195.178.110.109 timeout 3472 195.178.110.246 timeout 3472 144.91.93.174 timeout 4972 20.211.1.210 timeout 4972 130.12.180.34 timeout 4972 45.148.10.238 timeout 4972 195.178.110.199 timeout 4972 89.248.168.239 timeout 7071 94.26.88.31 timeout 4972
CrowdSec como Camada Base
O CrowdSec é conectado através de um bouncer, que adiciona IPs ao ipset crowdsec_blacklist com TTL. Assim:
- As assinaturas globais são fechadas automaticamente;
- As decisões são sincronizadas;
- O banimento é removido por timeout sem intervenção manual.
O CrowdSec não substitui a lógica customizada. Ele fecha padrões conhecidos, e os módulos country e suspicious fecham a especificidade do projeto. Esta é mais uma barreira em nossa defesa.
Métricas: O Que Mudou
Período de comparação: setembro–dezembro de 2025 (sem proteção) e janeiro–fevereiro de 2026 (após a implementação do WAF).
| Indicador | Sem Proteção | Com WAF |
|---|---|---|
| Tráfego Lixo | ~12% | < 0.3% |
| Média LA | 1.8–2.5 | 0.9–1.2 |
| Taxa de Rejeição | Até 40% | 12–15% |
| Análise | Ruído e bots | Dados limpos |
Picos de varredura, chegando a dezenas de RPS, agora são interrompidos no nível do kernel e não chegam ao Nginx.
O gráfico mostra claramente: após ativar o bloqueio por país, o número de solicitações diminui significativamente.
No entanto, esta solução tem limitações:
- Não salva de um ataque com um grande fluxo de tráfego na rede (L4);
- Não substitui as atualizações do Bitrix e outros CMSs;
- Não protege contra ataques que vêm através de endereços IP domésticos comuns;
- Não é necessário se você tiver um projeto global com um CDN completo.
Isso ajuda a filtrar o lixo comum e pequenos ataques, mas não substitui a segurança completa (SOC).
O Que Vem a Seguir
Atualmente, o gerenciamento é feito através do console:
bashauto-block-control.sh status ipset list blocked_ips
Eu gostaria de fazer um painel leve:
- Um mapa de países bloqueados;
- Os principais IPs agressores;
- Um botão "perdoar".
Se alguém tiver ideias sobre uma stack de UI leve para essa tarefa, ficarei feliz em discutir.
Conclusão
O resultado é um sistema de filtragem claro e gerenciável. Ele não substitui a atualização do Bitrix (outros CMSs) e o fechamento de vulnerabilidades, mas remove o ruído de fundo. O tráfego passa por vários níveis, as decisões são tomadas de forma transparente, as atualizações são executadas atomicamente e toda a lógica permanece sob controle. Agora, o servidor processa o que foi projetado para processar: solicitações de usuários reais.
Toda a estrutura está disponível no GitHub. Lá, há um instalador que configurará tudo para você.
Ficarei feliz se você compartilhar seus padrões de bloqueio ou contar como você sobrevive em condições de bloqueios locais.






