docs(issues): encolar 0002 (media v2), 0003 (descentralización HA), 0004 (hardening seguridad)

Specs de los tres issues siguientes del bus, derivados de esta sesión:
- 0002 media v2: chunking, mimetype, GC del object store, exponer en clientes.
- 0003 descentralización/HA: cluster NATS magnus+homer (R1→R3), control plane
  SQLite→JetStream KV, quorum, failover. Tercer nodo = homer (141.94.69.66).
- 0004 hardening: cierra los hallazgos de la auditoría red-team (report 0004):
  DoS pre-auth, fail-open, autorización por pertenencia, ACL NATS, TLS control plane.
This commit is contained in:
agent
2026-06-07 14:04:33 +02:00
parent 484a07d6fd
commit bcd02716d5
3 changed files with 485 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
---
issue: 0004
title: Hardening de seguridad — autorización, anti-DoS y confidencialidad antes de exponer público
status: spec
created: 2026-06-07
domain: security
scope: unibus (pkg/membership/server.go, auth.go, pkg/embeddednats, pkg/client, cmd/membershipd, deploy/tls)
depends_on: 0001 (cierra los gaps que la auditoría 0004 encontró sobre lo entregado en 0001)
blocks: 0001f (deploy público) y 0003f (deploy descentralizado)
source: projects/message_bus/reports/0004-2026-06-07-unibus-security-audit.md
---
# Objetivo
La auditoría red-team (report 0004) concluyó: la **autenticación** del bus es sólida,
pero faltan **autorización, disponibilidad y confidencialidad de metadata** — justo lo
que un bus *público* necesita. Veredicto: **NO exponer público hoy**. Este issue cierra
los hallazgos bloqueantes (1 crítico + 4 altos) y los medios relevantes, de modo que el
deploy 0001f (público) y luego 0003 (descentralizado) sean seguros.
Cada fase corresponde a un hallazgo del report 0004. La **DoD de cada fase es portar el
test adversarial del auditor** (`TestAudit_*`) y verificar que ahora arroja el resultado
SEGURO (lo que antes pasaba el ataque, ahora lo rechaza).
# Fases (TBD, ramas `issue/0004x-*`, una por hallazgo)
## 0004a — H1 (Crítico): límite de cuerpo + anti-DoS pre-auth
**Problema:** `Server.ServeHTTP` hace `io.ReadAll(r.Body)` **sin límite y antes** de
`authenticate()`; `handlePutBlob` repite el `io.ReadAll` sin límite. 400 MB sin
credenciales → 898 MB RSS → OOM con pocas conexiones.
**Fix:**
- `http.MaxBytesReader` en el middleware **antes** del `io.ReadAll` (límite control plane,
p.ej. 1 MB).
- Límite separado y mayor para `/blobs`, con rechazo temprano por `Content-Length` antes
de bufferizar; idealmente stream a disco en vez de RAM.
- `Server.MaxHeaderBytes` ajustado.
- Rate-limit por IP (y por identidad tras auth). Reusar/crear una función del registry si
aplica (delegar a `fn-constructor` si es genérica).
**DoD:** test que envía un cuerpo > límite sin firma → `413`/`401` **sin** que el RSS se
dispare (medir `/proc/self/status` antes/después, delta acotado). Golden (cuerpo normal
pasa) + edge (justo en el límite) + error (excede → rechazo barato).
## 0004b — H2 (Alto): cerrar el fail-open de configuración
**Problema:** default `--bus-auth off`; el nkey de NATS solo se activa en `enforce`; TLS
es flag independiente. `--bind 0.0.0.0 --tls-cert …` **sin** `--bus-auth enforce` deja el
bus abierto con apariencia de seguro.
**Fix:**
- Si `--bind` no es loopback ⇒ exigir `--bus-auth enforce` (si no, `log.Fatal` con mensaje
claro).
- `--tls-cert`/`--tls-key` sin `--bus-auth enforce` ⇒ error de arranque.
- Arranque inseguro imposible o, como mínimo, ruidoso y rechazado.
**DoD:** portar `TestAudit_FailOpenTLSWithoutAuth` → ahora el arranque público-sin-enforce
falla; cliente no registrado NO conecta. Golden (bind loopback dev sigue permitido) + error
(bind público sin enforce aborta).
## 0004c — H3 (Alto): autorización por pertenencia en el control plane
**Problema:** "autorizado" = "registrado", no "miembro". Los GET de room no comprueban
pertenencia: `/rooms/{id}`, `/rooms/{id}/members` (expone `sign_pub`+`kex_pub` de todos),
`/members/{endpoint}/rooms`, y `/rooms/{id}/key?endpoint=X` (devuelve la `sealed_key` ajena).
**Fix:**
- Cada handler de room consulta `members` y exige que el firmante (`X-Unibus-Pub`
endpoint) sea miembro.
- `/rooms/{id}/key` solo sirve la clave sellada **para el propio firmante** (`endpoint ==
signer`), nunca de un tercero.
- `/members/{endpoint}/rooms` solo si `endpoint == signer`.
- No exponer la member-list completa a no-miembros.
**DoD:** portar `TestAudit_HorizontalMetadataLeak` → bob (no miembro) ahora recibe `403`
en todos. Golden (miembro legítimo accede) + edge (owner accede) + error (no-miembro 403).
## 0004d — H4 (Alto): control de acceso en el data plane NATS
**Problema:** el authenticator nkey solo decide "registrado sí/no"; no hay permisos por
subject. Cualquier registrado se suscribe/publica en cualquier subject; las rooms
`ModeNATS` (cleartext) quedan expuestas entre usuarios.
**Fix (elegir y documentar la estrategia):**
- Preferente: NATS `Permissions` por identidad (subjects que el usuario puede sub/pub),
derivadas de su pertenencia a rooms; o
- Subjects impredecibles (no derivables del nombre) + verificación de pertenencia
server-side; o
- Prohibir `ModeNATS` en despliegue público (forzar siempre E2E) como mínimo defensivo.
**DoD:** portar `TestAudit_NoSubjectACL` → eve (no invitada) ya NO recibe el mensaje de la
room ajena. Documentar la estrategia elegida y su límite.
## 0004e — H5 (Alto, público): TLS en el control plane
**Problema:** HTTP `:8470` firmado pero **sin TLS** → metadata (subjects, endpoints,
pubkeys, sealed keys, hashes de blobs, grafo social) legible por un MITM en la red pública.
**Fix:**
- Servir el control plane sobre TLS con la misma CA propia (o documentar un reverse-proxy
TLS delante).
- El cliente exige `https` cuando se le pasa una CA (`client.Connect(caPath)` ⇒ control
plane también TLS).
**DoD:** cliente contra control plane `https` con la CA → OK; contra `http` con CA esperada
→ rechaza; un observador no ve la metadata (argumentado + test de esquema).
## 0004f — medios: owner binding, nonce-cache, error leak
- **H6** `handleCreateRoom`: exigir `Owner.Endpoint == frame.EndpointID(X-Unibus-Pub)` y
`Owner.SignPub == pub`. (Portar `TestAudit_OwnerSpoof` → ahora 403.)
- **H7** mover `IsAuthorized` **antes** de tocar el `nonceCache` (no cachear nonces de
no-autorizados); poda por expiry-bucket/heap en vez de O(n) bajo mutex global; cap de
tamaño. (Portar `TestAudit_NonceCachePoisonPreAuth`.) **Nota:** este fix es prerequisito
del cambio a nonce-cache replicado del issue 0003.
- **H12** mensajes de error genéricos al cliente; detalle solo al log (no filtrar rutas/SQL).
# Fuera de alcance de este issue (encolado en otros)
- **H9** (cuota/GC de blobs) → issue 0002 (media v2) ya lo cubre.
- **H10** (AEAD nonce 12B → XChaCha o rekey por volumen) → bajo, futuro; abrir issue propio
si se necesitan rooms de muy alto volumen.
- **H11** (firma de owner sin nonce/ts) → cubierto en la práctica por el envelope `enforce`;
documentar la dependencia. Reforzar si se relaja `enforce`.
- **H8** (custodia de la CA: generar en om, `ca.key` fuera del PC) → tarea operacional del
deploy 0001f/0003f, no de código.
- **govulncheck** sobre nats-server/nats.go/modernc → paso de CI aparte.
# Definition of Done global
- Las cuatro pruebas adversariales bloqueantes del report 0004 (DoS acotado, fail-open
cerrado, fuga horizontal 403, ACL data plane) portadas como tests de regresión y en verde.
- `CGO_ENABLED=0 go build ./...` + `go vet ./...` + `go test ./...` verdes.
- Re-evaluación: tras el hardening, el veredicto de exposición pública pasa de "NO" a
"sí-con-condiciones operacionales" (CA custodiada, Restart=always). Anotar en un report
nuevo o como addendum al 0004.
# Orden respecto a otros issues
1. **0004 (este)** — primero: hace el bus seguro para exponer.
2. **0003 (descentralización)** — después: absorbe el nonce-cache→KV replicado (apoyado en
0004f-H7), la auth de routes del cluster y el guard de fail-open ×N nodos.
3. **0002 (media v2)** — ortogonal; incluye la cuota/GC de blobs (H9).