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¶
- 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) consourceRange: 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. - 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. - Excepción explícita: el webhook de rebuild de docs
(
docs.wetopi.net/hooks/rebuild, rutadocs-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. - 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 extiendesourceRangecon 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
ipAllowListrechaza 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/logindesde 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-authlegacy 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-edgeno 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), Secretargo-basic-authborrado. - 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
sourceRangecuando 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).
Related documents¶
- 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.