Files
unibus/dev/issues/0005-security-hardening-2.md
T

132 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 0005
title: Hardening 2 — CVEs, spoof por firma omitida, DoS por concurrencia, TLS forzado (re-auditoría)
status: spec
created: 2026-06-07
domain: security
scope: unibus (go.mod, pkg/client, pkg/membership/server.go, cmd/membershipd/config.go, pkg/embeddednats, pkg/blobstore)
depends_on: 0001, 0004 (cierra los hallazgos NUEVOS de la re-auditoría sobre lo entregado)
blocks: 0001f (deploy público) y 0003f (deploy descentralizado)
source: 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**:
```go
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 TLS``log.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".