Files
unibus/dev/issues/0004-security-hardening.md
T
egutierrez 618f6b61da chore(0004): close issue, bump unibus to 0.5.0, record dataplane-acl decision
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>
2026-06-07 14:40:39 +02:00

7.4 KiB
Raw Blame History

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