Files
unibus/dev/issues/0001-bus-auth-and-tls.md
T
agent 7de05c8591 docs(security): fijar exposición pública en el spec 0001
Decisión del operador: el bus se expone a internet protegido por auth+TLS
(WireGuard pasa a ser una vía más, no la barrera). ufw en om abre 8470/4250;
el server cert lleva SAN con la IP pública 135.125.201.30 + la IP WG 10.42.0.1
+ hostname; los clientes controlados embeben el ca.crt propio (sin Let's Encrypt).
La fase de despliegue 0001f la ejecuta el humano; el agente entrega 0001a-0001e.
2026-06-07 12:15:32 +02:00

11 KiB
Raw Blame History

issue, title, status, created, domain, scope
issue title status created domain scope
0001 Seguridad del bus — sistema de usuarios, auth firmada del control plane, NATS nkey + TLS spec 2026-06-07 security unibus (membershipd, pkg/membership, pkg/embeddednats, pkg/client) + clientes (mobile, web gateway, unibots)

Objetivo

Hoy el bus unibus solo está protegido por la red (WireGuard) y por el cifrado E2E por room (megolm). El control plane (HTTP :8470) y el data plane (NATS :4250) no tienen autenticación ni TLS: cualquiera que alcance esos puertos puede crear rooms, leer metadata, publicar, y hacer DoS. El contenido de las rooms ModeMatrix está cifrado E2E, pero las rooms ModeNATS (cleartext), la metadata de subjects y todo el control plane viajan en claro y sin control de acceso.

Este issue añade tres capas de seguridad al propio bus, de modo que WireGuard pase a ser opcional (defensa en profundidad) y el bus pueda exponerse de forma segura incluso a un cliente móvil en una red ajena:

  1. Sistema de usuarios — un registro a nivel bus de las identidades autorizadas (allowlist de claves públicas Ed25519), con roles y revocación.
  2. Auth del control plane — cada request HTTP va firmado con la identidad del peer; el server verifica la firma y que la identidad esté autorizada.
  3. NATS endurecido — autenticación por nkey (Ed25519) contra el registro de usuarios + TLS para cifrar todo el transporte del data plane.

Modelo de amenazas y capas

Capa Qué protege Estado hoy Tras este issue
WireGuard Acceso de red; oculta el bus de internet activo (opcional) sigue disponible, ya no imprescindible
TLS NATS Confidencialidad/integridad del canal (cleartext rooms, metadata, nonces de auth) ausente CA propia self-signed
Auth (firma Ed25519 / nkey) Autenticación: solo identidades registradas conectan/operan ausente control plane + data plane
E2E por room (megolm) Confidencialidad del contenido de rooms cifradas activo sin cambios

Principio: cada capa es independiente. TLS cifra el canal, la auth decide quién entra, el E2E protege el contenido aunque el bus fuera comprometido.

Diseño

Pieza 1 — Sistema de usuarios

Registro a nivel bus (no por room) de las identidades autorizadas. Migración aditiva migrations/002_users.sql (y su gemela embebida en pkg/membership/migrations/):

CREATE TABLE IF NOT EXISTS users (
  sign_pub   TEXT PRIMARY KEY,           -- clave pública Ed25519 en hex (identidad del peer)
  handle     TEXT NOT NULL,              -- nombre legible (único recomendado, no PK)
  role       TEXT NOT NULL DEFAULT 'member',  -- 'admin' | 'member'
  status     TEXT NOT NULL DEFAULT 'active',  -- 'active' | 'revoked'
  created_at TEXT NOT NULL,
  revoked_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
  • sign_pub es la misma clave que ya deriva el endpoint (frame.EndpointID(SignPub)).
  • CRUD en pkg/membership/store.go: AddUser, GetUser, ListUsers, RevokeUser, IsAuthorized(signPubHex) bool.
  • CLI de administración en cmd/membershipd: membershipd user add --handle h --sign-pub <hex> [--role admin], user list, user revoke <sign-pub>.
  • Bootstrap (chicken-egg): el primer admin se siembra ejecutando el CLI localmente en el host del bus (user add --role admin --sign-pub <tu_pub>). El CLI local se considera de confianza (quien tiene shell en el host ya manda). Sin al menos un admin, los endpoints de gestión de usuarios devuelven 403.

Pieza 2 — Auth del control plane (HTTP :8470)

Generaliza la firma que ya existe (pkg/client.signRequestpkg/membership.verifyOwnerSig) de "solo owner" a "todo request".

Cliente (pkg/client): cada request añade cabeceras:

X-Unibus-Pub:   <sign_pub hex>
X-Unibus-Ts:    <unix seconds>
X-Unibus-Nonce: <16 bytes aleatorios, base64>
X-Unibus-Sig:   Ed25519( canonical )   ; canonical = method "\n" path "\n" ts "\n" nonce "\n" sha256(body)

Server (middleware en membershipd):

  1. Parsear cabeceras; reconstruir canonical; verificar firma con X-Unibus-Pub.
  2. Comprobar IsAuthorized(pub) (status active). Si no → 401.
  3. Anti-replay: rechazar si |now - ts| > 30s; cachear nonce con TTL 60s y rechazar repetidos (LRU en memoria, suficiente para un único membershipd).
  4. Autorización fina: operaciones de gestión de usuarios exigen role=admin; operaciones de room siguen exigiendo ownership donde ya aplica.

Feature flag bus-auth en dev/feature_flags.json con tres estados de rollout: off (sin verificar) → soft (verifica y loguea rechazos pero deja pasar) → enforce (rechaza). Permite migrar clientes sin cortar el servicio.

Pieza 3 — NATS: nkey auth + TLS

Auth (nkey sobre la identidad Ed25519)

Los nkeys de NATS son claves Ed25519, así que reutilizamos la identidad del peer sin material nuevo.

  • Server (pkg/embeddednats): server.Options.CustomClientAuthentication con un autenticador que, dado el nonce que NATS presenta al cliente y la firma que el cliente devuelve, verifica la firma con la pubkey declarada y consulta store.IsAuthorized(pub). Validar dinámicamente contra la BD permite revocar sin reiniciar el server (ventaja sobre precargar Options.Nkeys).
  • Cliente (pkg/client): conectar con nats.Nkey(pubSeedEncoded, sigCB) donde sigCB firma el nonce con la Ed25519 del peer. Convertir cs.Identity → formato nkey con github.com/nats-io/nkeys (nkeys.FromRawSeed(PrefixByteUser, seed)).

TLS (CA self-signed propia)

Exposición DECIDIDA: pública. El bus se expone a internet protegido por auth+TLS (WireGuard pasa a ser una vía de acceso más, no la barrera). En consecuencia: ufw en om abre 8470/tcp y 4250/tcp, y el server cert incluye en su SAN la IP pública de om 135.125.201.30, la IP WG 10.42.0.1 (los peers internos siguen funcionando) y el hostname de om. Los clientes son todos controlados por nosotros (pkg/client, binding móvil, gateway web, unibots), así que embeben el ca.crt propio — no hace falta Let's Encrypt ni un dominio público apuntando al NATS.

  • Generar una CA propia una vez (deploy/tls/ca.{key,crt}), y un server cert para el bus con SAN = 135.125.201.30, 10.42.0.1, hostname de om.
  • pkg/embeddednats: server.Options.TLSConfig con el server cert. NATS pasa a tls://.
  • Cliente: nats.Secure(&tls.Config{RootCAs: caPool}) cargando la CA propia.
  • Las claves privadas (CA key, server key) nunca se commitean: van gitignored y se distribuyen por pass/scp. Solo el ca.crt (público) viaja con los clientes.

Decisiones técnicas

Decisión Elegido Alternativa descartada Razón
Auth NATS CustomClientAuthentication contra tabla users Options.Nkeys estático revocación dinámica sin reinicio
TLS CA self-signed propia Let's Encrypt infra privada, sin dependencia de dominio público apuntando al NATS
Anti-replay control plane timestamp ±30s + cache de nonce nonce emitido por server (round-trip extra) menos latencia, suficiente con un solo membershipd
Material de identidad reutilizar la Ed25519 del peer (firma + nkey) claves separadas por capa una identidad, menos gestión
Rollout feature flag bus-auth off→soft→enforce corte directo no romper clientes en vuelo

Fases (TBD, ramas issue/0001x-*, feature flags)

  1. 0001a — users store + CLI — migración 002_users.sql, CRUD en store, comandos membershipd user *, seed admin. Flag bus-auth: off. Tests de store.
  2. 0001b — control-plane auth — firma generalizada en pkg/client, middleware de verificación + anti-replay en membershipd. Flag bus-auth: soft. Tests: request firmado OK, no-autorizado 401, replay rechazado, reloj desfasado 401.
  3. 0001c — NATS nkey authCustomClientAuthentication + cliente con nats.Nkey. Tests: peer no registrado rechazado al conectar; revocado pierde acceso sin reiniciar.
  4. 0001d — TLS NATS — generación de CA/cert (deploy/tls/ + script), server TLSConfig, cliente RootCAs. Flag bus-tls. Test: handshake TLS, cliente sin CA rechazado.
  5. 0001e — migrar clientesmobile/ (binding), gateway web (playground/), unibots (shell/transportunibus): todos firman requests y conectan con nkey+TLS. Pasar bus-auth a enforce.
  6. 0001f — deploy — unibus en om (bind 10.42.0.1 o público con auth+TLS), unibots como systemd-user en el PC local. Verificación E2E.

Migración de clientes

Todo el cambio se concentra en pkg/client (firma de requests HTTP + conexión NATS nkey+TLS). mobile/, el gateway web y unibots lo heredan al recompilar; solo necesitan pasar la ruta de la CA y su identidad (que ya tienen). El binding gomobile expone un parámetro nuevo caPath en NewSession.

Plan de despliegue (fase 0001f)

  1. Cross-build CGO_ENABLED=0 GOOS=linux GOARCH=amd64 del membershipd.
  2. scp binario + ca.crt + server cert/key a om (/opt/unibus/), dir de datos persistente para JetStream/db/blobs.
  3. systemd-system unit, --bind 0.0.0.0 (exposición pública), Restart=always.
  4. ufw allow 8470/tcp y ufw allow 4250/tcp en om.
  5. Seed del admin (tu identidad) por CLI local en om.
  6. Verificar desde fuera de la VPN (red pública) y desde la WG: handshake TLS, curl firmado a /healthz OK, curl sin firma → 401, conexión NATS de un peer no registrado → rechazada.
  7. unibots local: systemd-user con caPath + identidad registrada.

Nota: la fase de despliegue (0001f: abrir firewall público, scp a om, systemd en el VPS) la ejecuta el humano en coordinación, no el agente autónomo — es una acción outward sobre infraestructura pública. El agente entrega 0001a0001e (código + tests + CA/cert generados) en master de unibus, listos para desplegar.

Tests (DoD: golden + edge + error path, evidencia ejecutable)

  • Golden: peer autorizado crea room, publica y recibe por el bus con auth+TLS activos.
  • Edge: revocar un usuario activo → su próxima conexión NATS y su próximo request HTTP son rechazados sin reiniciar el server.
  • Error path: request con firma válida pero identidad no registrada → 401; conexión NATS con nkey no autorizado → rechazada; cliente sin la CA → fallo de handshake TLS; replay de un request firmado → rechazado.
  • Suite completa CGO_ENABLED=0 go test ./... verde.

Riesgos y mitigaciones

Riesgo Mitigación
Chicken-egg del primer admin seed por CLI local en el host (confianza de shell)
Romper clientes en vuelo al activar auth flag bus-auth off→soft→enforce; migrar clientes en soft
Rotación/caducidad de certs CA propia de larga vida; documentar regeneración del server cert en deploy/tls/README.md
Coste de verificar firma por request Ed25519 verify ≈ µs; despreciable frente a la latencia de red
Conversión Ed25519 → nkey mal hecha test dedicado de ida y vuelta firma/verify nkey antes de tocar el server
Claves privadas filtradas en git CA key / server key gitignored; distribución por pass/scp; solo ca.crt versionado

Fuera de alcance (futuro)

  • Rotación automática de credenciales de usuario.
  • Cuentas/multi-tenant de NATS (un solo account basta hoy).
  • Federación entre buses.