618f6b61da
Issue 0004 (security hardening) done across 0004a-0004f. app.md version 0.5.0 with the capability growth log entry; dev/0004d-dataplane-acl.md documents the chosen minimum-defense strategy for the NATS data plane and its residual limit (per-subject ACL deferred to 0003). Full work report in projects/message_bus/reports/0005-2026-06-07-unibus-security-hardening.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
7.4 KiB
Markdown
147 lines
7.4 KiB
Markdown
---
|
||
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).
|