Issue 0004 (security hardening) done across 0004a-0004f. app.md version 0.5.0
with the capability growth log entry; dev/0004d-dataplane-acl.md documents the
chosen minimum-defense strategy for the NATS data plane and its residual limit
(per-subject ACL deferred to 0003). Full work report in
projects/message_bus/reports/0005-2026-06-07-unibus-security-hardening.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bus de mensajería unificado sobre NATS+JetStream con cifrado E2E por room (megolm/olm reducido): service de membresía/claves, librería cliente y peers demo.
unibus es un bus de mensajería unificado sobre NATS + JetStream. Una capa fina
encima de NATS aporta lo que NATS no tiene: membresía de rooms, invitaciones con
reparto de clave, y cifrado extremo a extremo (E2E) del payload por room, con
rotación de clave activa (forward secrecy) al expulsar a un miembro.
Todos los participantes —procesos/workers, interfaces de chat humanas, agentes
LLM— son peers de primera clase que hablan el mismo protocolo y se diferencian
solo por las rooms a las que se unen y por lo que hacen con lo que reciben.
Piezas
Componente
Ruta
Rol
membershipd
cmd/membershipd
Service (control plane): metadata de rooms, directorio de miembros, reparto de claves selladas, object store de media. Arranca NATS embebido si no se pasa --nats-url.
librería cliente
pkg/client
La API que consume cualquier peer. Crear/unirse a rooms, invitar, publicar/suscribir, request/reply, kick con rotación de clave, media cifrada.
worker
cmd/worker
Peer demo: publica un contador incremental cada segundo a una room cleartext.
chat
cmd/chat
Peer demo: suscriptor en vivo (modo simple) + demo de cifrado E2E y forward secrecy (--demo-encrypted).
Dos planos
Control plane: HTTP autoritativo en membershipd. Quién está en cada room,
sus claves públicas, y la clave de room K sellada por epoch para cada miembro.
Data plane: NATS. Los mensajes (frames) viajan por subject; los blobs de
media NO viajan por el bus, se cifran y se suben al object store, y por NATS
solo viaja una referencia (hash + nonce).
Criptografía (importada del registry, NO reescrita)
El cifrado E2E se compone de las 7 primitivas del capability group
e2e-messaging (docs/capabilities/e2e-messaging.md), importadas del paquete
fn-registry/functions/cybersecurity. Esta app no reimplementa ninguna
primitiva criptográfica.
Ejemplo
Demo end-to-end con go run (NATS embebido, nada que instalar):
cd projects/message_bus/apps/unibus
# 1. Service de membresía/claves (NATS embebido en :4250, HTTP en :8470)
go run ./cmd/membershipd
# 2. En otra terminal: peer publicador (proceso) — publica ticks cada 1s
go run ./cmd/worker
# 3. En otra terminal: peer suscriptor (chat humano) — imprime cada tick en vivo
go run ./cmd/chat
# 4. Demo de cifrado E2E + forward secrecy (contra el membershipd ya corriendo):# A crea room cifrada, invita a B, A publica (B descifra), A expulsa a B,# A publica de nuevo en el nuevo epoch (B ya NO puede descifrar).
go run ./cmd/chat --demo-encrypted
Para apuntar a un NATS externo en producción: --nats-url nats://host:4222 en
membershipd, worker y chat.
Cuando usarla
Cuando necesites un tejido de mensajería donde procesos, humanos y agentes LLM
sean peers uniformes (mismo protocolo, distinta política por room).
Cuando quieras rooms cifradas E2E con forward secrecy (paridad con Matrix) sin
montar un Synapse: room.ModeMatrix.
Cuando quieras fan-out cleartext rápido para telemetría/coordinación de
procesos: room.ModeNATS.
Como sustituto de la capa de transporte Matrix de agents_and_robots (fase
posterior; v1 valida el bus de forma autónoma).
Gotchas
El service NO está endurecido (v1). No hay TLS, ni rate-limit, ni auth en
las rutas GET de lectura. Confía en la red interna. Las rutas mutantes
(/rooms, /invite, /rekey) sí exigen firma Ed25519 del owner sobre los
bytes canónicos de la request. Endurecer es fase posterior.
Identidad = secreto crítico. El archivo de identidad (worker.id,
chat.id) contiene las claves privadas (Ed25519 + X25519). Se escribe 0600.
Perderlo = mensajes ilegibles, sin recuperación. Trátalo como una clave SSH.
Las rooms reciben un ULID fresco al crearse. No hay "crear o unirse por
nombre": cada CreateRoom produce un room nuevo. Los peers demo cleartext
comparten el subject (NATS enruta por subject), así que worker→chat funcionan
aunque cada uno tenga su propio room id mapeado al mismo subject.
La media no viaja por el bus.PublishMedia cifra, sube al object store y
publica solo un BlobRef. El receptor, si ve Frame.Blob != nil, descarga y
descifra con FetchMedia. El frame de media NO lleva payload inline (su nonce
vive en BlobRef.Nonce); Subscribe no intenta descifrar payloads vacíos.
Forward secrecy depende del rekey.Kick rota K a un epoch nuevo y la
re-sella solo para los miembros restantes. El expulsado pierde acceso a los
mensajes publicados después del kick, pero conserva los anteriores (las claves
de epochs pasados no se borran: cifraban datos que ya podía leer).
NATS embebido escribe JetStream en disco.--nats-store apunta a
local_files/jetstream; borrarlo resetea el historial persistido.
Build sin CGO. Usa el driver modernc.org/sqlite (pure-Go) y el paquete
cybersecurity del registry compila limpio con CGO_ENABLED=0. NO requiere
fts5 ni gcc.
Convención de subjects
proc.<svc>.<canal> telemetría/coordinación de procesos (proc.test.ticks)
rpc.<svc> request/reply (rpc.indexer)
room.<grupo> chat humano/grupo (room.general)
agent.<nombre>.{in,out} inbox/outbox de agente LLM (agent.scout.in)
Capability growth log
v0.5.0 (2026-06-07) — hardening de seguridad (issue 0004) que cierra los
hallazgos de la auditoría red-team (report 0004) y lleva el veredicto de
exposición pública de "NO" a "sí-con-condiciones". Anti-DoS pre-auth
(http.MaxBytesReader por ruta + rechazo por Content-Length + rate-limit
por IP + MaxHeaderBytes); guard de fail-open que prohíbe arrancar con bind
público o TLS sin --bus-auth enforce; autorización por pertenencia en los GET
de room (metadata y clave sellada solo para miembros / el propio endpoint);
rooms cleartext deshabilitadas en bind público (contenido siempre E2E, mínimo
defensivo del data plane mientras la ACL por subject llega con 0003); TLS en el
control plane HTTP con la CA propia y cliente que exige https cuando hay CA;
y los medios H6/H7/H12 (owner ligado al firmante, IsAuthorized antes del
nonce-cache con poda O(expired) + cap, errores genéricos al cliente). Cada
hallazgo lleva su test adversarial TestAudit_* portado como regresión.
v0.4.0 (2026-06-07) — descubrimiento de rooms: GET /members/{endpoint}/rooms
lista las rooms de un endpoint con su metadata y rol, y client.ListMyRooms()
lo consume. El control plane es pull (no hay push de invitaciones), así que un
peer recién invitado a una room cifrada la descubre por polling y luego hace
Join + Subscribe. Pieza base para que los bots de agents_and_robots
hablen por el bus en vez de Matrix (modelo "todo son rooms", E2E).
v0.3.0 (2026-06-06) — membershipd se convierte en service de verdad: flag
--bind (default 127.0.0.1) que gobierna a la vez el HTTP de control y el NATS
embebido (embeddednats.StartHost), de modo que con --bind 0.0.0.0 un
teléfono o PC de la LAN conecta a ambos planos. Se añade un systemd-user unit
(deploy/unibus-membershipd.service, Restart=always) + deploy/install.sh
idempotente, y el bloque service: queda completo (systemd-user, restart
always, health /healthz). El Frame (pkg/frame) gana threading aditivo
(ThreadID, ReplyTo) y un tipo REACT, con PublishReply/React en el
cliente — la base para que bots de chat hablen por el bus (fase 2). Cambios
100% aditivos: el wire de los frames no-threaded es idéntico y los tests
existentes siguen verdes.
v0.2.0 (2026-06-03) — el playground gana un benchmark de rendimiento
(GET /api/bench, SSE): un publisher inunda una room con miles de mensajes a
N subscribers y una gráfica en vivo anima el throughput. Expone las dos
políticas como flags independientes (JetStream/Persist y encriptación
E2E/Encrypt) más tamaño de payload, de modo que se mide el coste de cada
capa (core NATS vs JetStream vs E2E vs E2E+JetStream) usando la librería
cliente real, sin reimplementar nada.