Traefik y crowdsec
Hasta hace poco tenía en funcionamiento traefik con crowdsec usando las plantillas de unraid. Hice varios manuales sobre la instalación pero por motivos que desconozco crowdsec no parseaba las líneas de logs de traefik y por tanto no hacía absolutamente nada.
He decicido hacer una instalación limpia desde 0 y aquí va.
Gran parte de este manual se ha hecho usando Perplexity. Debo decir que tiene mucho que mejorar, pero nos marca unas bases buenas para empezar.
Preparación del entorno
Lo primero que vamos a hacer es preparar nuestro entorno de directorios:
Directorios de traefik y crowdsec:
mkdir -p /mnt/user/appdata/traefik/{letsencrypt,log}
mkdir -p /mnt/user/appdata/crowdsec/data
Ficheros de configuración y logs de traefik:
touch /mnt/user/appdata/traefik/letsencrypt/acme.json
chmod 600 /mnt/user/appdata/traefik/letsencrypt/acme.json
touch /mnt/user/appdata/traefik/traefik.yml
touch /mnt/user/appdata/traefik/dynamic_conf.yml
touch /mnt/user/appdata/traefik/log/access.log
chmod 644 /mnt/user/appdata/traefik/log/access.log
Fichero de configuración de crowdsec. Dentro de la carpeta de configuración de crowdsec creamos el fichero acquis.yaml:
touch /mnt/user/appdata/crowdsec/config/acquis.yaml
Docker-compose
Sigo usando Unraid. Nos vamos a la pestaña compose y creamos nuestro stack de docker:
docker-compose.yml
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "1880:80"
- "18443:443"
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN} # API TOKEN DE CLOUDFLARE
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /mnt/user/appdata/traefik/letsencrypt/acme.json:/letsencrypt/acme.json
- /mnt/user/appdata/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- /mnt/user/appdata/traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro
- /mnt/user/appdata/traefik/log:/var/log/traefik # Volumen para persistencia de logs
networks:
- cloud
crowdsec:
image: crowdsecurity/crowdsec
container_name: crowdsec
restart: unless-stopped
environment:
- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve
volumes:
- /mnt/user/appdata/traefik/log:/var/log/traefik:ro # comparte los logs
- /mnt/user/appdata/crowdsec/data:/var/lib/crowdsec/data
- /mnt/user/appdata/crowdsec/config:/etc/crowdsec
networks:
- cloud
networks:
cloud:
external: true
Fichero .env
CF_DNS_API_TOKEN=MI_API_KEY_CLOUDFLARE. # Se obtiene en el panel de cloudflare
CROWDSEC_BOUNCER_API_KEY= xxxxxxxx # Con la última versión del docker-compose se puede borrar
DOMAIN_NAME=mi_dominio.com # Con la última versión del docker-compose se puede borrar
EMAIL=mi_correo@hotmail.com # Con la última versión del docker-compose se puede borrar
Nota Importante: ES IMPRESCINDIBLE QUE LOS SERVICIOS QUE USEMOS CON TRAEFIK ESTÉN EN LA MISMA RED DOCKER. En mi caso se llama cloud
Ficheros de configuración
Traefik - traefik.yml
api:
dashboard: false # Cuando necesito revisar el dashboard lo cambio a true y reinicio el contenedor. Mientras no sea necesario lo dejo en false.
providers:
docker:
exposedByDefault: false
network: cloud # Debe coincidir con el nombre de tu red externa en docker-compose
file:
fileName: /etc/traefik/dynamic_conf.yml
watch: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
transport:
respondingTimeouts:
idleTimeout: 3600
# 1. HABILITAR LOGS (Vital para que CrowdSec detecte ataques)
accessLog:
filePath: "/var/log/traefik/access.log"
format: json
bufferingSize: 100
fields:
headers:
defaultMode: keep # Mantiene headers para que CrowdSec vea IPs reales
# 2. PLUGINS (CrowdSec Bouncer para Traefik v3)
#
# AQUI AÑADIREMOS EL PLUGIN DE CROWDSEC
#
# 3. CONFIGURACIÓN DE CERTIFICADOS (Cloudflare DNS Challenge)
certificatesResolvers:
cloudflare:
acme:
email: mi_correo@hotmail.com
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
Traefik - dynamic_conf.yml
http:
routers:
dashboard:
rule: "Host(`traefik.mi_dominio.com`)"
service: api@internal
entryPoints:
- websecure
tls:
certResolver: cloudflare
middlewares:
- auth
- security-headers
middlewares:
auth:
basicAuth:
# Generar con: echo $(htpasswd -nB usuario) | sed -e s/\\$/\\$\\$/g
users:
- "noah:$mi_pass_hasheada"
security-headers:
headers:
stsSeconds: 15552000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
frameDeny: true # Previene Clickjacking
contentTypeNosniff: true # Previene sniffing de MIME
browserXssFilter: true
referrerPolicy: "same-origin"
# Ajuste para Nextcloud:
customFrameOptionsValue: "SAMEORIGIN"
#
# AQUI AÑADIREMOS EL MIDDLEWARE DE CROWDSEC
#
Crowdsec - acquis.yaml
---
filenames:
- /var/log/traefik/access.log
poll_without_inotify: true
labels:
type: traefik
Integración del bouncer en traefik
Iniciamos nuestro compose y no debería lanzar errores.
Es momento de crear nuestra API key del bouncer crowdsec:
---
docker exec -it crowdsec cscli bouncers add traefik-bouncer
# te devolverá una API key, guárdala
Verificamos que nuestras colecciones estén actualizadas:
docker exec -it crowdsec cscli hub update
docker exec -it crowdsec cscli hub upgrade
docker restart crowdsec
Modificamos nuestros ficheros traefik.yml y dynamic_conf.yml para añadir lo siguente:
Traefik - traefik.yml
experimental:
plugins:
crowdsec-bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.3.5
Traefik - dynamic_conf.yml
middlewares:
[...AQUI TENEMOS NUESTROS OTROS MIDDLEWARES......]
crowdsec-bouncer:
plugin:
crowdsec-bouncer:
Enabled: true
CrowdsecMode: live # o streaming si prefieres
CrowdsecLapiUrl: "http://crowdsec:8080"
CrowdsecLapiKey: "ESTA KEY SE GENERA MAS ADELANTE"
ForwardedHeadersCustomName: "X-Forwarded-For"
# Obtenemos la lista oficial de IPs con el siguiente comando:
# curl https://www.cloudflare.com/ips-v4 -o cloudflare-ips-v4.txt
ForwardedHeadersTrustedIps:
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "108.162.192.0/18"
- "131.0.72.0/22"
- "141.101.64.0/18"
- "162.158.0.0/15"
- "172.64.0.0/13"
- "173.245.48.0/20"
- "188.114.96.0/20"
- "190.93.240.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
Ficheros completos:
Traefik - traefik.yml
api:
dashboard: false # Cuando necesito revisar el dashboard lo cambio a true y reinicio el contenedor. Mientras no sea necesario lo dejo en false.
providers:
docker:
exposedByDefault: false
network: cloud # Debe coincidir con el nombre de tu red externa en docker-compose
file:
fileName: /etc/traefik/dynamic_conf.yml
watch: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
transport:
respondingTimeouts:
idleTimeout: 3600
# 1. HABILITAR LOGS (Vital para que CrowdSec detecte ataques)
accessLog:
filePath: "/var/log/traefik/access.log"
format: json
bufferingSize: 100
fields:
headers:
defaultMode: keep # Mantiene headers para que CrowdSec vea IPs reales
# 2. PLUGINS (CrowdSec Bouncer para Traefik v3)
experimental:
plugins:
crowdsec-bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.3.5
# 3. CONFIGURACIÓN DE CERTIFICADOS (Cloudflare DNS Challenge)
certificatesResolvers:
cloudflare:
acme:
email: mi_correo@hotmail.com
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
Traefik - dynamic_conf.yml
http:
routers:
dashboard:
rule: "Host(`traefik.mi_dominio.com`)"
service: api@internal
entryPoints:
- websecure
tls:
certResolver: cloudflare
middlewares:
- auth
- security-headers
- crowdsec-bouncer # Protegemos el dashboard con crowdsec
middlewares:
auth:
basicAuth:
# Generar con: echo $(htpasswd -nB usuario) | sed -e s/\\$/\\$\\$/g
users:
- "noah:$mi_pass_hasheada"
security-headers:
headers:
stsSeconds: 15552000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
frameDeny: true # Previene Clickjacking
contentTypeNosniff: true # Previene sniffing de MIME
browserXssFilter: true
referrerPolicy: "same-origin"
# Ajuste para Nextcloud:
customFrameOptionsValue: "SAMEORIGIN"
crowdsec-bouncer:
plugin:
crowdsec-bouncer:
Enabled: true
CrowdsecMode: live # o streaming si prefieres
CrowdsecLapiUrl: "http://crowdsec:8080"
CrowdsecLapiKey: "zEtyY2gJsQgQca03vEvWWwcowSG8f9yJz84nF95qZq4"
ForwardedHeadersCustomName: "X-Forwarded-For"
ForwardedHeadersTrustedIps:
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "108.162.192.0/18"
- "131.0.72.0/22"
- "141.101.64.0/18"
- "162.158.0.0/15"
- "172.64.0.0/13"
- "173.245.48.0/20"
- "188.114.96.0/20"
- "190.93.240.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
Obtener las IPs confiables de cloudflare
Para configurar ForwardedHeadersTrustedIps con precisión en el middleware de CrowdSec, se usan las IPs de Cloudflare. Esto evita que IPs falsas se usen para evadir bloqueos.
Lista oficial de IPs de Cloudflare.
Cloudflare publica sus rangos IPv4/IPv6 en dos archivos JSON. Los podemos obetner así
# Desde la terminal de Unraid (donde corre Docker)
curl https://www.cloudflare.com/ips-v4 -o cloudflare-ips-v4.txt
curl https://www.cloudflare.com/ips-v6 -o cloudflare-ips-v6.txt
Las IPs que obtenemos las añadimos al fichero dynamic_conf.yml, apartado ForwardedHeadersTrustedIps del middleware crowdsec-bouncer.
Puesta en marcha y comprobación
Con todo esto reiniciamos el stack de docker y debería arrancar funcionando sin errores. Revisaremos los logs para verificar fallos.
docker compose down
docker compose up -d
# En Unraid lo hacemos de forma gráfica
docker logs traefik | grep -i crowdsec
docker logs crowdsec
docker exec crowdsec cscli decisions list
Comandos intersantes de crowdsec:
# Listado de decisiones tomadas
docker exec -it crowdsec cscli decisions list
# Métricas completas de crowdsec
docker exec -it crowdsec cscli metrics
# Unban IP
docker exec -it crowdsec cscli decisions delete -i x.x.x.x
# Banear una IP
docker exec -it crowdsec cscli decisions add --ip xx.xx.xx.xx --duration 1h
Enrutar servicios a través de traefik
Las etiquetas básicas que debe llevar cualquier servicio para que traefik lo enrute son las siguientes:
# Ejemplo para nuestro karakeep
traefik.enable=true
traefik.http.routers.karakeep.rule=Host(`karakeep.mi_dominio.com`)
traefik.http.routers.karakeep.entrypoints=websecure
traefik.http.routers.karakeep.tls.certresolver=cloudflare
traefik.http.services.karakeep.loadbalancer.server.port=3000
# middelware que vamos a aplicar a este servcio
traefik.http.routers.karakeep.middlewares=crowdsec-bouncer@file,security-headers@file
IMPORTANTE: La etiqueta traefik.http.services.karakeep.loadbalancer.server.port debe indicar el puerto interno del contenedor, ya que tenemos que recordar que nuestros servicios siempre deben estar en la misma red que traefik para que funcionen.
En mi caso, el docker-compose de karakeep lleva la siguiente configuración de puertos porque el puerto 3000 lo estoy usando para otro servicio:
[....]
ports:
- 3333:3000
[....]
Entonces, el acceso a karakeep a través de la red local es:
http://192.168.10.55:3333
Pero traefik sólo entiende de la red docker, por lo que el puerto para traefik es 3000.
Protección de servicios con crowdsec
Hemos configurado crowdsec como un middleware, por tanto, la solicitud de conexión al servicio debe pasar antes por crowdsec que dirá si permite esa conexión o no.
Para proteger servicios tenemos dos opciones:
1.- Configuración estática en dynamic_conf.yml
2.- Etiquetas traefik en el servicio en cuestión.
Por facilidad vamos a hacer la segunda opción, a través de etiquetas, así mantenemos más limpio el dynamic_conf.yml y traefik hace una carga en vivo sin reiniciar el proxy.
Etiqueta:
# Ejemplo para Nextcloud. El resto de servicios sería exactamente igual
traefik.http.routers.nextcloud.middlewares=rowdsec-bouncer@file,security-headers@file
Protección de Nextcloud
Crowdsec tiene una colección específica para Nextcloud. Vamos a proteger nuestra instancia.
Modificamos el docker-compose añadiendo la colección de crowdsecurity/nextcloud y montando la carpeta donde almacenamos los logs de Nextcloud.
crowdsec:
image: crowdsecurity/crowdsec
container_name: crowdsec
restart: unless-stopped
environment:
- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/nextcloud
volumes:
- /mnt/user/appdata/traefik/log:/var/log/traefik:ro # comparte los logs
- /mnt/user/appdata/nextcloud/config/nextcloud-logs:/var/log/nextcloud:ro # comparte los logs
Reiniciamos crowdsec y podemos actualizar las colletions:
docker exec -it crowdsec cscli hub update
Revisamos nuestro config.php de Nextcloud para verificar que los logs se guardan en el lugar correcto:
'mail_smtpport' => '465',
'bulkupload.enabled' => false,
'loglevel' => 2,
'logfile' => '/var/www/html/config/nextcloud-logs/nextcloud.log',
'log_rotate_size = 0',
Modificamos nuestro fichero acquis.yaml de crowdsec, quedano así:
# Logs de Traefik (para todo lo que pasa por el proxy)
---
filenames:
- /var/log/traefik/access.log
poll_without_inotify: true
labels:
type: traefik
# Logs de Nextcloud (brute-force y eventos específicos NC)
---
filenames:
- /var/log/nextcloud/nextcloud.log
labels:
type: nextcloud
Reiniciamos y verificamos si se están parseando los dos ficheros de logs que tenemos:
docker restart crowdsec
docker exec crowdsec cscli metrics # Verifica parsing

Con esta configuración Nextcloud registraba la dirección IP de Cloudflare, no la IP real del usuario que accede a Nextcloud. Vamos a arreglarlo.
Problema clásico Cloudflare + Traefik + Nextcloud: Nextcloud ve IP de Cloudflare porque no confía en Traefik como proxy ni lee el header CF-Connecting-IP (IP real que pasa Cloudflare).
Editamos nuestro fichero config.php de Nextcloud:
[.......]
'dbtype' => 'mysql',
'version' => '32.0.5.0',
'trusted_proxies' =>
array (
0 => '192.168.10.55',
// añadida rango IP de nuestra red "cloud" de docker:
1 => '172.21.0.0/16',
// añadidas IPs de clouflare para que Nextcloud confíe en ellas:
2 => '103.21.244.0/22',
3 => '103.22.200.0/22',
4 => '103.31.4.0/22',
5 => '104.16.0.0/13',
6 => '104.24.0.0/14',
7 => '108.162.192.0/18',
8 => '131.0.72.0/22',
9 => '141.101.64.0/18',
10 => '162.158.0.0/15',
11 => '172.64.0.0/13',
12 => '173.245.48.0/20',
13 => '188.114.96.0/20',
14 => '190.93.240.0/20',
15 => '197.234.240.0/22',
16 => '198.41.128.0/17',
),
// añadida según info de perplexity para que crowdsec funcione bien.
'forwarded_for_headers' =>
array (
'HTTP_CF_CONNECTING_IP', // ← IP real de Cloudflare
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR',
),
'overwrite.cli.url' => 'https://nextcloud.mi_dominio.com',
'overwritehost' => 'nextcloud.mi_dominio.com',
[......]
Y añadimos las siguientes etiquetas al docker-compose de Nextcloud:
# Esta primera la editamos como sigue:
traefik.http.routers.nextcloud.middlewares=crowdsec-bouncer@file,security-headers@file,nextcloud-headers@docker
# Estas se añaden nuevas:
traefik.http.middlewares.nextcloud-headers.headers.customrequestheaders.X-Forwarded-Proto=https
traefik.http.middlewares.nextcloud-headers.headers.customrequestheaders.X-Forwarded-For=true
traefik.http.middlewares.nextcloud-headers.headers.customrequestheaders.X-Forwarded-Port=443
traefik.http.middlewares.nextcloud-headers.headers.customrequestheaders.X-Real-IP={{.ClientIP }}
Y con esto nuestro Nextcloud ya detecta la IP verdadera para, si procede, banearla.
Conexión de nuestro contenedor a Crowdsec.net
Accedemos a nuestra cuenta en https://app.crowdsec.net/.
En el dashboard → Security Engines → Add Instance (o “Engines”)
Copia el Enrollment Token que genera (algo como cscli console enroll abc123…def456)
# Registrar con el token
docker exec -it crowdsec cscli console enroll TU_ENROLLMENT_TOKEN_AQUI
Esto creará /etc/crowdsec/online_api_credentials.yaml con login y password válidos.
Reiniciamos nuestro contenedor:
docker restart crowdsec
docker logs crowdsec | grep -i capi
Volvemos al dashboard de crowdsec.net y aceptamos el enrolado del equipo. Podemos cambiar el nombre para ser más visual al acceder. En mi caso le puse Unraid.
Ventajas de conectar a Console:
1.- Dashboard web con alertas en tiempo real
2.- Blocklist comunitaria (millones de IPs malas)
3.- Métricas globales y threat intel
**En mi caso, al final no hice el enrolado del equipo por los siguientes motivos:
Notificaciones en Telegram de Crowdsec
Para añadir notificaciones realizamos la siguiente configuración:
IMPORTANTE: DETENEMOS EL COMPOSE
Fichero /mnt/user/appdata/crowdsec/config/notifications/http.yaml
type: http # No cambiar
name: telegram # Nombre que usaremos en el perfil
log_level: info
# URL de la API de Telegram con tu TOKEN
# Mucho OJO en la url - Está bien escrita
url: https://api.telegram.org/botXXXXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXXXX/sendMessage
method: POST
headers:
Content-Type: application/json
# Formato del mensaje (JSON)
# Formato del mensaje (JSON)
format: |
{
"chat_id": "-5002897396",
"text": "{{range .}}{{$alert := .}}{{range .Decisions}}IP {{.Value}} baneada {{.Duration}} por {{.Scenario}}{{end}}{{end}}",
"parse_mode": "HTML"
}
Fichero /mnt/user/appdata/crowdsec/config/profiles.yaml
name: default_ip_remediation
#debug: true
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
notifications:
- telegram
Probamos a enviar una notificación de prueba:
docker exec -it crowdsec cscli notifications test telegram
Versión tuneada de notificaciones en Telegram. Tomando como ejemplo el modelo de la documentación de crowdsec.net:
type: http # Don't change
name: telegram # Must match the registered plugin in the profile
# One of "trace", "debug", "info", "warn", "error", "off"
log_level: info
format: |
{
"chat_id": "-5002897396",
"text": "
{{range . -}}
{{$alert := . -}}
{{range .Decisions -}}
🚨 CrowdSec Alert on MyServer! 🚨
🆔 IP: {{.Value}}
⚠️ Scenario: {{ .Scenario }}
🚧 Decision: {{.Type}} for next {{.Duration}}
{{end -}}
{{end -}}
",
"reply_markup": {
"inline_keyboard": [
{{ $arrLength := len . -}}
{{ range $i, $value := . -}}
{{ $V := $value.Source.Value -}}
[
{
"text": "See {{ $V }} on shodan.io",
"url": "https://www.shodan.io/host/{{ $V -}}"
},
{
"text": "See {{ $V }} on crowdsec.net",
"url": "https://app.crowdsec.net/cti/{{ $V -}}"
}
]{{if lt $i ( sub $arrLength 1) }},{{end }}
{{end -}}
]
}
url: https://api.telegram.org/botXXXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/sendMessage
method: POST
headers:
Content-Type: "application/json"
Fuentes y enlaces de interés que ayudaran a complementar esta guía: