Skip to content

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
Los tres middlewares (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 }]
Sin esta política (o con el deny por defecto) el pod NO sale a Internet salvo por el SNAT del NAT-GW que el CiliumEgressGatewayPolicy asigne.

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 }] }]
(el mismo patrón que 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

  1. 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 patch live es ÚLTIMO recurso y se replica a Git en el mismo día (Argo selfHeal lo revierte).
  2. Los applies live del edge (Gateway, traefik DS, DNS) son del operador — el lead prepara, el operador ejecuta en su ventana.
  3. Probar el delta antes de tocar el live: helm template del chart real o un pod throwaway; el incidente 2026-09-12 nació de un delta no validado contra el chart.
  4. El detector real es el probe HTTP público (docs.wetopi.net 200 desde fuente allowlist), no Argo health (Law 4 del GitOps skill).
  5. 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