INV-001 — CrowdSec como capa reactiva del edge (Q3 del ADR-0019)¶
Question¶
¿Activa Wetopi CrowdSec (Security Engine + LAPI in-cluster, alimentando el
set nft wetopi_fwedge existente) como capa reactiva del edge? El ADR-0019
§3 la propuso y la dejó abierta: el fw-edge actual solo aplica blocklists
estáticas (reacción de minutos-horas); CrowdSec añadiría detección de
ataques EN la plataforma con reacción de segundos. La pregunta no es técnica
"¿funciona?" sino de decisión: ¿el coste (componentes, tuning, superficie de
falsos positivos multi-tenant) justifica cerrar el hueco entre "detección
pasiva" (Suricata/Tetragon observan) y "bloqueo reactivo" (hoy: manual)?
Method¶
Estudio de escritorio (sin deploys, sin manifests): (1) lectura del contrato
CON-CROWDSEC y del ADR-0019 §3-Q3; (2) lectura del código real del
fw-edge-controller (platform-security/controller/app.py, API v1) para
fundamentar el contrato de integración propuesto; (3) lectura del pipeline
de access logs traefik (E-OBS.1: JSON en stdout del DS hostNetwork, headers
dropped, ingest a access-traefik-*) — exactamente el input que el Engine
parsería; (4) research upstream: release 1.7.8 (2026-05-27) con
CVE-2026-44981 (LAPI DoS, high) y CVE-2026-44982 (WAF bypass, high) — el
pin v1.7.1 del ADR quedó VIEJO; (5) estimación de coste sobre el flujo real
(~100k req/día agregado; pico estimado <5 req/s sostenido) contra perfiles
conocidos de despliegues comparables; (6) análisis de riesgo de falsos
positivos en hosting multi-tenant con IP de salida compartida.
Evidence (ART-*)¶
No se generaron ART-* (estudio de escritorio sin probes). Fuentes citadas
en línea: CON-CROWDSEC (concepto, comparativa con fw-edge),
platform-security/controller/app.py (superficie API v1: ipsets /
revisions / subscribers / stream SSE), platform-edge/kubernetes/edge/
helmchartconfig-rke2-traefik-edge.yaml (access log JSON stdout, campos
RequestAddr/DownstreamStatus/RouterName conservados; headers DROP),
ADR-0019 (Q3 abierta, pin CrowdSec v1.7.1), advisory upstream 1.7.8
(CVE-2026-44981 LAPI DoS + CVE-2026-44982 WAF bypass, ambos HIGH,
2026-05-27), hub CrowdSec (colecciones crowdsecurity/wordpress:
http-bf-wordpress leakspeed 10s/capacity 5; http-wordpress-scan;
bruteforce genérica; http-crawl-non_statics).
Findings¶
F1. Coste: el flujo es pequeño para el Engine; el pin v1.7.1 es el riesgo real¶
- Input: el Engine parsea el JSON de access log del traefik que YA
producimos (flags
--accesslog=true --accesslog.format=jsonen el HelmChartConfig del edge, transportado por CRI stdout). ~100k req/día agregados ≈ 1.2 req/s de media, picos < 50 req/s. Los benchmarks y la práctica común de la comunidad sitúan el Engine en decenas-hundreds de EPS por núcleo para parsing JSON de access log: el coste de parsing es despreciable (fracción de 1 vCPU). - LAPI (Local API): es el componente más pesado (SQLite, retención de decisiones, servido de decisiones a bouncers + consola). Con nuestro volumen de alertas (decenas-día tras el parsing, no miles: los scenarios agregan por IP+behavior) una réplica única está sobrada con 250-512 MiB RAM / 0.2 vCPU. La retención por defecto de decisiones (4-5 h para la mayoría de scenarios) mantiene la DB en decenas de MB.
- Superficie añadida: 1 Deployment (Engine+LAPI en el mismo pod o separados; recomendado UN pod para day-one), 1 PVC pequeño (SQLite), y NO un firewall nuevo (el bouncer alimenta el set existente). El pin del ADR (v1.7.1) es anterior a 1.7.8, que arregla dos CVEs HIGH (CVE-2026-44981 DoS al LAPI — relevante si el Engine es alcanzable por tráfico no confiable; CVE-2026-44982 bypass del WAF AppSec). Si se activa, el pin mínimo aceptable es 1.7.8+, no 1.7.1. El bouncer traefik (maxlerebourg/crowdsec-bouncer-traefik v1.7.1 referenciado en el ADR) es una pieza aparte y NO se usa en la opción recomendada (ver F3).
- Retención de decisiones: por defecto, las decisiones del Engine
expiran solas (ban durations típicas 1-4 h con
decisions_expire). Persistencia LAPI: purga automática (api.server.gc) — sin crecimento ilimitado del PVC. Dimensionar 5 GiB es holgado para años de decisiones.
F2. Scenarios WordPress: lista inicial recomendada, umbrales POR SITIO¶
Set day-one (colecciones del hub, parseables con el parser traefik JSON +
parser crowdsecurity/http-logs):
| Scenario (hub) | Qué detecta | Umbrales por defecto | Riesgo FP en multi-tenant |
|---|---|---|---|
crowdsecurity/http-bf-wordpress |
brute-force en wp-login.php | capacity 5 / leakspeed 10s / ban 4h | MEDIO: un plugin de marketing con muchos logins legítimos desde 1 IP (p.ej. pasarela de pagos que re-loggea) puede trippearlo |
crowdsecurity/http-wordpress-scan |
probing de wp-config.php, /wp-content/plugins/ enumeration, etc. | ban 4h | BAJO: los scanners legítimos de un cliente (pentest propio) DEBEN ir a una allowlist del sitio, no global |
crowdsecurity/http-crawl-non_statics |
crawlers agresivos sobre endpoints no estáticos | leakspeed 0.5s / capacity 10 | ALTO: crawlers legítimos de clientes (SEO, price-comparison) comparten la IP de egress del NAT-GW si el crawler corre EN la plataforma — ver riesgo estructural abajo |
crowdsecurity/http-sensitive-path (genérica) |
probes de /.env, /.git/, /vendor/phpunit |
ban 4h | NULO: no hay uso legítimo de estos paths en el edge público |
crowdsecurity/http-bf-generic |
ráfagas 401/403/404 genéricas | ban 4h | MEDIO: APIs de clientes con auth failures legítimas (webhooks mal configurados) |
Umbrales por SITIO, no globales (el modelo de modos del WAF —
wetopi.io/waf-mode: off|detect|block — es el patrón a replicar):
Cada wepod declara en SU namespace un ConfigMap de overrides del Engine
o, en v0, se resuelve con un scope: site_id en las alertas (el Engine
etiqueta cada evento con el RouterName del access log → site_id) y
UMBRALES por sitio vía http-logs parsers específicos. El override
concreto: los umbrales capacity/leakspeed de cada scenario son
configurables por whitelist/simulation Y por parser-override del sitio
(patrón: un ConfigMap crowdsec-scenarios por namespace, montado en el
Engine; el GitOps lo lleva el repo de la plataforma igual que las reglas
de Suricata — cf. el ConfigMap suricata-rules).
Riesgo estructural multi-tenant (el más serio de la lista): si un
crawler legítimo del cliente A (corriendo en un wepod, saliendo por el
NAT-GW SNAT 92.222.245.83) golpea el wp-login del cliente B, y B ha
afinado su threshold bajo, el ban recae sobre la IP del NAT-GW: bloquea
el egress de TODOS los wepods hacia B. Mitigaciones, en orden de
preferencia:
1. Nunca banear rangos internos/Wetopi-owned: whitelist explícita en
el Engine de los CIDRs de los nodos y del pool del NAT-GW (el Engine
no puede distinguir el cliente real detrás del SNAT — el remediation
component tampoco).
2. Baneo por IP+scope, no por IP global: las decisiones CrowdSec
soportan scope: ip (default) vs range vs custom. Para el day-one
usar SOLO scope: ip y bans cortos (≤1h) — un falso positivo expira
rápido y solo afecta a UN destino (B), no a la plataforma.
3. Los crawlers que corren dentro de la plataforma deben tener egress
class propio (wordpress-restricted/custom del ADR-0019 §3) —
su tráfico al edge de otros sitios NO debería existir (egress netpol
ya lo restringe); la detección es segunda línea, no primera.
F3. Contrato de integración con fw-edge: ruta dynamic-blocks en la API del controller¶
Decisión estructural que el ADR ya fija: las revisiones fw-rev-N son
inmutables y viajan por artifact store (GitOps-adjacent); el estado
dinámico de incidentes va por API del backend, NUNCA por GitOps
(ADR-0019 §2 D-fw-edge: "el estado dinámico de incidentes va por API del
backend"). CrowdSec es 100% estado dinámico → su integración es una ruta
NUEVA en el controller, separada de /api/v1/revisions.
Propuesta concreta (encaja en platform-security/controller/app.py
sin tocar el Store existente):
POST /api/v1/dynamic-blocks/{ipset-name} # upsert de entradas dinámicas
body: { entries: [{ip, reason, source, expires_at}], ttl_hint_s: int }
- source: "crowdsec" (única admitida day-one; valida con un enum)
- expires_at: el controller purga la entrada al expirar (NO el agent)
- el ipset DEBE existir como ipset Git-managed (el controller NO crea
ipsets dinámicos — el día-uno solo acepta el ipset reservado
"dynamic-blocks" que el GitOps declara vacío)
GET /api/v1/dynamic-blocks/{ipset-name} # estado (para el agent y para drift)
DELETE /api/v1/dynamic-blocks/{ipset-name}/{ip} # unban manual (audit trail)
POST /api/v1/dynamic-blocks/{ipset-name}/purge # operator: vaciar (rollback de FP en masa)
El flujo: Engine decide → bouncer-bridge (un servicio nuevo, NO el
bouncer traefik) llama POST /api/v1/dynamic-blocks/dynamic-blocks con la
decisión → el controller la acopia en un ipset reservado dynamic-blocks
que convive en la tabla wetopi_fwedge con las listas duras → los agents
aplican en su poll ≤60s (mismo mecanismo que ya tienen, con el mismo
sha256-verified pull). La tabla nft final es una UNIÓN de: (a) blocklists
estáticas de la revisión inmutable fw-rev-N, (b) el ipset dinámico
dynamic-blocks servido por la API. El agent NO distingue: el
artifacts contract sigue igual (el ipset dynamic-blocks entra en la
snapshot del revision publish, con la única salvedad de que su contenido
cambia con frecuencia).
TTL/decay: la decisión del Engine ya lleva su duración (4h por defecto
en los scenarios wordpress); el expires_at de la entrada dinámica lo
copia del Engine. El controller purga al expirar (lazy: en el próximo
GET/publish) + un GC periódico (cron del propio controller, cada 5 min).
NO se propone un decay progresivo (más maquinaria, no hay caso de uso:
los bans de CrowdSec ya son por duración fija).
Coexistencia con el set wetopi_fwedge: el ipset dynamic-blocks es
UNA entrada más de la tabla inet wetopi_fwedge (contrato
REC-20260909-FW-COEXIST intacto: tabla exclusiva, nunca flush ruleset).
Las listas duras del GitOps y el dinámico NO compiten: son entradas
diferentes del mismo set que el agent ya materializa. El orden de
evaluación nft (prerouting blocklist ANTES del REDIRECT de edge_lb) no
cambia.
Descarte del bouncer traefik plugin (v1.7.1 del ADR): un plugin en el DS hostNetwork del traefik es la superficie que ya nos quemó con el WAF Coraza (incidente 2026-09-12: HelmChartConfig del DS = caída del edge). Alimentar el set nft vía el controller es estrictamente más robusto (el plugin muere → el ban no se aplica, pero el edge sigue; el controller muere → last-known-good persiste en el kernel). El ADR ya listaba la opción "alimentación directa del set nft" como preferible.
F4. Reporte a la red global: opt-in, y QUÉ señal saldría¶
- El opt-in es binario y barato de desactivar (flag
api.clientdel Engine — si no está enrolado, el Engine opera 100% local). - Qué saldría si se activa: hashes de las IPs detectadas por nuestros scenarios (brute-force wp-login, scanners) + metadatos (scenario id, timestamps) — anonimizado (no URL, no dominios, no site_id, no payloads). Es la señal que la red devuelve en forma de blocklist comunitaria.
- Qué NO saldría: el contenido de los requests (el access log headers ya lo prohíbe — GDPR, E-OBS.1), dominios de clientes, volúmenes por sitio, topología (site_id es un campo NUESTRO).
- Riesgo real de activarlo day-one: el Engine (con red activada) es
una salida HTTPS desde el pod LAPI → rompe el modelo egress
"WordPress restringido" para ESTE pod, que además maneja datos de
seguridad de la plataforma. Implicaciones: (a) netpol egress propia
(solo
api.crowdsec.net:443— FQDN allowlist), (b) el pod se convierte en un token de reputación NUESTRO (si un FP masivo de nuestros scenarios sale reportado, afecta a la confianza de la señal, no a nuestro tráfico), (c) el valor de la señal global es MAYOR que el costo — miles de servidores WordPress comparten atacantes, y la blocklist comunitaria filtra IPs que las feeds estáticas no tienen (las IPs de brute-force rotan demasiado rápido para firehol level1). - Recomendación F4: activar el reporte en la FASE 2, no en day-one. La fase 1 (Engine local, sin red) ya produce el 80% del valor (reacción a ataques EN la plataforma); la señal global es un multiplicador que conviene activar tras validar los umbrales (no reportar falsos positivos propios a la red).
Conclusion (VEREDICTO)¶
ACTIVAR CON RECORTES (recomendación de la Q3, lista para decisión del operador):
- Sí al Engine + LAPI in-cluster (1 Deployment, 0.5 vCPU / 512 MiB request, PVC 5 GiB para SQLite), parseando el access log JSON del traefik que ya produce E-OBS.1. Pin v1.7.8+ (NO el v1.7.1 del ADR — CVE-2026-44981/44982).
- Sí al set inicial de 5 scenarios (F2) con umbrales por sitio
(ConfigMap por namespace) y ban scope=ip, duración ≤4h, whitelists
estructurales de IPs internas/NAT-GW. Los crawlers (F2, riesgo alto)
entran en fase 2 tras una semana de simulación (
simulation: true— el Engine soporta modo dry-run por scenario). - Sí a la ruta
dynamic-blocksen la API del fw-edge-controller (F3), separada de las revisiones inmutables; el bouncer traefik plugin queda DESCARTADO (el incidente 2026-09-12 es la razón). - NO al reporte global en fase 1 (F4): opt-in diferido a fase 2 tras validar FP; la señal que saldría son solo metadatos anonimizados de decisiones, sin dominios ni site_id.
- Coste total de la pila a mantener: Engine+LAPI (1 pod), la ruta dynamic-blocks (~150 líneas en el controller + tests), 0 bouncers externos, 0 cambios en el agent nft existente. Beneficio: reacción de SEGUNDS a ataques en la plataforma (vs minutos-horas de las feeds estáticas) y escalada automática a incidente con señal compartida. El beneficio supera el coste: es el cierre del hueco "detección pasiva" → "bloqueo reactivo" que las capas Suricata (observa) y Tetragon (observa) no cubren.
Lo que NO decide este estudio: la implementación de la ruta dynamic-blocks (card operator-gated aparte, igual que el resto de applies del edge), el tuning fino de umbrales con tráfico real (requiere fase de simulación en un wepod de prueba), y la elección de bouncer-bridge concreto (script que llama a la API del controller vs un remediación component Go propio — a decidir en la card de implementación).
Follow-ups (ADR/REC candidates)¶
- Card operator-gated: activar Engine+LAPI (Deployment + PVC) y la
ruta
/api/v1/dynamic-blocksen el controller — tras aceptar el veredicto de este estudio. No incluye deploy (esta card es estudio). - ADR (candidato): "CrowdSec como capa reactiva del edge" — si el operador acepta el veredicto, extraer las 4 decisiones (Engine in-cluster v1.7.8+, scenarios per-site, dynamic-blocks por API, reporte diferido) a un ADR-0022 que enmiende el ADR-0019 §3 (Q3 → decidida).
- REC (candidato): registro del POC de simulación (1 semana de simulation-mode sobre el access log real) — evidencia para cerrar el ADR-0022 sin riesgo de FP.
- Update CON-CROWDSEC: anotar el veredicto y el pin v1.7.8 (la ficha dice v1.7.1, ya stale) — en el MISMO PR que esta INV (regla de Same-Change de DocOps).