Edge security & networking — el sistema completo¶
Este documento explica TODAS las capas de seguridad y red del edge Wetopi, qué hace cada componente, y cómo se opera: qué fichero se toca y dónde para cada cambio habitual (activar un WAF, cambiar un egress, cuarentenar un sitio, añadir una IP a una blocklist).
0. El mapa completo — de qué se protege qué¶
Internet
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CAPA 1 — PERÍMETRO (nftables en nodos WAN) │
│ wetopi_fwedge: blocklists maliciosas + GeoIP (barato, │
│ temprano, en el kernel) │
├─────────────────────────────────────────────────────────────┤
│ CAPA 2 — INGRESS L7 (Traefik edge, hostNetwork wan01/02) │
│ TLS terminate + rutas Gateway API + WAF Coraza (por sitio) │
├─────────────────────────────────────────────────────────────┤
│ CAPA 3 — SEGMENTACIÓN (Cilium: netpols label-selector) │
│ default-deny por namespace; egress controlado por Cilium │
│ EgressGateway → NAT-GW pool (natgw01) │
├─────────────────────────────────────────────────────────────┤
│ CAPA 4 — DETECCIÓN │
│ Suricata IDS (wan01/02+natgw01, AF_PACKET) │
│ Tetragon runtime (platform01-03, kprobes) │
├─────────────────────────────────────────────────────────────┤
│ CAPA 5 — OBSERVABILIDAD de seguridad │
│ eve.json/flows/access → otel-collector → data-prepper → │
│ OpenSearch security-* / access-traefik-* │
└─────────────────────────────────────────────────────────────┘
Los componentes y sus repos:
| Componente | Namespace | Repo | Rol |
|---|---|---|---|
| fw-edge (controller+agents) | fw-edge |
platform-security |
Compila/distribuye blocklists a nftables |
| Traefik edge | kube-system (DS) |
platform-edge |
Ingress L7 público (wan01/02) |
| WAF Coraza (plugin traefik) | kube-system |
platform-edge kubernetes/edge-test/waf/ |
Filtro CRS por sitio |
| Cilium + netpols | cluster-wide | infrastructure + cada app |
Segmentación E-W |
| NAT-GW / egress | node wetopi-natgw01 |
infrastructure (tofu+ansible) |
SNAT del clúster |
| Egress profiles §3 | por wepod (label) | platform-security kubernetes/egress-profiles/ |
Perfiles de egress por sitio |
| Suricata | edge-ids (DS) |
platform-security suricata/ |
IDS chokepoints |
| Tetragon | tetragon (DS) |
platform-security tetragon/ |
Runtime security |
| otel-collector + prepper | observability |
platform-observability |
Ingest telemetría |
1. Capa perímetro — fw-edge (nftables + blocklists)¶
Qué hace: en los nodos WAN (wan01/02) corre un agent que recibe del
controller sets de IPs maliciosas (firehol level1), allow/deny list duras
de Wetopi y políticas GeoIP, y las aplica como nft sets en la tabla
inet wetopi_fwedge. El tráfico a IPs de la blocklist se rechaza en el
kernel — barato y temprano, sin tocar el plano L7.
Reglas duras del sistema:
- Tabla propia wetopi_fwedge — nunca flush ruleset, nunca
tocar edge_lb (el REDIRECT 80→8000/443→8443 vive ahí).
- Sin Ansible para la distribución: el controller FastAPI expone los
sets, los agents hacen pull.
Operación — añadir una IP a la blocklist duradera:
1. La lista dura vive en el controller (platform-security/controller/).
Editar la allow/deny list (fichero de datos del controller) → PR →
merge → el controller re-serializa; los agents aplican en su próximo
pull.
2. Emergencia (una IP atacando AHORA): sudo nft add element inet
wetopi_fwedge blocklist-test { 1.2.3.4 } en cada wan (efímero: el
próximo sync del agent lo reconcilia con la lista del controller —
para que sea permanente, la IP DEBE entrar en la lista del
controller).
Cómo se rompe y se arregla: un agent caído = el nodo sigue sirviendo
con el último set aplicado (fail-open del perímetro, no del tráfico).
systemctl del agent + logs en el pod fw-edge-agent-*.
2. Capa ingress L7 — Traefik edge + WAF Coraza¶
Qué hace: el DS rke2-traefik (hostNetwork, SOLO wan01/02) termina
TLS y enruta vía Gateway API (kube-system/wetopi-edge). El WAF Coraza
es un plugin WASM del propio traefik que evalúa cada request contra
OWASP CRS v4 (PL1) cuando la ruta del sitio lleva el middleware
correspondiente.
El modelo de modos (ADR-0019 §27-29): cada sitio declara
wetopi.io/waf-mode: off|detect|block; el platform reconcilia aplicando
la route twin con el middleware que corresponde. El default es OFF —
el WAF se activa bajo demanda para sitios amenazados, nunca global.
Operación — activar el WAF para un sitio (ej. detect):
# En el HTTPRoute del sitio (ej. kubernetes/<repo>/<sitio>/httproute.yaml):
spec:
rules:
- filters: # AÑADIR este bloque a la ruta existente
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: waf-detect # off | detect | block
waf-off, waf-detect, waf-block) ya viven
aplicados en el namespace del sitio (edge-test hoy; al replicar a un ns
de producción, aplicar middlewares-waf.yaml de
platform-edge/kubernetes/edge-test/waf/ en ese ns). Cambiar de modo =
cambiar el name: del extensionRef. Quitar el WAF = quitar el bloque
filters (fail-open: la ruta sigue sirviendo).
Operación — dónde vive la configuración del WAF:
- Reglas CRS/exclusiones WordPress: middleware-waf-detect.yaml /
middleware-waf-block.yaml (mismos ficheros, platform-edge). Las
exclusiones tienen ids 100001-100005 con msg del motivo.
- Paranoia level / engine: directivas SecRuleEngine dentro del
middleware. PL1 es el techo acordado para producción hasta tunear.
⚠ Prohibido y por qué (incidente 2026-09-12): NO aplicar el delta
helmchartconfig-rke2-traefik-waf-plugin.yaml — el chart rke2-traefik
40.1.009 renderiza additionalVolumeMounts pero NO extraVolumes, así
que helm-controller desinstala el DS y el reinstal falla (DaemonSet
inválido) = caída total del edge. El plugin ya está instalado live
vía patch del DS (ver README de edge-test/waf/). El patch es
out-of-band: cualquier cambio futuro del HelmChartConfig de traefik lo
revierte (fail-open: el WAF deja de evaluar, el tráfico sigue).
Operación — upgrade de seguridad del Traefik del edge (GAPS-A19 /
W-TRAEFIK-CVE): el plan completo vive en
platform-edge/kubernetes/edge/UPGRADE-3.7.13.md (3.7.8 → 3.7.13, 5 CVE
fixes upstream 2026-09-04, todos con rangos que cubren 3.7.8; el crítico
CVE-2026-88007 + el bypass de request-target que desactiva el matching de
rutas del WAF). La vía es un delta de IMAGEN sobre el chart actual
(helmchartconfig-rke2-traefik-upgraded.yaml: valores gen-11 + image.tag
explícito — el gate de versiones del chart solo dispara para semver estable
puro, el sufijo -build lo bypassa por diseño). ⚠ El apply re-renderiza el
DS y revierte el patch out-of-band del plugin — la ventana del operador
(GATE-EDGE-UPGRADE) re-aplica la receta del patch INMEDIATAMENTE tras el
roll (mismo recipe README step-3) y corre el sondeo HTTP continuo con
rollback preparado. Build-only: sin apply por CI/agentes.
3. Capa segmentación — Cilium netpols + egress NAT-GW¶
Qué hace: cada namespace de app vive bajo default-deny-ingress
(+egress según app). Solo entra lo que una netpol permite, SIEMPRE por
label-selector (nunca ipBlock 0.0.0.0/0 — regla dura). El egress a
Internet de los pods sale por el NAT-GW (Cilium Egress Gateway → nodo
wetopi-natgw01, SNAT a 92.222.245.83).
Operación — cambiar el egress de un servicio (qué manifest, dónde): El egress de un wepod/servicio se controla con una NetworkPolicy de egress en SU namespace, por label+FQDN/PUERTO — p.ej. permitir solo updates de WordPress y SMTP autenticado:
# <repo-del-servicio>/kubernetes/<ns>/netpol-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-curated
namespace: <ns-del-servicio>
spec:
podSelector: { matchLabels: { app: <servicio> } }
policyTypes: [Egress]
egress:
- to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } },
podSelector: { matchLabels: { k8s-app: kube-dns } } }]
ports: [{ port: 53, protocol: UDP }]
- to: [{ podSelector: { matchLabels: { wetopi.io/egress: natgw } } }] # vía EgressGateway
ports: [{ port: 443, protocol: TCP }, { port: 587, protocol: TCP }]
Operación — encaminar un selector de pods por el NAT-GW: el objeto
es CiliumEgressGatewayPolicy (CRD cilium v1.20.1 instalado;
egressGateway.enabled=true live — infrastructure PR #57; apply del
operador tras su gate — el POC con la forma completa está en
platform-security/docs/egress-poc/). ⚠ TRAP (verificado live
2026-09-12): destinationCIDRs: ["0.0.0.0/0"] ROMPE el egress de los
pods que selecciona — SIEMPRE /32s explícitos (re-resueltos en la
ventana de asignación):
apiVersion: cilium.io/v2
kind: CiliumEgressGatewayPolicy
metadata:
name: <sitio>-egress
spec:
selectors: [{ podSelector: { matchLabels: { wetopi.io/site-id: <id> } } }]
destinationCIDRs: ["66.6.42.252/32"] # /32s explícitos, NUNCA 0.0.0.0/0
egressGateway:
nodeSelector: { matchLabels: { wetopi.io/role: edge-egress } }
egressIP: 92.222.245.83 # pool-a (.83) / pool-b (.84 — requiere attach del operador)
Operación — perfiles de egress de un sitio (ADR-0019 §3): los cinco
perfiles están pre-construidos como manifests BUILD-ONLY
(platform-security/kubernetes/egress-profiles/, PR W-EGPROF):
wordpress-standard (DNS+80/443, SMTP bloqueado), wordpress-restricted
(FQDNs aprobados pinneados como /32s en la CEGP del pool-b),
wordpress-mail-relay (solo relay interno, TCP 587 →
wetopi.io/role: mail-relay), quarantine (forense only),
custom (owner+expiry REQUIRED). Activar uno en un sitio = label del
pod template wetopi.io/egress-class: <perfil> + apply del set por el
operador (runbook de 10 líneas en kubernetes/egress-profiles/README.md
de platform-security; tabla de perfiles/pools en el mismo README).
Operación — cuarentena de un sitio comprometido: el perfil quarantine
está pre-construido (platform-security/manifests/wepod-test/
netpol-quarantine/): default-deny-ingress/egress + DNS-only +
forensic-egress. Aplicarlo a un sitio = label el pod
wetopi.io/quarantine: "true" y aplicar el set de netpols
quarantine-scoped en su namespace (runbook completo en
docs/runbooks/quarantine.md de platform-security). binding equivalente
por perfil §3: wetopi.io/egress-class: quarantine
(kubernetes/egress-profiles/quarantine-netpol.yaml).
⚠ El trap del hostNetwork (live-verified 2026-09-12): el traefik edge
corre hostNetwork, así que Cilium clasifica su tráfico como identidad de
NODO. Una netpol from: podSelector(traefik) NUNCA puede abrirle el
paso a un pod. El mecanismo que funciona es un CiliumNetworkPolicy:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata: { name: allow-host-ingress, namespace: <ns> }
spec:
endpointSelector: {}
ingress:
- fromEntities: ["host", "remote-node"]
toPorts: [{ ports: [{ port: "80", protocol: TCP }] }]
observability/allow-host-ingress).
4. Capa detección — Suricata (red) y Tetragon (runtime)¶
Suricata IDS¶
Qué hace: DS hostNetwork en los 3 chokepoints (wan01/02, natgw01)
capturando AF_PACKET de eth0. Evalúa el ruleset
wetopi-edge.rules (ataques de red de alta señal) y escribe TODO evento
(flows/alerts/stats/tls/dns/http) a /var/log/suricata/eve.json del
nodo, que el otel-collector taila → índice security-suricata-*.
Operación — añadir/cambiar una regla: el ruleset es el ConfigMap
suricata-rules (platform-security/suricata/manifests/
suricata-rules-configmap.yaml). Editar la regla (formato Suricata
signatures, sid del rango 20260xx de Wetopi) → PR → merge → kubectl apply +
roll del DS. Regla inválida = warning en el arranque, el engine sigue
con el resto (fail-soft).
Operación — el health check: el suricatasc del pod; kubectl -n
edge-ids get pods (3/3 Running); el índice security-suricata-*
creciendo (consulta _count, NO _cat/indices — este cachea).
Tetragon runtime security¶
Qué hace: DS en platform01-03 con 4 TracingPolicies de alta señal:
nginx-shell-spawn, php-fpm-shell-spawn (un worker del web tier
spawnando shell = compromiso), exec-from-wp-content (ejecución desde
uploads), wepod-connect-baseline. Los eventos van al stdout del agent
→ logs-* (índice dedicado security-tetragon-* pendiente W-TETRA.2).
Operación — añadir una policy: fichero YAML TracingPolicy en
platform-security/tetragon/tracing-policies/ → añadirlo al ConfigMap
tetragon-tracing-policies (mismo repo) → PR → merge → apply + roll del
DS (el agent carga el tp.d al arrancar). Los kprobes con
matchParentBinaries requieren parents-map-enabled: "true" (ya set).
OBSERVE-ONLY por diseño (ADR-006): ninguna policy lleva enforcement.
5. Capa observabilidad de seguridad¶
Qué hace: cada feed de telemetría tiene su índice OpenSearch:
| Feed | Índice | Estado |
|---|---|---|
| Suricata eve.json | security-suricata-* |
✅ vivo |
| Access logs traefik | access-traefik-* |
✅ vivo |
| Audit k8s | security-auditlog-* |
✅ vivo |
| Hermes agents | logs-hermes-* |
✅ vivo |
| General pods | logs-* |
✅ vivo |
| Hubble flows | security-hubble-* |
✅ conectado (E-OBS.2: el receiver filelog/hubble del collector taila el export JSON del agent; Data Prepper lo attribute-rutea a security-hubble-*; PR W-OBS.2b) |
| WAF Coraza (eventos) | security-waf-* |
✅ conectado (W-WAF-OBS: las líneas Coraza: ... del MISMO stdout del traefik que los access logs van por la rama waf del receiver file_log/access-traefik (router waf-line-router → marca event_category=waf-event undotted + waf_rule_id/waf_action/waf_http_host/waf_http_uri/waf_tx_id); Data Prepper rutea a security-waf-*; el índice lo crea security-waf-template priority 200 + ISM 30d) |
| fw-edge controller (eventos) | security-edgefw-* |
✅ conectado (W-WAF-OBS: el controller emite UN JSON event por línea a stdout (revision_applied al publicar, feed_error cuando un feed falla — last-known-good aparte); el receiver filelog/fw-edge-controller los marca con event_category=edgefw-event y Data Prepper rutea a security-edgefw-* con event_action/revision/sha256/feed_name al doc root) |
| Tetragon | logs-* (dedicado W-TETRA.2) |
pendiente |
El contrato técnico (por qué las cosas se rompieron antes):
1. Attrs undotted: el codec del data-prepper substituye puntos por
@ en los KEYS de atributos OTLP, y los routes evalúan el doc root
con paths dot-as-nested. Cualquier attr de routing DEBE ser
undotted (event_category, access_route — NUNCA
access.route).
2. copy_values antes del route: el prepper solo rutea lo que ha sido
promovido al doc root (copy_values: from_key:
attributes/log.attributes.<k> → to_key: <k>).
3. El stanza fileconsumer aborta el batch entero ante CUALQUIER
error de operator (aunque lleve error_mode: ignore): los
operators que pueden fallar van siempre detrás de un router con
default a un operator terminal.
4. _cat/indices cachea el count — verificar ingest SIEMPRE con
_count o _search.
Operación — diagnóstico de ingest: receiver activo (kubectl logs
<collector> | grep "Started watching"), 0 batch-aborts (grep "failed
to emit"), exporter enviando (tcpdump del prepper :2189 si hace falta),
prepper render (kubectl exec <prepper> -- cat .../pipelines.yaml —
cada cambio del cm del prepper exige rollout restart deploy
data-prepper: el init-container renderiza SOLO al arrancar).
6. Cómo se opera el conjunto — reglas de oro¶
- Todo cambio de manifest va por PR → CI verde → merge → (Argo
sync si la app es Argo-managed; apply desde ops01 si es
edge/out-of-band). Un
kubectl patchlive es ÚLTIMO recurso y se replica a Git en el mismo día (Argo selfHeal lo revierte). - Los applies live del edge (Gateway, traefik DS, DNS) son del operador — el lead prepara, el operador ejecuta en su ventana.
- Probar el delta antes de tocar el live:
helm templatedel chart real o un pod throwaway; el incidente 2026-09-12 nació de un delta no validado contra el chart. - El detector real es el probe HTTP público (docs.wetopi.net 200 desde fuente allowlist), no Argo health (Law 4 del GitOps skill).
- Co-tenants primero: cualquier cambio del edge se hace con sondeo público continuo cada 10s y rollback documentado ANTES de empezar.
7. Cómo se rompe y cómo se arregla (resumen por capa)¶
| Síntoma | Causa típica | Primera acción |
|---|---|---|
| Todo el edge caído (connection refused en 443) | DS traefik ausente/inválido (helm uninstall fallido) | Restaurar el DS del snapshot; ver INC-EDGE-OUTAGE-20260912 |
| Un sitio 404 con el WAF activo | Plugin disabled (traefik#13005) | Cambiar la route twin a waf-off; investigar el stdout del traefik |
| Un pod no recibe tráfico del traefik | Netpol label errónea o falta el CCNP host-ingress | hubble observe; recordar: traefik = hostNetwork = node identity |
| Índice security-* a 0 docs | Batch-abort del fileconsumer o route sin copy_values | grep "failed to emit" en el collector; _count real |
| Egress de un pod roto | Sin CEGP o netpol egress sin el selector | Verificar CiliumEgressGatewayPolicy + la netpol del ns |
| Suricata CrashLoop | Liveness probe (suricatasc v8: usar version, no ping) o regla inválida |
kubectl logs --previous del pod |