docs(issue): 0006 completar+endurecer cluster — wiring KV + N1-N6 auditoría 0008 + material deploy magnus/homer/datardos
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
---
|
||||
issue: 0006
|
||||
title: Completar y endurecer el cluster — wiring del control plane KV + N1-N6 de la auditoría 0008
|
||||
status: spec
|
||||
created: 2026-06-07
|
||||
domain: security
|
||||
scope: unibus (cmd/membershipd, pkg/membership, pkg/embeddednats, pkg/busauth, pkg/client)
|
||||
depends_on: 0003 (completa su wiring), 0005 (hereda el bus single-node ya seguro)
|
||||
blocks: 0003f (deploy del cluster descentralizado)
|
||||
source: projects/message_bus/reports/0008-2026-06-07-unibus-decentralization-audit.md
|
||||
---
|
||||
|
||||
# Objetivo
|
||||
|
||||
La auditoría dedicada de la superficie de 0003 (report 0008) concluyó: **el bus en
|
||||
cluster NO es seguro para público** por dos bloqueantes, y además **0003 dejó el
|
||||
control plane descentralizado SIN cablear** (el binario sigue usando SQLite single-store;
|
||||
el flag `decentralized` existe pero ningún código Go lo lee). Como nodo único standalone
|
||||
unibus YA es seguro (report 0008 lo confirma); como cluster, no.
|
||||
|
||||
Este issue cierra los bloqueantes de seguridad del cluster Y completa el wiring que 0003
|
||||
dejó a medias, de modo que el deploy descentralizado (0003f) sea seguro. Cada fase
|
||||
reproduce el ataque del report 0008 (`TestAttack0008_*`) y verifica que ahora se rechaza.
|
||||
|
||||
# Fases (TBD, ramas `issue/0006x-*`)
|
||||
|
||||
## 0006a — N3 (BLOQUEANTE): cablear el nonce replicado en el binario
|
||||
|
||||
**Hallazgo (ALTA):** `membershipd` **nunca llama** `Server.UseReplicatedNonces`; cada nodo
|
||||
usa `memNonceCache` por-proceso. Un request firmado aceptado en el nodo A se **replaya con
|
||||
éxito en el nodo B** (200+200). La API (`kvNonceStore`) y el test
|
||||
(`TestReplicatedNonceRejectsCrossNodeReplay`) existen, pero el binario no los invoca.
|
||||
|
||||
**Fix:** en `cmd/membershipd/main.go`, cuando se arranca con `--cluster-name` (o siempre que
|
||||
haya JetStream disponible), llamar `srv.UseReplicatedNonces(js, replicas)` y **fail-fast** si
|
||||
el bucket `KV_UNIBUS_nonces` no se crea. Regla dura: `--cluster-name != ""` ⇒ nonce replicado
|
||||
**obligatorio** (no arrancar un nodo de cluster con nonce-cache local).
|
||||
|
||||
**DoD:** reproducir `TestAttack0008_N3` (2 nodos con el wiring exacto del binario) → el replay
|
||||
del nonce al nodo B ahora da **401**. Golden (request normal OK en cualquier nodo) + edge
|
||||
(single-node sin cluster sigue usando cache local OK) + error (replay cross-node → 401).
|
||||
|
||||
## 0006b — N2 (BLOQUEANTE): cerrar `$JS.API.>` / aislar el control plane KV
|
||||
|
||||
**Hallazgo (ALTA):** el grant ACL `clientInfraSubjects = {"_INBOX.>", "$JS.API.>"}`
|
||||
(`acl.go:20`) deja a cualquier peer registrado leer los buckets KV del control plane
|
||||
(`KV_UNIBUS_users/rooms/members/room_keys`) directo por NATS, saltándose `requireMember` y
|
||||
los chequeos del HTTP. Fuga del allowlist (handles+roles+claves), grafo de rooms y metadata
|
||||
de sealed-keys. (La ESCRITURA al KV ya está denegada — verificado; la fuga es de lectura.)
|
||||
|
||||
**Fix (elegir y documentar):**
|
||||
- Sustituir el grant amplio `$JS.API.>` por permisos JetStream **mínimos por-room** (solo la
|
||||
API del stream/consumer de las rooms del peer), y **denegar explícitamente** los streams
|
||||
`KV_UNIBUS_*` y `OBJ_*`; o
|
||||
- (Más robusto) aislar el control plane KV en una NATS **account separada**, inaccesible
|
||||
desde la account de clientes.
|
||||
|
||||
**DoD:** reproducir `TestAttack0008_N2` → eve (registrada, no miembro) ya **NO** puede leer
|
||||
los buckets KV (`Permissions Violation` o equivalente). La JetStream API legítima de las
|
||||
rooms del peer sigue funcionando.
|
||||
|
||||
## 0006c — wiring del control plane KV (completar 0003)
|
||||
|
||||
**Hallazgo (MEDIA / raíz):** el binario no activa el store descentralizado. `membership.Open`
|
||||
(SQLite) está hardcodeado en `main.go:90`; `OpenJetStream` solo lo usa `migrate-to-kv`.
|
||||
|
||||
**Fix:** leer el flag `decentralized` (o un `--store kv|sqlite`) y **seleccionar el store** en
|
||||
el arranque: SQLite (default, single-node/dev) o `jetstreamStore` (cluster). Resolver el
|
||||
"ciclo bootstrap" del authenticator interno (el authenticator necesita el store para
|
||||
`IsAuthorized`, y el store KV necesita el NATS arrancado). Mantener branch-by-abstraction:
|
||||
con el flag off, comportamiento idéntico al actual. `IsAuthorized`/lecturas sobre KV
|
||||
**fail-closed** ante pérdida de quorum/timeout (ya implementado en `jetstreamStore` —
|
||||
verificar que el wiring lo preserva).
|
||||
|
||||
**DoD:** con `decentralized: on` + cluster, el control plane sirve desde el KV replicado y un
|
||||
nodo nuevo ve las rooms creadas en otro (cierra la divergencia de estado que nota N5).
|
||||
Fail-closed: simular KV no disponible → deniega. Con flag off, suite idéntica al baseline.
|
||||
|
||||
## 0006d — N1 (ALTA): posture homogénea del cluster
|
||||
|
||||
**Hallazgo:** el cluster es tan seguro como su nodo más débil; un nodo sin authenticator o
|
||||
`--bus-auth off` deja a un peer no autenticado `Subscribe(">")` y cosechar el tráfico
|
||||
reenviado de los nodos con ACL.
|
||||
|
||||
**Fix:** garantizar (en arranque/health) que todos los nodos corren `enforce`+ACL+TLS;
|
||||
rechazar formar cluster con un peer en posture inferior, o como mínimo documentar y exponer
|
||||
un health que lo detecte. Nunca exponer el puerto de cliente de un nodo sin enforce.
|
||||
|
||||
**DoD:** reproducir `TestAttack0008_N1` escenario 2 (cluster con un nodo `withACL=false`) →
|
||||
el arranque/health lo rechaza o lo señala; documentar la garantía.
|
||||
|
||||
## 0006e — N4 (MEDIA): RefreshSession en los clientes
|
||||
|
||||
**Hallazgo:** la ACL congela permisos al conectar; un peer que crea/se une a una room debe
|
||||
llamar `client.RefreshSession()` para pub/sub en su subject. **Ningún cliente lo llama**
|
||||
(`cmd/chat`, `cmd/worker`, `mobile`, `gateway`). Es fail-closed (deniega), pero rompe la
|
||||
usabilidad bajo `enforce`+ACL → empuja al operador a desactivar la ACL (regresión de
|
||||
seguridad a discreción del operador).
|
||||
|
||||
**Fix:** llamar `RefreshSession` tras cambios de membresía en `cmd/chat`/`cmd/worker` (y
|
||||
documentar el contrato para `mobile`/`gateway`), o implementar refresh transparente (rehacer
|
||||
suscripciones automáticamente al unirse a una room).
|
||||
|
||||
**DoD:** test que crea/une room bajo enforce+ACL y publica/recibe SIN intervención manual
|
||||
(el cliente refresca solo o el demo llama RefreshSession). Documentar el requisito.
|
||||
|
||||
## 0006f — bajos: CA de routes, secreto de cluster, migrate-to-kv, R1≠HA
|
||||
|
||||
- **N1 (BAJA):** CA **separada** para las routes del cluster (no reusar la CA del data plane
|
||||
de clientes); pasar el secreto de cluster por **archivo/env**, no por `--routes
|
||||
nats://user:pass@host` en argv (hoy visible en `ps`/`journald`).
|
||||
- **N6 (BAJA):** `migrate-to-kv` solo en loopback o con TLS (hoy el allowlist viaja plaintext
|
||||
si `--nats-url` remoto sin `--ca`).
|
||||
- **N3-DoS (MEDIA, doc):** documentar que el nonce/control plane en **R1 es SPOF de auth** (su
|
||||
caída rechaza todos los requests autenticados); R3 (quorum 2/3) es la condición de HA real.
|
||||
No vender R1 como HA.
|
||||
|
||||
## 0006g — preparar el material de deploy del cluster (3 nodos)
|
||||
|
||||
Los tres nodos del cluster están decididos: **magnus + homer + datardos** (3 VPS OVH →
|
||||
quorum R3 real, tolera la caída de uno). Datos: homer `141.94.69.66`; datardos `ssh dd`
|
||||
`51.91.100.142` (WG `datardos-wg` 10.21.0.x); magnus en `pass` (`MAGNUS_ovh_ssh_ROOT`).
|
||||
|
||||
**Preparar (NO ejecutar en los VPS — eso es 0003f, lo hace el humano):** dejar en
|
||||
`deploy/cluster/` el material parametrizado por nodo:
|
||||
- `generate-cluster-certs.sh` — CA propia del cluster (separada de la de clientes, ver 0006f)
|
||||
+ un server cert por nodo con SAN = su IP pública + su IP WG + hostname.
|
||||
- una plantilla de systemd unit por nodo (`membershipd@.service` o tres units) con
|
||||
`--bind 0.0.0.0 --bus-auth enforce --tls-cert … --cluster-name unibus --routes
|
||||
nats://…@<otros-2-nodos> --store kv` y `Restart=always`, secreto de cluster por archivo/env.
|
||||
- `deploy-cluster.sh` (cross-build linux + rsync por nodo + plan de arranque escalonado).
|
||||
- un `README.md` con el runbook: orden de arranque, seed del admin, `migrate-to-kv` (loopback/TLS),
|
||||
escalado de réplicas a R3 (`nats stream update --replicas 3`), verificación de quorum y chaos
|
||||
test (matar un nodo). Marcar claramente qué pasos toca el humano.
|
||||
|
||||
**DoD:** el material existe y es coherente (los certs cubren los 3 nodos; las units referencian
|
||||
los routes correctos); un `bash -n` de los scripts pasa; el README describe el deploy end-to-end.
|
||||
NO se toca ningún VPS desde el agente.
|
||||
|
||||
# Fuera de alcance (otros issues / operacional)
|
||||
|
||||
- **H8** (CA generada/custodiada en om) → operacional del deploy 0003f.
|
||||
- **H9/H10/H11** → issue 0002 / futuro.
|
||||
- **Object Store (blobs) vía `$JS.API.>`**: el report 0008 lo marca como "probable misma
|
||||
clase que N2, no verificado" (impacto menor: blobs son ciphertext E2E). El fix de 0006b
|
||||
(denegar `OBJ_*`) lo cubre; verificar.
|
||||
- **Chaos test de red real** (matar 1/3, 2/3, split-brain) → 0003f (requiere 3 VPS).
|
||||
|
||||
# Definition of Done global
|
||||
|
||||
- `TestAttack0008_N3` → replay cross-node **401**; `TestAttack0008_N2` → eve no lee buckets KV;
|
||||
`TestAttack0008_N1` → nodo débil rechazado/señalado. Portados como regresión.
|
||||
- Con `decentralized: on`: control plane sobre KV replicado, fail-closed verificado, estado
|
||||
consistente entre nodos. Con flag off: baseline idéntico.
|
||||
- Clientes operan bajo `enforce`+ACL sin intervención manual (RefreshSession resuelto).
|
||||
- `CGO_ENABLED=0 go build ./... && go vet ./... && go test ./...` verdes + `govulncheck` 0.
|
||||
- Veredicto re-evaluado: el bus DESCENTRALIZADO pasa de "NO" a "sí-con-condiciones" (3 nodos
|
||||
R3 para HA real, posture homogénea, CA en om).
|
||||
Reference in New Issue
Block a user