Pitch: Criei um dashboard local para o CrowdSec + NGINX
Faz um tempo que administro meu próprio servidor em casa. A segurança dele já era funcional — CrowdSec bloqueando ataque, nginx logando tudo — mas era cega. Para entender o que estava acontecendo eu vivia no terminal, cruzando cscli decisions list com tail -f do access.log na mão.
Como o dashboard local do CrowdSec (cscli dashboard, baseado em Metabase) foi deprecado na 1.6 e removido na 1.7. A recomendação oficial virou o CrowdSec Console, hospedado na nuvem. Quem faz questão de manter tudo self-hosted ficou sem alternativa oficial.
Fonte: https://docs.crowdsec.net/blog/cscli_dashboard_deprecation
Então construí a minha. É o Mini SOC.
Para que serve
Ele junta, numa interface só, as duas coisas que antes viviam separadas: os bloqueios do CrowdSec (IDS/IPS) e os logs de acesso do nginx. Tudo rodando na própria máquina do servidor — nada sai para a nuvem.
Por que não um Grafana?
Foi a primeira pergunta que me fizeram, e é o ponto central do projeto.
Grafana, Metabase e afins são read-only. Desenham gráfico lindo em cima do banco, mas param aí. Se vejo um IP martelando o servidor, o dashboard não me deixa reagir — tenho que voltar para o terminal.
Eu não queria olhar, queria operar. No Mini SOC dá para banir, desbanir, ajustar a duração de um ban, tornar permanente ou jogar um IP na whitelist com um clique, direto do painel.
E, para não virar gambiarra: não tem banco paralelo. A leitura sai direto do SQLite do CrowdSec em modo somente-leitura (PRAGMA query_only = ON); toda escrita vai por subprocess chamando o próprio cscli (lista de argumentos, nunca shell=True). O CrowdSec continua sendo a fonte da verdade — o painel é só uma cara nova em cima dele.
O que tem dentro
CrowdSec (IDS/IPS) — cards de resumo (bloqueados agora, IPs únicos, novos hoje, últimas 24h), série temporal e rankings por país / cenário / ASN. Filtros que aplicam sozinhos, sem botão "Aplicar". Geolocalização de IP sob demanda, com cache.
Logs do nginx — leitor dos *access.log* (inclui .gz rotacionados), com filtros, busca e estatísticas em gráfico. Tem tail ao vivo via SSE: dá para acompanhar o acesso em tempo real sem F5. Modal de detalhe por request e badge marcando IP banido.
Alertas — um poller avisa na hora sobre banimentos novos via Telegram, e-mail ou webhook. Não preciso ficar com o painel aberto.
Onde o bloqueio realmente acontece (duas camadas)
O painel decide banir, mas quem executa o bloqueio são os bouncers do CrowdSec. E aqui tem um detalhe que me pegou de surpresa e talvez pegue você: um bouncer só não cobre tudo. Uso duas camadas, porque elas bloqueiam em pontos diferentes.
Camada 3/4 — cs-firewall-bouncer (bloqueio direto no firewall). Ele dropa os pacotes pelo IP de origem, via nftables/iptables. Cobre muito bem quem bate direto no IP do servidor — o scanner que varre a internet por IP nu. O problema: se o tráfego passa por um CDN (no meu caso, Cloudflare), o pacote chega com o IP da borda do CDN, não o do atacante. Ou seja: o IP banido passa pelo firewall, porque o firewall nunca vê o IP real.
Camada 7 — crowdsec-nginx-bouncer (bloqueio no HTTPS/443). Esse roda dentro do próprio nginx (Lua) e checa o IP real do visitante (o CF-Connecting-IP, resolvido pelo real_ip) contra as decisões do CrowdSec. Se estiver banido, devolve 403 na cara — na porta 443, no request HTTP, independente de o pacote ter vindo do CDN. É o que fecha o buraco que o firewall-bouncer sozinho deixa.
Os dois convivem: o firewall cobre acesso direto e tráfego não-HTTP; o nginx-bouncer cobre o HTTPS que vem via CDN. E, já que a página de 403 padrão entrega que é CrowdSec, troquei por uma tela de bloqueio própria — sem dar pista da stack pra quem apanhou:
Um aprendizado prático que deixo registrado: nos meus testes (nginx 1.26 + bouncer 1.1.6), o modo
livedo nginx-bouncer não bloqueava de forma confiável — só firmou comMODE=stream. Se o seu 403 não estiver disparando, é o primeiro lugar pra olhar.
Como instalar
Se você já tem CrowdSec + nginx num Debian com Python 3.11+, subir o painel é clonar, criar o venv, ajustar um .env e apontar para os seus caminhos:
git clone https://github.com/MateusBrandeburski/Mini-SOC.git
cd Mini-SOC
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python -c "import secrets; print('APP_SECRET=' + secrets.token_urlsafe(48))" # cole no .env
python auth.py 'sua-senha-forte' # gera o hash bcrypt do admin
$EDITOR .env # ajuste APP_SECRET, ADMIN_PASSWORD_HASH e os caminhos do CrowdSec/nginx
python app.py # sobe em http://0.0.0.0:8100
Em produção você joga isso numa unit systemd (After=crowdsec.service) — tem um exemplo pronto em deploy/painel-crowdsec.service. O .env.example documenta todas as variáveis; o mínimo é APP_SECRET, a senha do admin e os caminhos do banco do CrowdSec e dos logs do nginx.
Em poucos minutos o painel está no ar lendo os seus dados. Sem migração, sem banco extra, sem nuvem.
Como eu acesso: reverse proxy no nginx
A app sobe em 127.0.0.1:8100, mas eu não acesso ela por IP:porta. Já que o servidor já roda nginx, faço o painel entrar no mesmo esquema de todo o resto: um server de reverse proxy que atende por um nome interno (ex.: painel.interno.exemplo) e repassa para a app. Assim tenho tudo gerenciado num lugar só — mesma stack, mesmo TLS, mesma cara dos outros serviços internos.
Só que o painel tem duas particularidades que o proxy precisa respeitar:
1. Ele nunca deve ficar exposto na internet. Como ele opera o CrowdSec (bane e desbane IP), é a última coisa que você quer aberta para fora. No meu server a primeira coisa é uma allowlist de IP — só a rede interna / VPN passa, o resto leva deny all. O login da própria app continua valendo; a allowlist é uma camada antes dele.
server {
listen 80;
server_name painel.interno.exemplo;
# Só a rede interna / VPN entra. O resto nem chega no login.
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
location / {
proxy_pass http://127.0.0.1:8100;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE (tail ao vivo dos logs): sem buffering, conexão viva.
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_set_header Connection "";
}
}
2. O tail ao vivo é SSE, e SSE morre atrás de um proxy com buffering ligado. Por isso o proxy_buffering off e o proxy_read_timeout alto — sem isso, o "acompanhar em tempo real" simplesmente não chega ao navegador. Esse detalhe me custou um tempinho de debug, então já deixo o exemplo pronto no repo.
O arquivo completo (com a variante HTTPS via certbot e a camada opcional de basic-auth do nginx) está em deploy/nginx-reverse-proxy.conf.example.
[IMG: opcional — o painel aberto no navegador já pela URL interna (
https://painel.interno.exemplo), mostrando que ele vive junto dos outros serviços, não numIP:8100solto.]
Código aberto (MIT): https://github.com/MateusBrandeburski/Mini-SOC
Feedback e PR bem-vindos — principalmente de quem também roda CrowdSec self-hosted e sentiu essa mesma falta.