--- issue: 0004 title: Hardening de seguridad — autorización, anti-DoS y confidencialidad antes de exponer público status: done created: 2026-06-07 completed: 2026-06-07 report: projects/message_bus/reports/0005-2026-06-07-unibus-security-hardening.md 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).