Skip to content

ADR-0014: Edge source-IP allow-list (capa VPN del operador)

Status

ACCEPTED (2026-09-01, directriz del operador durante la operación; implementado y verificado en vivo el mismo día — infra PR #36, merge b369b0a).

Amplía ADR-0009 (re-cierre de su decisión 2 por otra vía de enforcement) y la postura de ADR-0006 (exposición de servicios por el edge WAN): el edge pasa de "auth HTTP" a "filtro de red por origen".

Context

La decisión 2 de ADR-0009 prohibía que el API de machine identities (Universal Auth) fuera alcanzable por vault.wetopi.net, con enforcement por path-DENY en el edge (infra#27). El filtro resultó demasiado ancho: bloqueaba también POSTs same-origin de la propia UI humana (/api/v1/auth/universal-auth/identities/{id}), rompiendo la configuración interactiva de identidades. El deny se retiró (infra#35, 2026-09-01) y el API completo quedó expuesto detrás de la única auth de Infisical, dejando la Ley 1 de ADR-0009 en deuda abierta con un compensating control prometido: "el filtrado de máquinas pasa a la capa VPN/red".

En paralelo, el operador dictaminó retirar el HTTP Basic Auth del edge ("quitamos los authbasic"): la autenticación por htpasswd duplicaba la auth nativa de las apps (Argo login, Infisical login+TOTP), no expresa autorización por secreto (alternativa ya descartada en ADR-0009) y su gestión de credenciales (Secrets operator-side con rotación manual) es deuda operativa sin valor añadido si existe un control de red previo.

Los hostnames de control-plane del edge (argo.wetopi.net, vault.wetopi.net, docs.wetopi.net) son de uso exclusivo del operador; no hay razón para que internet pueda ni siquiera intentar login contra ellos.

Decision

  1. Todo hostname de control-plane publicado por el edge se filtra por IP de origen: Middleware Traefik ipAllowList (edge-ip-allowlist, una instancia por namespace de ruta: kube-system, vault, docs-site) con sourceRange: 51.254.106.87/32 — el egress de la VPN del operador. Cualquier otro origen recibe 403 Forbidden en el edge, antes de alcanzar backend, auth de aplicación o API.
  2. El HTTP Basic Auth se retira de las rutas del edge (directriz del operador, 2026-09-01): la auth de aplicación (login de Argo CD, login+TOTP de Infisical) queda como única capa de autenticación, sobre el control de red. El middleware/Secret de docs-site (docs-basic-auth) se conserva temporalmente SOLO porque el runbook de rollback del cutover docs-runtime (CUTOVER.md) restaura la ruta pre-incidente desde git history con ese filtro; se elimina en el paso de cutover.
  3. Excepción explícita: el webhook de rebuild de docs (docs.wetopi.net/hooks/rebuild, ruta docs-runtime-webhook) NO lleva filtro IP: su cliente es GitHub (IPs no controlables) y su auth es la firma HMAC-SHA256 (ADR-0011) validada timing-safe por el receiver. Un endpoint de máquina autenticado por criptografía no necesita ni debe heredar el gate de origen humano.
  4. El CIDR autorizado vive en 4 ficheros (middleware-ipallowlist.yaml ×3 + el valor en el runbook del apply): cuando exista el pool VPN definitivo se extiende sourceRange con su CIDR (un cambio reviewed por Git). Hasta entonces, 51.254.106.87/32 es el egress VPN vigente del operador (NO es la WAN del bastión ops01, que es 92.222.245.91).

Rationale

  • La Ley 1 se re-implementa donde de verdad vive: no importan los paths que abra la UI (el fallo de infra#27/#35 desaparece — el API completo puede quedar abierto sin riesgo), porque las máquinas no llegan: su IP de origen no está en la lista. Elimina la fragilidad estructural del path-DENY (cada endpoint nuevo de la UI puede romper la regla) por un invariante de red.
  • Fail-closed garantizado por diseño: Traefik ipAllowList rechaza con 403 lo no listado; si el middleware no resuelve, la ruta falla cerrada (500), nunca abierta.
  • Menos superficie y menos deuda: sin htpasswd que rotar ni Secrets de credenciales fuera del vault; la identidad del operador en el edge es su egress VPN, rotable en un sitio.
  • Coste: el control depende de la estabilidad del egress VPN. Si el egress rota, el operador pierde acceso (403) hasta actualizar la lista — riesgo aceptado y documentado; la reversibilidad es un git revert (Argo selfHeal aplica el rollback).

Consequences

  • Positivas: Ley 1 de ADR-0009 enforcement de nuevo verificable (403 en /api/v1/auth/universal-auth/login desde internet); el edge queda con una postura uniforme "control de red + auth nativa de la app"; un solo mecanismo para argo/vault/docs.
  • Negativas / deuda registrada: acceso humano desde una IP fuera de la lista = 403 (si el operador navega fuera de su VPN, no entra — extender sourceRange o conectarse por la VPN); el webhook de docs queda expuesto a internet con HMAC como única defensa (flood/DoS mitigable con rateLimit futuro si se necesita); el docs-basic-auth legacy persiste hasta el cutover (objeto inerte pero vivo).
  • Operativas: añadir una fuente autorizada = 1 línea en los 4 middleware-ipallowlist.yaml (PR revisado, Argo sincroniza). Las apps include-only exigen que cualquier fichero nuevo esté en la include list de su Application (lección del caso PR#36, en wetopi-gitops-ops).

Alternatives considered

Alternativa Por qué se descartó
Restaurar el path-DENY en vault (Ley 1 por paths) Ya demostró ser frágil: rompió la UI misma (infra#27→#35); cada endpoint nuevo del API reabre la herida
Mantener basic-auth como capa extra Duplicaba la auth nativa, credenciales fuera del vault, sin valor tras el gate IP (directriz explícita del operador)
WireGuard/VPN propia ya (Ley 1 en L3) Es el objetivo final pero requiere diseñar/desplegar la VPN; el ipAllowList entrega HOY el mismo enforcement con el egress existente del operador
Cloudflare Access / SSO en el edge Añade dependencia externa y cambia el modelo de acceso; no requerido por el operador

Migration / implementation notes

  • Implementado en infra PR #36 (branch feat/edge-ip-allowlist, merge b369b0a, 2026-09-01), revisión en 2 rondas (r1 REQUEST_CHANGES por include lists de las apps include-only, corregido; r2 APPROVE).
  • Apply-time ejecutado: middleware creado en 3 namespaces (la app argo-edge no está anclada a la root app — su Application se re-aplicó por el patrón bootstrap documentado), ruta legacy de docs parcheada out-of-band (objeto restore deliberadamente no gestionado por Argo; re-own por Git la resucitaría tras el cutover — invariante RC4), Secret argo-basic-auth borrado.
  • Verificación pública (2026-09-01): 403 en argo/vault/docs desde egress no autorizado (92.222.245.x); 200/302 esperado desde la VPN del operador; webhook sin abrir hasta el cutover docs-runtime.
  • Pendiente: extender sourceRange cuando exista el pool VPN (ampliación trivial); el webhook pasará a estar routable con el flip de docs-runtime (card t_ffe4d39b, secrets de webhook pendientes del operador).
  • ADR-0009 (vault del harness): su decisión 2 queda amendida — la Ley 1 se enforcementa por IP de origen (esta ADR) en lugar de path-DENY; "Edge Basic Auth como control único" sigue descartado.
  • ADR-0006 (edge v1 WAN): postura de exposición de servicios por el edge, ahora con control de origen para control-plane.
  • Infra PRs: #27 (deny original), #35 (apertura del API), #36 (este gate). Runbooks: kubernetes/edge-argo/README.md, kubernetes/docs-runtime/CUTOVER.md.