---
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).
