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>
7.4 KiB
issue, title, status, created, completed, report, domain, scope, depends_on, blocks, source
| issue | title | status | created | completed | report | domain | scope | depends_on | blocks | source |
|---|---|---|---|---|---|---|---|---|---|---|
| 0004 | Hardening de seguridad — autorización, anti-DoS y confidencialidad antes de exponer público | done | 2026-06-07 | 2026-06-07 | projects/message_bus/reports/0005-2026-06-07-unibus-security-hardening.md | security | unibus (pkg/membership/server.go, auth.go, pkg/embeddednats, pkg/client, cmd/membershipd, deploy/tls) | 0001 (cierra los gaps que la auditoría 0004 encontró sobre lo entregado en 0001) | 0001f (deploy público) y 0003f (deploy descentralizado) | 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.MaxBytesReaderen el middleware antes delio.ReadAll(límite control plane, p.ej. 1 MB).- Límite separado y mayor para
/blobs, con rechazo temprano porContent-Lengthantes de bufferizar; idealmente stream a disco en vez de RAM. Server.MaxHeaderBytesajustado.- Rate-limit por IP (y por identidad tras auth). Reusar/crear una función del registry si
aplica (delegar a
fn-constructorsi 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
--bindno es loopback ⇒ exigir--bus-auth enforce(si no,log.Fatalcon mensaje claro). --tls-cert/--tls-keysin--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
membersy exige que el firmante (X-Unibus-Pub→ endpoint) sea miembro. /rooms/{id}/keysolo sirve la clave sellada para el propio firmante (endpoint == signer), nunca de un tercero./members/{endpoint}/roomssolo siendpoint == 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
Permissionspor 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
ModeNATSen 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
httpscuando 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: exigirOwner.Endpoint == frame.EndpointID(X-Unibus-Pub)yOwner.SignPub == pub. (PortarTestAudit_OwnerSpoof→ ahora 403.) - H7 mover
IsAuthorizedantes de tocar elnonceCache(no cachear nonces de no-autorizados); poda por expiry-bucket/heap en vez de O(n) bajo mutex global; cap de tamaño. (PortarTestAudit_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 relajaenforce. - H8 (custodia de la CA: generar en om,
ca.keyfuera 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
- 0004 (este) — primero: hace el bus seguro para exponer.
- 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.
- 0002 (media v2) — ortogonal; incluye la cuota/GC de blobs (H9).