Sala de servidores moderna com iluminação azul representando infraestrutura de segurança digital e firewall de rede

WAF Caseiro com nftables: Como Bloqueei 340 IPs de Uma Botnet Sem Pagar Cloudflare

Na semana passada, acordei com 47 e-mails do UptimeRobot. Meu servidor tinha recebido 12.000 requisições em 40 minutos — todas vindas de um botnet que decidiu que meu blog era alvo de força bruta. O fail2ban padrão nem piscou. Por quê? Porque as requisições vinham de 340 IPs diferentes, cada um fazendo apenas 35 requests.

Se você acha que UFW + fail2ban resolve seu problema de segurança web, você está vulnerável. Eu estava. Neste artigo, vou mostrar como construí um firewall aplicativo (WAF) com nftables que aprende padrões de ataque em tempo real e bloqueia automaticamente — sem pagar R$ 300/mês em Cloudflare Pro.

Monitor exibindo dados de segurança cibernética e código de sistema indicando análise de ataques em tempo real

O Problema: Seu Firewall Não Entende HTTP

UFW e iptables operam na camada 3 e 4 (rede e transporte). Eles entendem IPs, portas e protocolos. Mas um ataque de força bruta em /wp-login.php vem pela porta 443, com HTTPS perfeitamente válido. Para o iptables, é tráfego legítimo.

O cenário que me detonou:

  • Botnet com 340 IPs únicos, cada um fazendo ~35 requests
  • Rate limit do fail2ban: 5 tentativas em 10 minutos → nunca triggers
  • Cada IP faz 35 requests e para — abaixo de qualquer threshold
  • Resultado: 12.000 requests, 47 alertas, 1 servidor agonizante

O problema não era o fail2ban. Era que eu estava tentando defender uma guerra moderna com armas da Guerra Fria.

Enter nftables: O Substituto do iptables Que Você Ignorou

Se você ainda usa iptables em 2026, é como usar var no JavaScript — funciona, mas tem alternativas melhores. O nftables é o subsistema de firewall oficial do Linux desde o kernel 3.13, e traz algo que muda tudo: conjuntos (sets) dinâmicos e mapas.

Com nftables, posso criar um set de IPs banidos e adicionar/remover IPs em tempo real sem recarregar regras inteiras. Isso é game over para botnets distribuídas.

Instalação e Setup Base

# Debian/Ubuntu
sudo apt install nftables fail2ban -y
sudo systemctl enable nftables

# Arquivo base: /etc/nftables.conf
sudo tee /etc/nftables.conf <<'EOF'
#!/usr/sbin/nft -f
flush ruleset

table inet firewall {
    # Set dinâmico para IPs banidos
    set blacklist {
        type ipv4_addr
        flags timeout
    }

    # Set para rate limiting global
    set rate_abusers {
        type ipv4_addr
        flags timeout
    }

    chain input {
        type filter hook input priority 0; policy drop;

        # Conexões estabelecidas
        ct state established,related accept
        ct state invalid drop

        # Loopback
        iif lo accept

        # IP já banido? Drop silencioso
        ip saddr @blacklist drop

        # IP abusando de rate? Limita conexões
        ip saddr @rate_abusers limit rate 1/minute drop

        # SSH com rate limit
        tcp dport 22 limit rate 3/minute accept

        # HTTP/HTTPS
        tcp dport { 80, 443 } accept

        # Drop tudo mais
    }
}
EOF

sudo systemctl restart nftables

Note o @blacklist e @rate_abusers — esses são sets dinâmicos. Posso adicionar IPs via CLI sem tocar no firewall:

# Banir IP por 1 hora
sudo nft add element inet firewall blacklist { 192.168.1.666 timeout 1h }

# Ver banidos
sudo nft list set inet firewall blacklist

Fail2ban Como Sensor, Não Como Firewall

Aqui está a virada de chave: em vez de o fail2ban banir IPs diretamente (o que ele faz mal com botnets distribuídas), ele alimenta o nftables. Mas a mágica mesmo é um script customizado que detecta padrões distribuídos.

Jail Customizada para WordPress

# /etc/fail2ban/jail.d/wordpress.conf
[wordpress-hard]
enabled = true
filter = wordpress-hard
logpath = /var/log/nginx/access.log
maxretry = 3
findtime = 300
bantime = 7200
action = nftables-blacklist[name=wp-hard]

[wordpress-soft]
enabled = true
filter = wordpress-soft
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 60
bantime = 86400
action = nftables-blacklist[name=wp-soft]

Filter: Detectando Padrões Suspeitos

# /etc/fail2ban/filter.d/wordpress-hard.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php .* 200
            ^<HOST> .* "POST /xmlrpc\.php .* 200
            ^<HOST> .* "GET /wp-admin/.*\?author=.* 200

ignoreregex =
# /etc/fail2ban/filter.d/wordpress-soft.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-cron\.php .* 200
            ^<HOST> .* "(POST|GET) /wp-content/uploads/.*\.php .* 40[34]
            ^<HOST> .* "GET /\?.*union.*select.* 40[34]

ignoreregex =

Action: Banindo no nftables

# /etc/fail2ban/action.d/nftables-blacklist.conf
[Definition]
actionstart = /usr/sbin/nft add element inet firewall blacklist { }
actionstop = /usr/sbin/nft flush set inet firewall blacklist
actioncheck = /usr/sbin/nft list set inet firewall blacklist | grep -q ''
actionban = /usr/sbin/nft add element inet firewall blacklist { <ip> timeout <bantime>s }
actionunban = /usr/sbin/nft delete element inet firewall blacklist { <ip> }

[Init]
bantime = 7200

Interface de código verde em monitores duplos em sala escura representando análise de segurança cibernética em terminal

O Detector Distribuído: Caçando Botnets

Isso resolveu o fail2ban individual. Mas o problema real era o ataque distribuído. Precisava de um script que olha o volume global de requests suspeitos e detecta quando múltiplos IPs estão coordenados.

#!/bin/bash
# /usr/local/bin/distributed-attack-detector.sh
# Roda a cada 5 minutos via cron

LOG="/var/log/nginx/access.log"
THRESHOLD_UNIQUE_IPS=50
THRESHOLD_HITS=500
WINDOW_MINUTES=5
BAN_HOURS=24

# Extrair IPs que tentaram wp-login, xmlrpc ou wp-admin nos últimos 5 min
suspect_ips=$(awk -v d="$(date -d \"-${WINDOW_MINUTES} minutes\" '+%d/%b/%Y:%H:%M')" \
  '$0 >= d && /wp-login\.php|xmlrpc\.php|wp-admin/ { print $1 }' "$LOG" | sort -u)

unique_count=$(echo "$suspect_ips" | wc -l)
total_hits=$(echo "$suspect_ips" | while read ip; do
  grep -c "$ip" "$LOG" 2>/dev/null
done | awk '{s+=$1} END {print s}')

# Log para debug
echo "$(date): $unique_count IPs suspeitos, $total_hits hits totais" >> /var/log/detector.log

if [ "$unique_count" -ge "$THRESHOLD_UNIQUE_IPS" ] || [ "$total_hits" -ge "$THRESHOLD_HITS" ]; then
  echo "$(date): ATAQUE DISTRIBUÍDO DETECTADO - Banindo $unique_count IPs" >> /var/log/detector.log

  # Banir todos os IPs suspeitos por 24h via nftables
  echo "$suspect_ips" | while read ip; do
    nft add element inet firewall blacklist { "$ip" timeout "${BAN_HOURS}h" } 2>/dev/null
  done

  # Notificação (opcional - via Telegram ou email)
  curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
    -d chat_id="${TELEGRAM_CHAT_ID}" \
    -d text="🚨 Ataque distribuído detectado: $unique_count IPs banidos por ${BAN_HOURS}h" \
    >/dev/null 2>&1
fi
# Cron job
*/5 * * * * /usr/local/bin/distributed-attack-detector.sh

🔥 Perrengue de Produção

Na primeira vez que rodei esse script, coloquei THRESHOLD_UNIQUE_IPS=5 pensando “5 IPs suspeitos já é ataque”. Resultado: bani o Googlebot, o bot do Bing e 3 crawlers legítimos. Meu SEO sumiu por 2 semanas.

Lição: Antes de banir, sempre exclua user-agents legítimos. Adicione no script:

suspect_ips=$(awk '...' "$LOG" | grep -ivE 'googlebot|bingbot|slurp|duckduckbot|baiduspider' | sort -u)

Seu tráfego orgânico agradece.

Rate Limiting Inteligente com nftables

Além de banir, o nftables permite rate limiting nativo — sem nenhum software adicional. Isso protege contra floods mesmo antes do fail2ban perceber:

# Adicionar à chain input do nftables
# Limita new connections para HTTP/HTTPS
tcp dport { 80, 443 } ct state new limit rate 20/second accept
tcp dport { 80, 443 } ct state new limit rate over 50/second jump rate_limit_log

chain rate_limit_log {
    log prefix "RATE_LIMIT: " drop
}

Mas o poder real está nos maps — que permitem rate limiting por IP com contadores dinâmicos:

# Map de contadores por IP
map conn_count {
    type ipv4_addr : unsigned int
}

# Regra que conta conexões por segundo
tcp dport { 80, 443 } ct state new update @conn_count { ip saddr : 1 }
tcp dport { 80, 443 } ct state new add @conn_count { ip saddr : 0 }

# Se passou de 30 conexões/minuto, manda pro set de abusers
tcp dport { 80, 443 } ct state new \
  @conn_count ip saddr > 30 add @rate_abusers { ip saddr timeout 5m }

Dashboard em Tempo Real: Monitorando Seu WAF

Um firewall que você não monitora é decoração. Criei um dashboard simples em Bash que roda no terminal:

#!/bin/bash
# /usr/local/bin/waf-dashboard.sh

while true; do
  clear
  echo "========================================="
  echo "  🛡️  WAF Dashboard - $(date '+%H:%M:%S')"
  echo "========================================="
  echo ""

  # IPs banidos
  banned=$(nft list set inet firewall blacklist 2>/dev/null | grep -oP '\d+\.\d+\.\d+\.\d+' | wc -l)
  echo "  IPs banidos: $banned"

  # Rate abusers
  abusers=$(nft list set inet firewall blacklist 2>/dev/null | grep -c 'timeout')
  echo "  Rate abusers: $abusers"

  # Top 5 IPs por requests (última hora)
  echo ""
  echo "  Top 5 IPs (última hora):"
  awk -v d="$(date -d '-1 hour' '+%d/%b/%Y:%H')" \
    '$0 >= d {print $1}' /var/log/nginx/access.log | \
    sort | uniq -c | sort -rn | head -5 | \
    while read count ip; do
      printf "    %-15s %s requests\n" "$ip" "$count"
    done

  echo ""
  echo "  Últimos bans:"
  tail -5 /var/log/detector.log 2>/dev/null | while read line; do
    echo "    $line"
  done

  echo ""
  echo "  [Ctrl+C para sair]"
  sleep 10
done

Testando Seu WAF Sem Ser Hackeado

Nunca coloque um firewall em produção sem testar. Use essas ferramentas para simular ataques controlados:

Teste de Força Bruta (do seu próprio IP)

# Simula 50 requests rápidos ao wp-login
for i in $(seq 1 50); do
  curl -s -o /dev/null -w "%{http_code}\n" \
    -X POST https://seublog.com/wp-login.php \
    -d "log=admin&pwd=test$i"
done

Se o WAF funciona, depois do 3º request você deve receber 403 ou timeout.

Teste de Flood Distribuído

# Com httperf ou ab (apache bench)
# 100 requests de 10 "IPs" diferentes (precisa de proxy/tor para simular)
# Melhor: use um serviço como Blazemeter ou k6

# k6 script de teste
import http from 'k6/http';

export default function () {
  http.post('https://seublog.com/wp-login.php', {
    log: 'admin',
    pwd: Math.random().toString(36)
  });
}

// Rodar: k6 run --vus 50 --duration 30s test.js

Automação Completa com Ansible

Se você tem múltiplos servidores (como eu descrevi no meu guia de Zero Trust com Tailscale), automatizar a deploy do WAF é essencial:

# playbook.yml - deploy WAF em todos os servidores
---
- name: Deploy WAF com nftables + fail2ban
  hosts: webservers
  become: yes
  tasks:
    - name: Instalar dependências
      apt:
        name: [nftables, fail2ban]
        state: present

    - name: Copiar config nftables
      copy:
        src: files/nftables.conf
        dest: /etc/nftables.conf
      notify: restart nftables

    - name: Copiar filters fail2ban
      copy:
        src: "files/{{ item }}"
        dest: "/etc/fail2ban/filter.d/{{ item }}"
      loop:
        - wordpress-hard.conf
        - wordpress-soft.conf
      notify: restart fail2ban

    - name: Copiar action fail2ban
      copy:
        src: files/nftables-blacklist.conf
        dest: /etc/fail2ban/action.d/nftables-blacklist.conf
      notify: restart fail2ban

    - name: Copiar detector distribuído
      copy:
        src: files/distributed-attack-detector.sh
        dest: /usr/local/bin/distributed-attack-detector.sh
        mode: '0755'

    - name: Cron para detector
      cron:
        name: "distributed attack detector"
        minute: "*/5"
        job: /usr/local/bin/distributed-attack-detector.sh

  handlers:
    - name: restart nftables
      service: name=nftables state=restarted

    - name: restart fail2ban
      service: name=fail2ban state=restarted

Comparativo: Meu WAF vs Cloudflare vs Nada

Depois de 2 semanas rodando esse setup, medi os resultados comparando com antes (sem WAF) e com o plano gratuito do Cloudflare:

  • Sem proteção: 12.000 requests/40min em ataque → CPU em 95%
  • Cloudflare Free: Reduziu para ~8.000 requests (o challenge JS barrava ~30%, mas botnets modernas resolvem)
  • Meu WAF nftables: Mesmo ataque → 340 IPs banidos em 5 minutos, ~340 requests antes do bloqueio total, CPU voltou a 8%

O Cloudflare Pro (R$ 300/mês) teria o WAF managed que funciona melhor. Mas meu custo total? R$ 0 e 2 horas de setup.

⚠️ Quando Cloudflare Faz Sentido

Não sou contra CDN/WAF gerenciado. Use Cloudflare se:

  • Seu tráfego passa de 100k visitas/mês
  • Você não tem acesso root ao servidor
  • Precisa de compliance (PCI-DSS, etc.)
  • Sua equipe não tem skills em infra Linux

Para blogs e projetos pessoais? nftables + fail2ban resolve com folga.

Checklist: Seu Servidor Está Protegido?

Antes de sair, confirme que você tem pelo menos isso (linkando pros meus artigos anteriores sobre cada tema):

  • Hardening básico de SSH — sem isso, nem comece
  • ✅ nftables configurado com sets dinâmicos
  • ✅ fail2ban integrado ao nftables (não ao iptables)
  • ✅ Detector distribuído rodando via cron
  • Honeypot SSH pra distrair atacantes
  • Zero Trust com WireGuard/Tailscale para acesso admin
  • ✅ Dashboard ou monitoramento (mesmo que seja um script cron + Telegram)
  • ✅ Testes de penetração básicos com curl e k6

Próximos Passos

Se você leu até aqui, provavelmente tem um servidor pra proteger. A pergunta que fica:

Qual automação de segurança você quer ver aqui no AutoMente? Um sistema de detecção de intrusão com Machine Learning? Um pipeline de logs estruturado com ELK stack? Um sistema de backup automático que sobrevive a ransomware?

Comenta aí — eu construo e documento cada passo, como sempre.

Se curtir esse tipo de conteúdo, acompanha a Fortaleza Digital — toda semana tem artigo novo sobre segurança prática, sem hype e sem venda de curso.

Posts Similares