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.
11 KiB
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:
- Sistema de usuarios — un registro a nivel bus de las identidades autorizadas (allowlist de claves públicas Ed25519), con roles y revocación.
- 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.
- 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_pubes la misma clave que ya deriva elendpoint(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
adminse 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.signRequest ↔
pkg/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):
- Parsear cabeceras; reconstruir
canonical; verificar firma conX-Unibus-Pub. - Comprobar
IsAuthorized(pub)(status active). Si no →401. - Anti-replay: rechazar si
|now - ts| > 30s; cachearnoncecon TTL 60s y rechazar repetidos (LRU en memoria, suficiente para un único membershipd). - 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.CustomClientAuthenticationcon 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 consultastore.IsAuthorized(pub). Validar dinámicamente contra la BD permite revocar sin reiniciar el server (ventaja sobre precargarOptions.Nkeys). - Cliente (
pkg/client): conectar connats.Nkey(pubSeedEncoded, sigCB)dondesigCBfirma el nonce con la Ed25519 del peer. Convertircs.Identity→ formato nkey congithub.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.TLSConfigcon el server cert. NATS pasa atls://.- 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 elca.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)
- 0001a — users store + CLI — migración
002_users.sql, CRUD en store, comandosmembershipd user *, seed admin. Flagbus-auth: off. Tests de store. - 0001b — control-plane auth — firma generalizada en
pkg/client, middleware de verificación + anti-replay enmembershipd. Flagbus-auth: soft. Tests: request firmado OK, no-autorizado 401, replay rechazado, reloj desfasado 401. - 0001c — NATS nkey auth —
CustomClientAuthentication+ cliente connats.Nkey. Tests: peer no registrado rechazado al conectar; revocado pierde acceso sin reiniciar. - 0001d — TLS NATS — generación de CA/cert (
deploy/tls/+ script), serverTLSConfig, clienteRootCAs. Flagbus-tls. Test: handshake TLS, cliente sin CA rechazado. - 0001e — migrar clientes —
mobile/(binding), gateway web (playground/),unibots(shell/transportunibus): todos firman requests y conectan con nkey+TLS. Pasarbus-authaenforce. - 0001f — deploy — unibus en om (bind
10.42.0.1o 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)
- Cross-build
CGO_ENABLED=0 GOOS=linux GOARCH=amd64delmembershipd. scpbinario +ca.crt+ server cert/key a om (/opt/unibus/), dir de datos persistente para JetStream/db/blobs.- systemd-system unit,
--bind 0.0.0.0(exposición pública),Restart=always. ufw allow 8470/tcpyufw allow 4250/tcpen om.- Seed del admin (tu identidad) por CLI local en om.
- Verificar desde fuera de la VPN (red pública) y desde la WG: handshake TLS,
curlfirmado a/healthzOK,curlsin firma → 401, conexión NATS de un peer no registrado → rechazada. - 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 0001a–0001e (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.