docs(issue): 0005 hardening 2 — CVEs, sig-nil spoof, DoS concurrencia, TLS forzado (re-auditoría 0006)
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
---
|
||||
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ó H1–H7/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".
|
||||
Reference in New Issue
Block a user