Files
unibus/dev/issues/0005-security-hardening-2.md
T
egutierrez 6976537842 chore(0005): bump unibus to 0.7.0, close issue 0005 (hardening 2)
Hardening 2 (issue 0005, fases 0005a-0005e) cierra los hallazgos nuevos de la
re-auditoría red-team (report 0006): bump de nats-server + toolchain (16 CVEs ->
0 alcanzables), drop de frames sin firma en rooms SignMsgs, limiter global de
bytes en vuelo contra el DoS por concurrencia, TLS obligatorio en bind publico, y
cableado de la ACL por subject que cierra el wildcard metadata leak. Detalle por
fase en el capability growth log del app.md y en el report 0007.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:17:41 +02:00

6.7 KiB
Raw Blame History

issue, title, status, created, completed, domain, scope, depends_on, blocks, source
issue title status created completed domain scope depends_on blocks source
0005 Hardening 2 — CVEs, spoof por firma omitida, DoS por concurrencia, TLS forzado (re-auditoría) done 2026-06-07 2026-06-07 security unibus (go.mod, pkg/client, pkg/membership/server.go, cmd/membershipd/config.go, pkg/embeddednats, pkg/blobstore) 0001, 0004 (cierra los hallazgos NUEVOS de la re-auditoría sobre lo entregado) 0001f (deploy público) y 0003f (deploy descentralizado) projects/message_bus/reports/0006-2026-06-07-unibus-security-reaudit.md

Objetivo

La re-auditoría red-team (report 0006) confirmó que el hardening 0004 cerró H1H7/H12, pero encontró hallazgos nuevos que mantienen el veredicto en "NO exponer público aún". Este issue los cierra. La re-auditoría se hizo sobre el commit 618f6b6 (pre-0003); algunos hallazgos pueden haber cambiado con 0003 — cada fase debe primero verificar si el hallazgo sigue vivo en el master actual (post-0003, v0.6.0) antes de arreglarlo.

Estado verificado al crear este issue (master post-0003):

  • N1 vivo: go.mod sigue en nats-server v2.10.22 y go 1.25.0.
  • N3 vivo: pkg/client/client.go:802 tiene if info.Policy.SignMsgs && f.Sig != nil (el patrón vulnerable exacto).
  • H4: 0003 añadió pkg/membership/acl.go — hay que evaluar si cierra el wildcard Subscribe(">") o si falta la capa de NATS Permissions.
  • N2, N4: presumiblemente vivos (0003 no los tocó); verificar.

Fases (TBD, ramas issue/0005x-*)

0005a — N1 (Alto): CVEs en dependencias

Hallazgo: govulncheck ./... → 16 vulnerabilidades alcanzables: 14 en github.com/nats-io/nats-server/v2@v2.10.22 (servidor embebido, expuesto público en el deploy decidido) + 2 en la stdlib de Go (net/textproto GO-2026-5039, crypto/x509 GO-2026-5037).

Fix:

  • go get github.com/nats-io/nats-server/v2@v2.11.15 (o superior que cubra las 14).
  • Subir la toolchain a go1.26.4 (cubre las 2 de stdlib); actualizar la directiva go en go.mod si procede.
  • Re-correr govulncheck ./... hasta 0 affected.
  • Nota: este es un cambio de go.mod/go.sum justificado por CVE; documentarlo en el commit. Verificar que el bump de nats-server no rompe el cluster/JetStream de 0003 (correr toda la suite, incluido el e2e multi-nodo).

DoD: govulncheck ./... → "No vulnerabilities found" (o solo no-alcanzables); suite completa verde tras el bump.

0005b — N3 (Alto): spoof por firma omitida en rooms firmadas

Hallazgo: pkg/client/client.go::processFrame verifica la firma solo si el frame la trae: if info.Policy.SignMsgs && f.Sig != nil { verify }. Un atacante con acceso al data plane publica un frame con Sig==nil y Sender forjado → el receptor lo acepta como auténtico en una room que EXIGE firma.

Fix: en una room SignMsgs, un frame sin firma debe dropearse:

if info.Policy.SignMsgs {
    if f.Sig == nil { return }     // exige firma; sin ella, descarta
    if !verify(...) { return }
}

DoD: portar TestReaudit_SigNilSpoof → ahora el frame Sig==nil con Sender forjado en una room SignMsgs se descarta (no se entrega al handler). Golden (frame firmado válido se entrega) + edge (room sin SignMsgs no se ve afectada) + error (Sig==nil en SignMsgs → drop).

0005c — N2 (Medio-Alto): DoS por concurrencia

Hallazgo: el límite por-request (16 MiB) + rate-limit per-IP NO acotan la memoria agregada. 40 subidas de 16 MiB simultáneas (= el burst per-IP) → 1.42 GB RSS. Multi-IP escala sin techo.

Fix (elegir y documentar):

  • Límite global de conexiones concurrentes y/o de bytes-en-vuelo (semáforo con cota de memoria total), y/o
  • Stream del blob a disco en vez de io.ReadAll en RAM (encaja con la cuota/GC del issue 0002), y/o
  • Bajar maxBlobBytes y separar mejor el límite de control (1 MiB) del de blobs.

DoD: test que lanza N subidas concurrentes al techo y verifica que el RSS agregado queda acotado (mide /proc/self/status, cota declarada) en vez de crecer linealmente con N. Golden (concurrencia normal pasa) + edge (en la cota) + error (exceso → 429/503 sin OOM).

0005d — N4 (Medio): forzar TLS del control plane en bind público

Hallazgo: el guard validateBootConfig cierra "público sin enforce" y "TLS sin enforce", pero permite público + enforce sin --tls-cert → el control plane sirve HTTP plano públicamente (reaparece H5: metadata en claro).

Fix: el guard debe exigir --tls-cert/--tls-key cuando el bind no es loopback. public + enforce + sin TLSlog.Fatal.

DoD: portar TestGap_PublicEnforceNoTLS → ahora validateBootConfig("0.0.0.0", enforce, "", "") rechaza. Golden (público+enforce+TLS OK) + edge (loopback sin TLS sigue OK para dev) + error (público sin TLS aborta).

0005e — H4 (Medio, residual): evaluar y completar la ACL por subject

Contexto: 0003 añadió pkg/membership/acl.go. Primero evaluar con el ataque del report 0006 (TestReaudit_H4_WildcardMetadataLeak: un registrado no-miembro con Subscribe(">") raw capta subjects + advisories de JetStream de rooms ajenas) si ese acl.go ya lo cierra.

  • Si lo cierra → portar el test como regresión y documentar.
  • Si NO (probable: la ACL real necesita NATS Permissions por identidad a nivel del authenticator/cuenta, no solo lógica de membership en el control plane) → implementar las Permissions por identidad derivadas de pertenencia, o documentar el límite y el plan.

DoD: TestReaudit_H4_WildcardMetadataLeak → el no-miembro ya NO capta los subjects de rooms ajenas (o, si queda residual, está documentado con su límite exacto).

Fuera de alcance (otros issues)

  • H9 (cuota/GC de blobs) → issue 0002; se solapa con 0005c (streaming a disco).
  • H10 (AEAD nonce) / H11 (nonce/ts en firma de owner) → bajo, futuro.
  • H8 (custodia de la CA: generar en om) → operacional del deploy.
  • Auditoría de la superficie nueva de 0003 (cluster routes auth, jetstreamStore KV fail-closed, nonce-cache replicado, failover) → el report 0006 NO la cubrió (auditó pre-0003). Pendiente una re-auditoría dedicada de 0003 (prompt ya preparado).

Definition of Done global

  • govulncheck ./... → 0 alcanzables.
  • Los tests adversariales de la re-auditoría (TestReaudit_SigNilSpoof, TestGap_PublicEnforceNoTLS, TestReaudit_H4_WildcardMetadataLeak, DoS-concurrencia) portados como regresión y en verde (o el residual documentado).
  • CGO_ENABLED=0 go build ./... && go vet ./... && go test ./... verdes (incluido el e2e multi-nodo de 0003, para confirmar que el bump de nats-server no lo rompió).
  • Re-evaluación: el veredicto de exposición pública pasa de "NO-aún" a "sí-con-condiciones".