feat(crowdsec): añadir stack de detección de intrusiones con Grafana
- docker-compose.yml: CrowdSec + bouncer de Traefik + Postgres + Grafana - setup.sh: configuración inicial de colecciones, bouncers y whitelist - metrics-export.sh: exportación de métricas al endpoint de Prometheus - traefik-crowdsec-bouncer.yml: config dinámica de Traefik para el bouncer - stack.env: plantilla con API keys y configuración Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
# CrowdSec - Sistema de Seguridad y Prevención de Intrusiones
|
||||
|
||||
CrowdSec es un IPS/IDS colaborativo que analiza logs, detecta comportamientos maliciosos y bloquea atacantes
|
||||
en tiempo real. Usa inteligencia comunitaria para compartir reputación de IPs entre todos los usuarios.
|
||||
|
||||
## 📋 Descripción
|
||||
|
||||
Este stack despliega:
|
||||
- **CrowdSec Engine**: Motor de detección que analiza logs de Traefik y otros contenedores
|
||||
- **CrowdSec Bouncer (Traefik Plugin)**: Middleware que bloquea IPs maliciosas a nivel HTTP (L7)
|
||||
- **Grafana**: Dashboard web para visualizar alertas, decisiones y métricas (protegido con Authentik)
|
||||
- Baneos automáticos contra ataques de fuerza bruta, escaneos, CVEs conocidos y más
|
||||
|
||||
## 🚀 Despliegue
|
||||
|
||||
### Prerequisitos
|
||||
|
||||
1. **Red Docker**: La red `proxy` debe existir
|
||||
```bash
|
||||
docker network create proxy
|
||||
```
|
||||
|
||||
2. **Registro DNS**: Configura el registro A para el dashboard
|
||||
- `grafana.tudominio.com` → IP del servidor
|
||||
|
||||
3. **Directorios**: Créalos automáticamente con el setup:
|
||||
```bash
|
||||
cd crowdsec && ./setup.sh
|
||||
```
|
||||
|
||||
### Paso 1: Configurar variables de entorno
|
||||
|
||||
Copia la plantilla y edita las variables:
|
||||
|
||||
```bash
|
||||
cp crowdsec/stack.env crowdsec/.env
|
||||
nano crowdsec/.env
|
||||
```
|
||||
|
||||
Variables críticas:
|
||||
|
||||
```env
|
||||
# Clave API del bouncer (generada automáticamente por setup.sh o pon una manual)
|
||||
CROWDSEC_BOUNCER_API_KEY=tu-key-de-64-caracteres
|
||||
|
||||
# Dominio del dashboard Grafana
|
||||
CROWDSEC_GRAFANA_DOMAIN=grafana.tudominio.com
|
||||
|
||||
# Colecciones a instalar
|
||||
CROWDSEC_COLLECTIONS=crowdsecurity/traefik
|
||||
|
||||
# Rutas de datos
|
||||
CROWDSEC_CONFIG_PATH=/opt/crowdsec/config
|
||||
CROWDSEC_DATA_PATH=/opt/crowdsec/data
|
||||
TRAEFIK_LOG_PATH=/opt/traefik/logs
|
||||
```
|
||||
|
||||
### Paso 2: Ejecutar el script de setup
|
||||
|
||||
```bash
|
||||
cd crowdsec
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
El script:
|
||||
1. Genera una clave aleatoria para el bouncer (si no está definida)
|
||||
2. Crea los directorios necesarios en `/opt/crowdsec/` y `/opt/traefik/logs`
|
||||
3. Genera la configuración dinámica de Traefik (`crowdsec-bouncer.yml`)
|
||||
4. Crea los archivos `acquis.yaml` para leer logs de Traefik y Docker
|
||||
5. Aplica contextos SELinux si es necesario
|
||||
|
||||
### Paso 3: (Re)desplegar Traefik
|
||||
|
||||
**Importante**: Traefik necesita reiniciarse para cargar el plugin de CrowdSec y los nuevos volúmenes.
|
||||
|
||||
Desde Portainer:
|
||||
1. Ve a **Stacks** → stack `traefik`
|
||||
2. Actualiza el archivo `.env` de Traefik para incluir `TRAEFIK_LOG_DIR=/opt/traefik/logs`
|
||||
3. Haz clic en **Update the stack**
|
||||
|
||||
O desde CLI:
|
||||
```bash
|
||||
cd Traefik
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Paso 4: Desplegar CrowdSec
|
||||
|
||||
Desde Portainer:
|
||||
1. Ve a **Stacks** → **Add stack**
|
||||
2. Nombre: `crowdsec`
|
||||
3. Selecciona **Repository**
|
||||
4. Configura:
|
||||
- Repository URL: `<tu-repositorio>`
|
||||
- Repository reference: `main`
|
||||
- Compose path: `crowdsec/docker-compose.yml`
|
||||
5. Carga `crowdsec/.env` como archivo de variables de entorno
|
||||
6. Haz clic en **Deploy the stack**
|
||||
|
||||
O desde CLI:
|
||||
```bash
|
||||
cd crowdsec
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Paso 5: Verificar el despliegue
|
||||
|
||||
```bash
|
||||
# Ver contenedores
|
||||
docker ps | grep crowdsec
|
||||
|
||||
# Ver logs del engine
|
||||
docker logs crowdsec
|
||||
|
||||
# Verificar decisiones activas
|
||||
docker exec crowdsec cscli decisions list
|
||||
|
||||
# Verificar métricas
|
||||
docker exec crowdsec cscli metrics
|
||||
|
||||
# Verificar estado del bouncer
|
||||
docker exec crowdsec cscli bouncers list
|
||||
```
|
||||
|
||||
## 🔐 Configurar Authentik para el Dashboard (Grafana)
|
||||
|
||||
El dashboard de Grafana (`grafana.tudominio.com`) está protegido con `ths-authentik@docker`. Para que funcione:
|
||||
|
||||
### En el panel de Authentik:
|
||||
|
||||
1. **Crear Proxy Provider**:
|
||||
- Ve a **Applications** → **Providers** → **Create**
|
||||
- Tipo: **Proxy Provider**
|
||||
- Name: `CrowdSec Grafana`
|
||||
- External host: `https://grafana.tudominio.com`
|
||||
- Authentication flow: `default-authentication-flow`
|
||||
- Authorization flow: `default-provider-authorization-implicit-consent`
|
||||
- Mode: `Forward auth (single application)`
|
||||
|
||||
2. **Crear Application**:
|
||||
- Ve a **Applications** → **Create**
|
||||
- Name: `CrowdSec Grafana`
|
||||
- Slug: `grafana`
|
||||
- Provider: selecciona el creado en paso 1
|
||||
- UI Settings: opcional (icono, descripción)
|
||||
|
||||
3. **Asociar al Embedded Outpost**:
|
||||
- Ve a **Outposts** → `authentik Embedded Outpost` → **Edit**
|
||||
- Asegúrate de que la aplicación `CrowdSec Grafana` está seleccionada
|
||||
- Haz clic en **Update**
|
||||
|
||||
> El middleware `ths-authentik@docker` ya está configurado en las labels del servicio `crowdsec-grafana`. Si otros stacks también lo usan, comparte la misma configuración de Authentik.
|
||||
|
||||
## 🛡️ Proteger servicios con CrowdSec
|
||||
|
||||
### Método 1: Middleware global (recomendado)
|
||||
|
||||
Añade el middleware `crowdsec-bouncer@file` a los routers de Traefik que quieras proteger:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
# ... labels existentes ...
|
||||
traefik.http.routers.mi-app.middlewares: "ths-authentik@docker,crowdsec-bouncer@file"
|
||||
```
|
||||
|
||||
> Importante: los middlewares se encadenan en orden. `crowdsec-bouncer` primero bloquea IPs maliciosas, luego `ths-authentik` autentica.
|
||||
|
||||
### Método 2: Solo CrowdSec (sin Authentik)
|
||||
|
||||
Para servicios que no requieren login pero sí protección anti-ataques (ej: APIs):
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
traefik.http.routers.mi-api.middlewares: "crowdsec-bouncer@file"
|
||||
```
|
||||
|
||||
### Lista de servicios recomendados para proteger
|
||||
|
||||
| Prioridad | Servicio | Motivo |
|
||||
|-----------|----------|--------|
|
||||
| Alta | Traefik Dashboard | Expuesto, blanco de escaneos |
|
||||
| Alta | Authentik | Puerta de entrada SSO, blanco de brute force |
|
||||
| Alta | Gitea | Expuesto a clonaciones maliciosas |
|
||||
| Media | n8n | Webhooks públicos, riesgo de SSRF |
|
||||
| Media | Nextcloud | Login expuesto, brute force |
|
||||
| Media | Homepage | Aunque es solo dashboard, expuesto |
|
||||
|
||||
## 🔍 Comandos útiles
|
||||
|
||||
### Ver estado de CrowdSec
|
||||
|
||||
```bash
|
||||
# Estadísticas generales
|
||||
docker exec crowdsec cscli metrics
|
||||
|
||||
# Decisiones activas (baneos)
|
||||
docker exec crowdsec cscli decisions list
|
||||
|
||||
# Alertas recientes
|
||||
docker exec crowdsec cscli alerts list --limit 20
|
||||
|
||||
# Inspeccionar una IP específica
|
||||
docker exec crowdsec cscli decisions inspect --ip 1.2.3.4
|
||||
|
||||
# Ver bouncers registrados
|
||||
docker exec crowdsec cscli bouncers list
|
||||
```
|
||||
|
||||
### Gestión de decisiones
|
||||
|
||||
```bash
|
||||
# Banear una IP manualmente (1 hora)
|
||||
docker exec crowdsec cscli decisions add --ip 1.2.3.4 --duration 1h --reason "manual"
|
||||
|
||||
# Desbanear una IP
|
||||
docker exec crowdsec cscli decisions delete --ip 1.2.3.4
|
||||
|
||||
# Listar escenarios activos
|
||||
docker exec crowdsec cscli scenarios list
|
||||
```
|
||||
|
||||
### Allowlists (IPs que nunca se banean)
|
||||
|
||||
Usar `cscli allowlists` (disponible desde CrowdSec v1.6+) en lugar del antiguo truco de `decisions add --duration 99999h`. Las allowlists son permanentes y sobreviven a reinicios.
|
||||
|
||||
```bash
|
||||
# Crear una allowlist nueva
|
||||
docker exec crowdsec cscli allowlists create <nombre> -d "Descripción"
|
||||
|
||||
# Añadir una IP o CIDR a una allowlist
|
||||
docker exec crowdsec cscli allowlists add <nombre> <ip-o-cidr>
|
||||
|
||||
# Quitar una IP de una allowlist
|
||||
docker exec crowdsec cscli allowlists remove <nombre> <ip-o-cidr>
|
||||
|
||||
# Listar todas las allowlists y su contenido
|
||||
docker exec crowdsec cscli allowlists list
|
||||
docker exec crowdsec cscli allowlists inspect <nombre>
|
||||
```
|
||||
|
||||
#### Allowlists configuradas en este servidor
|
||||
|
||||
| Allowlist | Contenido | Propósito |
|
||||
|-----------|-----------|-----------|
|
||||
| `vpn-local` | `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `10.8.0.0/24` | Redes privadas y WireGuard VPN |
|
||||
| `cloudflare` | Todos los rangos IPv4 de Cloudflare | Tráfico legítimo de Cloudflare (si se usa Cloudflare Tunnel) |
|
||||
| `telegram-bot` | IPs añadidas dinámicamente con `/allow <ip>` | IPs gestionadas por el bot de Telegram |
|
||||
|
||||
Para recrearlas desde cero:
|
||||
|
||||
```bash
|
||||
# vpn-local
|
||||
docker exec crowdsec cscli allowlists create vpn-local -d "Redes privadas y VPN"
|
||||
docker exec crowdsec cscli allowlists add vpn-local 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 10.8.0.0/24
|
||||
|
||||
# cloudflare
|
||||
docker exec crowdsec cscli allowlists create cloudflare -d "Rangos IPv4 de Cloudflare"
|
||||
docker exec crowdsec cscli allowlists add cloudflare \
|
||||
173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 \
|
||||
141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 \
|
||||
197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 \
|
||||
104.24.0.0/14 172.64.0.0/13 131.0.72.0/22
|
||||
|
||||
# telegram-bot (vacía, el bot la gestiona)
|
||||
docker exec crowdsec cscli allowlists create telegram-bot -d "IPs allowlisteadas via bot de Telegram"
|
||||
```
|
||||
|
||||
### Actualizar la blocklist comunitaria
|
||||
|
||||
```bash
|
||||
# Actualizar manualmente
|
||||
docker exec crowdsec cscli hub update
|
||||
|
||||
# Ver si hay actualizaciones pendientes
|
||||
docker exec crowdsec cscli hub list
|
||||
```
|
||||
|
||||
## ⚙️ Configuración adicional
|
||||
|
||||
### Añadir más colecciones
|
||||
|
||||
Edita `crowdsec/.env` y añade colecciones separadas por coma:
|
||||
|
||||
```env
|
||||
CROWDSEC_COLLECTIONS=crowdsecurity/traefik,crowdsecurity/http-cve,crowdsecurity/whitelist-good-actors
|
||||
```
|
||||
|
||||
Luego redeploya el stack. Colecciones recomendadas:
|
||||
|
||||
| Colección | Descripción |
|
||||
|-----------|-------------|
|
||||
| `crowdsecurity/traefik` | Parsea logs de acceso de Traefik |
|
||||
| `crowdsecurity/http-cve` | Detecta intentos de explotación de CVEs HTTP |
|
||||
| `crowdsecurity/whitelist-good-actors` | Whitelist de servicios legítimos (Google, CloudFlare, etc.) |
|
||||
| `crowdsecurity/nginx` | Escenarios extra para servidores web |
|
||||
| `crowdsecurity/base-http-scenarios` | Escaneos básicos HTTP (path traversal, SQLi, etc.) |
|
||||
|
||||
### Configurar whitelist de IPs locales
|
||||
|
||||
Para que CrowdSec no banee IPs de tu red local/VPN, edita `/opt/crowdsec/config/parsers/s02-enrich/whitelists.yaml`:
|
||||
|
||||
```bash
|
||||
docker exec crowdsec cscli parsers install crowdsecurity/whitelists
|
||||
```
|
||||
|
||||
O añade manualmente en `notifications/http.yaml` el parámetro `whitelist_ips`.
|
||||
|
||||
### Añadir adquisición de logs adicionales
|
||||
|
||||
Edita `/opt/crowdsec/config/acquis.yaml` para añadir fuentes de log adicionales:
|
||||
|
||||
```yaml
|
||||
---
|
||||
# Logs de Traefik (ya configurado)
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/traefik/access.log
|
||||
labels:
|
||||
type: traefik
|
||||
|
||||
# Logs de Authentik (ejemplo adicional)
|
||||
# source: docker
|
||||
# container_name:
|
||||
# - ths-authentik-server
|
||||
# labels:
|
||||
# type: authentik
|
||||
```
|
||||
|
||||
## 🤖 Integración con el bot de Telegram
|
||||
|
||||
El bot `f2b-telegram-bot` (en `/home/felidae/f2b-telegram-bot/`) gestiona fail2ban, firewalld **y CrowdSec** de forma unificada. El comando `/allow <ip>` ejecuta en una sola operación:
|
||||
|
||||
1. Añade la IP a la allowlist persistente del bot
|
||||
2. Sincroniza `ignoreip` en fail2ban
|
||||
3. Permite la IP en firewalld (ipset)
|
||||
4. Elimina cualquier decisión activa en CrowdSec (`cscli decisions delete`)
|
||||
5. Añade la IP a la allowlist `telegram-bot` en CrowdSec (nunca volverá a banearse)
|
||||
|
||||
### Comandos relevantes del bot
|
||||
|
||||
| Comando | Efecto en CrowdSec |
|
||||
|---------|-------------------|
|
||||
| `/allow <ip>` | Unban + añade a allowlist `telegram-bot` |
|
||||
| `/unallow <ip>` | Quita de allowlist `telegram-bot` |
|
||||
| `/unban <ip>` | Solo unban (sin añadir a allowlist) |
|
||||
| `/health` | Muestra estado de LAPI + disponibilidad Docker |
|
||||
|
||||
### Problema CGNAT y mitigación
|
||||
|
||||
Si el servidor está en una red sin IP fija o compartida (CGNAT), un atacante en la misma red puede
|
||||
provocar que tu IP legítima quede baneada por CrowdSec. La mitigación implementada:
|
||||
|
||||
1. **Bot de Telegram**: `/allow <ip>` desbanea y allowlistea de inmediato en fail2ban + firewalld + CrowdSec.
|
||||
2. **WireGuard VPN**: Acceso por `10.8.0.0/24`, en la allowlist `vpn-local` → nunca baneada.
|
||||
3. **Cloudflare Tunnel** (opcional, ver `cloudflared/README.md`): elimina el problema de raíz al ocultar la IP real del servidor.
|
||||
|
||||
### Configuración del bot para CrowdSec
|
||||
|
||||
Variables de entorno relevantes en `.env` del bot:
|
||||
|
||||
```env
|
||||
CROWDSEC_ENABLED=true
|
||||
CROWDSEC_ALLOWLIST_NAME=telegram-bot
|
||||
```
|
||||
|
||||
Requiere que el bot se ejecute en el mismo host que el contenedor `crowdsec` (acceso a `docker exec`).
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Seguridad
|
||||
|
||||
- **API local solo en loopback**: La API de CrowdSec (`cscli lapi`) solo escucha en `127.0.0.1:8080`
|
||||
- **Bouncer integrado en Traefik**: No expone puertos adicionales, todo el filtrado ocurre dentro de Traefik
|
||||
- **Dashboard protegido por Authentik**: Grafana requiere autenticación SSO previa
|
||||
- **Motor con capacidades mínimas**: Solo `NET_BIND_SERVICE`, sin `CAP_SYS_ADMIN` ni privilegios elevados
|
||||
- **SELinux**: Volúmenes con contexto `:Z` para entornos con SELinux habilitado
|
||||
- **Key del bouncer**: Generada aleatoriamente y almacenada solo en `.env` (gitignorado)
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### El bouncer no bloquea
|
||||
|
||||
1. Verifica que el plugin está cargado en Traefik:
|
||||
```bash
|
||||
docker logs traefik | grep crowdsec
|
||||
```
|
||||
|
||||
2. Verifica que el middleware está configurado:
|
||||
```bash
|
||||
docker exec traefik wget -qO- http://localhost:8080/api/http/middlewares | grep crowdsec
|
||||
```
|
||||
|
||||
3. Verifica que CrowdSec tiene decisiones activas:
|
||||
```bash
|
||||
docker exec crowdsec cscli decisions list
|
||||
```
|
||||
|
||||
4. Comprueba conectividad entre Traefik y CrowdSec:
|
||||
```bash
|
||||
docker exec traefik wget -qO- http://crowdsec:8080/v1/heartbeat
|
||||
```
|
||||
|
||||
### CrowdSec no detecta ataques
|
||||
|
||||
1. Verifica que los logs de Traefik se están generando:
|
||||
```bash
|
||||
ls -lh /opt/traefik/logs/access.log
|
||||
tail -f /opt/traefik/logs/access.log
|
||||
```
|
||||
|
||||
2. Verifica el acquis de CrowdSec:
|
||||
```bash
|
||||
docker exec crowdsec cat /etc/crowdsec/acquis.yaml
|
||||
```
|
||||
|
||||
3. Revisa los logs del engine:
|
||||
```bash
|
||||
docker logs -f crowdsec
|
||||
```
|
||||
|
||||
4. Simula un ataque de prueba:
|
||||
```bash
|
||||
curl -H "User-Agent: Nikto" https://tu-dominio.com/
|
||||
docker exec crowdsec cscli decisions list
|
||||
```
|
||||
|
||||
### El dashboard pide login pero Authentik no redirige
|
||||
|
||||
1. Verifica que el middleware está correctamente referenciado en las labels:
|
||||
```bash
|
||||
docker inspect crowdsec-grafana | grep -A 20 Labels
|
||||
```
|
||||
|
||||
2. Revisa que la aplicación existe en Authentik y está asociada al Embedded Outpost
|
||||
3. Comprueba los logs de Authentik:
|
||||
```bash
|
||||
docker logs ths-authentik-server
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 💾 Backups
|
||||
|
||||
Incluye en tus backups de Kopia (ya configurados en `backups/kopia/`):
|
||||
|
||||
```bash
|
||||
# Datos de CrowdSec (decisiones, bouncers, config)
|
||||
/opt/crowdsec/data/
|
||||
|
||||
# Configuración personalizada
|
||||
/opt/crowdsec/config/
|
||||
|
||||
```
|
||||
|
||||
## 🔄 Actualizaciones
|
||||
|
||||
Para actualizar CrowdSec:
|
||||
|
||||
1. En Portainer, ve al stack `crowdsec`
|
||||
2. Edita `.env` y actualiza la versión si es necesario:
|
||||
```env
|
||||
CROWDSEC_IMAGE=crowdsecurity/crowdsec:latest
|
||||
GRAFANA_IMAGE=grafana/grafana:latest
|
||||
```
|
||||
3. Haz clic en **Update the stack**
|
||||
4. O desde CLI:
|
||||
```bash
|
||||
cd crowdsec
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> ⚠️ Antes de actualizar versiones mayores de CrowdSec, revisa el [changelog](https://github.com/crowdsecurity/crowdsec/releases).
|
||||
|
||||
## 📚 Recursos
|
||||
|
||||
- [Documentación oficial de CrowdSec](https://docs.crowdsec.net/)
|
||||
- [CrowdSec + Traefik Plugin](https://plugins.traefik.io/plugins/6335349fe93a526d1ec2aed6/crowdsec-bouncer)
|
||||
- [CrowdSec Hub (colecciones)](https://hub.crowdsec.net/)
|
||||
- [Grafana Docs](https://grafana.com/docs/grafana/latest/)
|
||||
- [CrowdSec Discord](https://discord.gg/crowdsec)
|
||||
@@ -0,0 +1,126 @@
|
||||
services:
|
||||
crowdsec:
|
||||
image: ${CROWDSEC_IMAGE}
|
||||
pull_policy: always
|
||||
container_name: crowdsec
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
environment:
|
||||
COLLECTIONS: ${CROWDSEC_COLLECTIONS}
|
||||
BOUNCER_KEY_TRAEFIK: ${CROWDSEC_BOUNCER_API_KEY}
|
||||
LEVEL: ${CROWDSEC_LOG_LEVEL}
|
||||
TZ: ${TZ:-America/Mexico_City}
|
||||
POSTGRES_HOST: crowdsec-postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DB: ${CROWDSEC_POSTGRES_DB}
|
||||
POSTGRES_USER: ${CROWDSEC_POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${CROWDSEC_POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- ${CROWDSEC_CONFIG_PATH}:/etc/crowdsec:Z
|
||||
- crowdsec_lib:/var/lib/crowdsec
|
||||
- ${TRAEFIK_LOG_PATH}:/var/log/traefik:ro
|
||||
- /var/log:/var/log/host:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
expose:
|
||||
- "8080"
|
||||
- "7422"
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
networks:
|
||||
- crowdsec_internal
|
||||
- proxy
|
||||
healthcheck:
|
||||
test: ["CMD", "cscli", "lapi", "status"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
crowdsec-postgres:
|
||||
image: ${CROWDSEC_POSTGRES_IMAGE}
|
||||
container_name: crowdsec-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${CROWDSEC_POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${CROWDSEC_POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${CROWDSEC_POSTGRES_DB}
|
||||
volumes:
|
||||
- ${CROWDSEC_POSTGRES_PATH}:/var/lib/postgresql/data:Z
|
||||
networks:
|
||||
- crowdsec_internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${CROWDSEC_POSTGRES_USER} -d ${CROWDSEC_POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
crowdsec-prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: crowdsec-prometheus
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=7d"
|
||||
- "--web.enable-lifecycle"
|
||||
volumes:
|
||||
- ${CROWDSEC_PROMETHEUS_CONFIG}/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ${CROWDSEC_PROMETHEUS_PATH}:/prometheus:Z
|
||||
networks:
|
||||
- crowdsec_internal
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/-/healthy"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
crowdsec-grafana:
|
||||
image: ${GRAFANA_IMAGE}
|
||||
container_name: crowdsec-grafana
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GF_SERVER_ROOT_URL: https://${CROWDSEC_GRAFANA_DOMAIN}
|
||||
GF_AUTH_PROXY_ENABLED: "true"
|
||||
GF_AUTH_PROXY_HEADER_NAME: X-Authentik-Username
|
||||
GF_AUTH_PROXY_HEADER_EMAIL: X-Authentik-Email
|
||||
GF_AUTH_PROXY_HEADER_GROUPS: X-Authentik-Groups
|
||||
GF_AUTH_PROXY_AUTO_SIGN_UP: "true"
|
||||
GF_AUTH_PROXY_ENABLE_LOGIN_TOKEN: "false"
|
||||
GF_AUTH_DISABLE_LOGIN_FORM: "true"
|
||||
GF_AUTH_SIGNOUT_REDIRECT: https://${AUTHENTIK_DOMAIN:-auth.sherlockhomeless.net}/application/o/${CROWDSEC_GRAFANA_SLUG:-grafana}/end-session/
|
||||
volumes:
|
||||
- ${CROWDSEC_GRAFANA_PATH}:/var/lib/grafana:Z
|
||||
- ${CROWDSEC_GRAFANA_PROVISIONING}:/etc/grafana/provisioning:ro
|
||||
- ${CROWDSEC_GRAFANA_DASHBOARDS}:/var/lib/grafana/dashboards:ro
|
||||
networks:
|
||||
- crowdsec_internal
|
||||
- proxy
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}"
|
||||
traefik.http.routers.crowdsec-grafana.rule: "Host(`${CROWDSEC_GRAFANA_DOMAIN}`)"
|
||||
traefik.http.routers.crowdsec-grafana.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
|
||||
traefik.http.routers.crowdsec-grafana.tls: "true"
|
||||
traefik.http.routers.crowdsec-grafana.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
|
||||
traefik.http.routers.crowdsec-grafana.service: "crowdsec-grafana"
|
||||
traefik.http.routers.crowdsec-grafana.middlewares: "crowdsec-bouncer@file,${TRAEFIK_AUTH_MIDDLEWARE}"
|
||||
traefik.http.services.crowdsec-grafana.loadbalancer.server.port: "3000"
|
||||
homepage.group: "Observabilidad"
|
||||
homepage.name: "Grafana"
|
||||
homepage.href: "https://${CROWDSEC_GRAFANA_DOMAIN}"
|
||||
homepage.description: "Dashboards CrowdSec y monitorizacion"
|
||||
homepage.icon: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg"
|
||||
homepage.siteMonitor: "https://${CROWDSEC_GRAFANA_DOMAIN}"
|
||||
|
||||
networks:
|
||||
crowdsec_internal:
|
||||
driver: bridge
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
crowdsec_lib:
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# crowdsec-metrics-export.sh
|
||||
# Exporta métricas de CrowdSec a PostgreSQL para Grafana.
|
||||
# Ejecutar cada 5 minutos via cron.
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
PG_DSN="postgresql://crowdsec:crowdsec_metrics_2026@crowdsec-postgres:5432/crowdsec"
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# 1. Decisiones activas (IPs bloqueadas)
|
||||
# -----------------------------------------------------------
|
||||
docker exec crowdsec cscli decisions list -o json 2>/dev/null | \
|
||||
python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for entry in data:
|
||||
for d in entry.get('decisions', []):
|
||||
ip = d.get('value','').replace(chr(39),chr(39)+chr(39))
|
||||
scenario = d.get('scenario','').replace(chr(39),chr(39)+chr(39))
|
||||
dtype = d.get('type','').replace(chr(39),chr(39)+chr(39))
|
||||
origin = d.get('origin','').replace(chr(39),chr(39)+chr(39))
|
||||
scope = d.get('scope','').replace(chr(39),chr(39)+chr(39))
|
||||
dur = d.get('duration','').replace(chr(39),chr(39)+chr(39))
|
||||
print(f'{ip}|{scenario}|{dtype}|{origin}|{scope}|{dur}')
|
||||
" 2>/dev/null | sort -u > /tmp/cs_decisions.txt
|
||||
|
||||
# Limpiar tabla y re-insertar
|
||||
docker exec crowdsec-postgres psql "$PG_DSN" -c "DELETE FROM decisions" 2>/dev/null
|
||||
|
||||
DECISION_COUNT=0
|
||||
while IFS='|' read -r ip reason action origin scope dur; do
|
||||
[ -z "$ip" ] && continue
|
||||
docker exec crowdsec-postgres psql "$PG_DSN" -c "
|
||||
INSERT INTO decisions (ip, reason, action, origin, scope, duration)
|
||||
VALUES ('$ip', '$reason', '$action', '$origin', '$scope', '$dur')
|
||||
" 2>/dev/null || true
|
||||
DECISION_COUNT=$((DECISION_COUNT + 1))
|
||||
done < /tmp/cs_decisions.txt
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# 2. Métricas agregadas
|
||||
# -----------------------------------------------------------
|
||||
ALERTS=$(docker exec crowdsec cscli alerts list -o json 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
|
||||
|
||||
docker exec crowdsec-postgres psql "$PG_DSN" -c "
|
||||
INSERT INTO metrics (metric_name, metric_value, labels) VALUES
|
||||
('active_decisions', $DECISION_COUNT, '{}'::jsonb),
|
||||
('active_alerts', $ALERTS, '{}'::jsonb)
|
||||
" 2>/dev/null || true
|
||||
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') crowdsec-metrics: $DECISION_COUNT decisions, $ALERTS alerts"
|
||||
|
||||
rm -f /tmp/cs_decisions.txt
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# CrowdSec Setup Script
|
||||
# ============================================================
|
||||
# Este script prepara los directorios, genera la clave del
|
||||
# bouncer y despliega la configuracion dinamica de Traefik.
|
||||
# ============================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||
|
||||
echo "=== CrowdSec Setup ==="
|
||||
|
||||
# 1. Cargar .env
|
||||
if [ ! -f "${ENV_FILE}" ]; then
|
||||
echo "ERROR: No se encontro ${ENV_FILE}"
|
||||
echo "Copia stack.env a .env y editalo:"
|
||||
echo " cp ${SCRIPT_DIR}/stack.env ${ENV_FILE}"
|
||||
echo " nano ${ENV_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
source "${ENV_FILE}"
|
||||
set +a
|
||||
|
||||
# 2. Generar clave del bouncer si no esta definida
|
||||
if [ "${CROWDSEC_BOUNCER_API_KEY}" = "change-me-to-a-random-64-char-hex" ] || [ -z "${CROWDSEC_BOUNCER_API_KEY}" ]; then
|
||||
CROWDSEC_BOUNCER_API_KEY=$(openssl rand -hex 32)
|
||||
echo "INFO: Generada nueva CROWDSEC_BOUNCER_API_KEY"
|
||||
sed -i "s/^CROWDSEC_BOUNCER_API_KEY=.*/CROWDSEC_BOUNCER_API_KEY=${CROWDSEC_BOUNCER_API_KEY}/" "${ENV_FILE}"
|
||||
fi
|
||||
|
||||
# 3. Crear directorios necesarios
|
||||
sudo mkdir -p "${CROWDSEC_CONFIG_PATH}"
|
||||
sudo mkdir -p "${CROWDSEC_DATA_PATH}"
|
||||
sudo mkdir -p "${TRAEFIK_LOG_PATH}"
|
||||
|
||||
# 4. Ajustar permisos para SELinux
|
||||
if command -v selinuxenabled &>/dev/null && selinuxenabled; then
|
||||
sudo chcon -Rt svirt_sandbox_file_t "${CROWDSEC_CONFIG_PATH}" 2>/dev/null || true
|
||||
sudo chcon -Rt svirt_sandbox_file_t "${CROWDSEC_DATA_PATH}" 2>/dev/null || true
|
||||
sudo chcon -Rt svirt_sandbox_file_t "${TRAEFIK_LOG_PATH}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 5. Generar Traefik dynamic config con la key del bouncer
|
||||
TRAEFIK_DYNAMIC_DIR="/opt/traefik/dynamic"
|
||||
TEMPLATE="${SCRIPT_DIR}/traefik-crowdsec-bouncer.yml"
|
||||
OUTPUT="${TRAEFIK_DYNAMIC_DIR}/crowdsec-bouncer.yml"
|
||||
|
||||
if [ -f "${TEMPLATE}" ]; then
|
||||
sudo mkdir -p "${TRAEFIK_DYNAMIC_DIR}"
|
||||
sed "s/{{CROWDSEC_BOUNCER_API_KEY}}/${CROWDSEC_BOUNCER_API_KEY}/g" "${TEMPLATE}" | sudo tee "${OUTPUT}" > /dev/null
|
||||
echo "INFO: Traefik dynamic config generado en ${OUTPUT}"
|
||||
else
|
||||
echo "WARN: No se encontro ${TEMPLATE}, la config dinamica de Traefik debe crearse manualmente"
|
||||
fi
|
||||
|
||||
# 6. Generar acquis.yaml para CrowdSec (lee logs de Traefik)
|
||||
ACQUIS_FILE="${CROWDSEC_CONFIG_PATH}/acquis.yaml"
|
||||
if [ ! -f "${ACQUIS_FILE}" ]; then
|
||||
sudo tee "${ACQUIS_FILE}" > /dev/null <<EOF
|
||||
---
|
||||
source: file
|
||||
filenames:
|
||||
- /var/log/traefik/access.log
|
||||
labels:
|
||||
type: traefik
|
||||
EOF
|
||||
echo "INFO: acquis.yaml generado en ${ACQUIS_FILE}"
|
||||
fi
|
||||
|
||||
# 7. Generar acquis.yaml para Docker logs
|
||||
ACQUIS_DOCKER="${CROWDSEC_CONFIG_PATH}/acquis-docker.yaml"
|
||||
if [ ! -f "${ACQUIS_DOCKER}" ]; then
|
||||
sudo tee "${ACQUIS_DOCKER}" > /dev/null <<EOF
|
||||
---
|
||||
source: docker
|
||||
EOF
|
||||
echo "INFO: acquis-docker.yaml generado para logs de contenedores"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "=== Setup completado ==="
|
||||
echo ""
|
||||
echo "Siguientes pasos:"
|
||||
echo " 1. (Re)desplegar Traefik para activar los plugins y access logs"
|
||||
echo " 2. Desplegar CrowdSec: cd ${SCRIPT_DIR} && docker compose up -d"
|
||||
echo " 3. Verificar: docker logs crowdsec"
|
||||
echo " 4. El middleware se usa asi: crowdsec-bouncer@file"
|
||||
echo ""
|
||||
echo "Para proteger un servicio, agrega el middleware en sus labels de Traefik:"
|
||||
echo ' traefik.http.routers.<svc>.middlewares: "crowdsec-bouncer@file"'
|
||||
echo ""
|
||||
echo "Dashboard Grafana (requiere Authentik): https://${CROWDSEC_GRAFANA_DOMAIN}"
|
||||
@@ -0,0 +1,34 @@
|
||||
##### Imágenes #####
|
||||
CROWDSEC_IMAGE=crowdsecurity/crowdsec:latest
|
||||
CROWDSEC_POSTGRES_IMAGE=docker.io/library/postgres:16-alpine
|
||||
GRAFANA_IMAGE=grafana/grafana:latest
|
||||
|
||||
##### CrowdSec Engine #####
|
||||
CROWDSEC_BOUNCER_API_KEY=change-me-to-a-random-64-char-hex
|
||||
CROWDSEC_FW_BOUNCER_KEY=change-me-to-another-random-64-char-hex
|
||||
CROWDSEC_LOG_LEVEL=info
|
||||
CROWDSEC_LOCAL_API_PORT=8080
|
||||
|
||||
##### Base de datos PostgreSQL (métricas para Grafana) #####
|
||||
CROWDSEC_POSTGRES_DB=crowdsec
|
||||
CROWDSEC_POSTGRES_USER=crowdsec
|
||||
CROWDSEC_POSTGRES_PASSWORD=change-me
|
||||
CROWDSEC_POSTGRES_PATH=/opt/crowdsec/postgres
|
||||
|
||||
##### Directorios de datos #####
|
||||
CROWDSEC_CONFIG_PATH=/opt/crowdsec/config
|
||||
CROWDSEC_DATA_PATH=/opt/crowdsec/data
|
||||
CROWDSEC_PROMETHEUS_CONFIG=/opt/crowdsec/prometheus
|
||||
CROWDSEC_PROMETHEUS_PATH=/opt/crowdsec/prometheus-data
|
||||
CROWDSEC_GRAFANA_PATH=/opt/crowdsec/grafana
|
||||
CROWDSEC_GRAFANA_PROVISIONING=/opt/crowdsec/grafana/provisioning
|
||||
CROWDSEC_GRAFANA_DASHBOARDS=/opt/crowdsec/grafana/dashboards
|
||||
TRAEFIK_LOG_PATH=/opt/traefik/logs
|
||||
|
||||
##### Traefik / dominios #####
|
||||
TRAEFIK_DOCKER_NETWORK=proxy
|
||||
CROWDSEC_GRAFANA_DOMAIN=grafana.sherlockhomeless.net
|
||||
TRAEFIK_ENTRYPOINT_SECURE=websecure
|
||||
TRAEFIK_CERTRESOLVER=letsencrypt
|
||||
TRAEFIK_AUTH_MIDDLEWARE=ths-authentik@docker
|
||||
TRAEFIK_CROWDSEC_MIDDLEWARE=crowdsec-bouncer@file
|
||||
@@ -0,0 +1,15 @@
|
||||
http:
|
||||
middlewares:
|
||||
crowdsec-bouncer:
|
||||
plugin:
|
||||
crowdsec-bouncer:
|
||||
enabled: true
|
||||
crowdsecLapiUrl: "http://crowdsec:8080"
|
||||
crowdsecLapiKey: "{{CROWDSEC_BOUNCER_API_KEY}}"
|
||||
crowdsecMode: live
|
||||
updateIntervalSeconds: "10"
|
||||
defaultDecisionSeconds: "60"
|
||||
httpTimeoutSeconds: "2"
|
||||
crowdsecLapiTLSInsecureVerify: false
|
||||
forwardedHeadersTrustedIPs:
|
||||
- "0.0.0.0/0"
|
||||
Reference in New Issue
Block a user