dec-0119

Vista generada de dec-0119: spire, punto único de seguridad de las comunicaciones

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0119-spire-punto-unico-seguridad-comunicaciones.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0119-spire-punto-unico-seguridad-comunicaciones.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoLOCKED
Fecha2026-09-28
Ficherodocs/decisions/dec-0119-spire-punto-unico-seguridad-comunicaciones.md

Enmienda a: r07, r18, dec-0005, dec-0032

Por qué importa (del frontmatter del ADR):

Fija que toda comunicación entre procesos de Styx (in-process, unix socket Bun<->daemon, NATS contenedor<->contenedor) pasa por spire (SDK Zig+TS) y que spire implementa UNA vez el threat model de comunicaciones: identidad de servicio, authz deny-by-default, verificación de capabilities en spire-zig, sobres firmados anti-replay, validación de contrato en cada frontera, límites por identidad, audit y fuzzing. El ADR del SDK spire (tanda SDK) y el AppSec final lo usan como lista de requisitos.

Nodos del roadmap que lo citan en refs: track/identity/sec, track/byte-runtime/sec, track/spire

Páginas de la documentación que lo citan: Arquitectura (implementado), Protocolos binarios (implementado), Comunicaciones entre procesos (implementado), Seguridad del data plane (implementado), Modelo de amenazas (implementado), Sesiones, BFF y autorización (implementado), Bus y contratos (implementado), Capacidades SCT (implementado), zkit, spire y conduit (implementado), Subir ficheros (especificado)

Texto del ADR

Leído de docs/decisions/dec-0119-spire-punto-unico-seguridad-comunicaciones.md, el fichero canónico.

dec-0119 — spire, punto único de seguridad de las comunicaciones

  • Fecha: 2026-09-28
  • Estado: LOCKED. Decisiones de waxin del 2026-09-28 (notas de entorno de la tanda 3):
    • "spire = SDK de mensajería UNIFICADO de styx (Zig+TS, bindings generados desde @styx/api-contracts) con transportes enchufables: in-process, unix socket (Bun<->daemon control IPC) y NATS JetStream … NATS SE QUEDA como broker (dec-0005/dec-0032)".
    • "spire = PUNTO ÚNICO DE SEGURIDAD de comunicaciones (threat model comms implementado una vez, heredado por todos) …", con la lista que concreta este ADR.
    • "Diseñar ADR threat model (tanda 3) y SDK spire JUNTOS": este ADR es la mitad de seguridad. La mitad de API/framing/codegen es el ADR del SDK en la tanda SDK, y debe cumplir esta.
  • Par: dec-0117 (data plane) y dec-0118 (web/TS). Ambos delegan aquí todo lo que cruza un proceso.
  • Enmienda:
    • r18 (bus cmd/qry/evt + MessageEnvelope<T>): el sobre gana campos de seguridad (§5) y el bus deja de ser de confianza implícita.
    • r07 (FFI order: daemon sobre unix socket): el socket lo sirve el transporte unix de spire, con autenticación de peer (§3.2).
    • dec-0005/dec-0032 (NATS como broker): se mantiene NATS y se añade identidad por servicio (NKeys/JWT) y ACL por subject.

Contexto (verificado en 54e997a)

  • NATS arranca sin autenticación (deploy/docker-compose.infra.yml:36-43). Cualquier proceso con red hacia nats:4222 publica en cualquier subject y responde a cualquier qry.*, por ejemplo a qry.identity.session (apps/identity-svc/src/transport/nats/session-query.ts:48), que es un oráculo de validez de tokens.
  • Cada servicio tiene su propio código NATS hecho a mano (apps/*/src/service/nats/*.ts, apps/*/src/transport/nats/*.ts) y valida el sobre por su cuenta.
  • El IPC Bun↔daemon es un protocolo JSON propio sobre un socket sin autenticar (native/zig/media-daemon/ipc/control.zig:1-30,82).
  • El daemon no publica en NATS: hoy no hay spire-zig.

Si cada servicio implementa identidad, authz, anti-replay y límites por su cuenta, habrá tantas variantes como servicios, y el AppSec tendría que auditar N implementaciones. Con spire se audita una.

Qué se decide

1. Todo lo que cruza un proceso pasa por spire

  • Transportes: inproc, unix y nats. Los tres llevan el mismo sobre, las mismas políticas y los mismos límites.
  • Prohibido abrir conexiones NATS o sockets de control fuera de spire. En TS lo vigila un guard que prohíbe importar nats/@nats-io/* fuera de @spire/*. En Zig, el audit de dec-0117 §6 prohíbe sockets unix crudos fuera del transporte de spire.
  • Los bytes de vídeo nunca pasan por spire (r01). spire lleva control, eventos y consultas.

2. Modelo de amenazas de las comunicaciones

AmenazaEjemploCierre
S: un proceso se hace pasar por un serviciopublicar cmd.playback.* como si fuera la webidentidad de servicio (§3) + firma del sobre (§5)
T: modificar un mensaje en tránsito o en JetStreamcambiar el actorId de un comandofirma sobre cabecera + digest del payload (§5)
R: negar un comandoadmin que borra una bibliotecasobre firmado + audit (§8)
I: leer subjects ajenosun servicio escucha evt.identity.* sin necesitarloACL de subscribe por servicio (§3.1)
D: flood, mensajes gigantes, consumidor lentoreventar un handler con payloads de 1 GiBlímites por identidad y por subject (§7)
E: confused deputyun servicio usa su propia autoridad para hacer lo que el usuario no puedeel actor viaja como JWT verificable, no como claim del emisor (§5.2), y se evalúa la policy del actor (§4)
Replayreenviar un cmd capturadots + nonce + caché anti-replay (§5.3)

3. Identidad de servicio

3.1 NATS

  • Modo operador de NATS con JWT descentralizado (nsc): un account Styx y un user por servicio, con NKey propia. La semilla se monta como fichero de secreto de sólo lectura.
  • Permisos pub/sub por user generados desde el registro de contratos: los subjects que el servicio declara como handler o cliente en @styx/api-contracts. allow_responses limitado para qry.*. Nada a mano: hay un test que regenera la ACL y hace diff contra la desplegada.
  • Deny por defecto: un user sin permiso explícito no publica ni suscribe nada. JetStream con límites por account (almacenamiento, streams, consumers).
  • qry.identity.session y qry.identity.browserSession sólo los pueden publicar los servicios que los declaran como cliente (web BFF, playback, catalog…).

3.2 Unix socket (Bun ↔ daemon)

  • Directorio 0750, socket 0660 y grupo dedicado (dec-0117 §4.2).
  • SO_PEERCRED/getpeereid en cada accept: uid/gid en la allowlist.
  • Y handshake de spire: el daemon envía un reto de 32 bytes y el peer responde con Ed25519(clave de servicio, reto ‖ identidad del daemon ‖ versión). Sin handshake válido en 2 s se cierra la conexión sin procesar ningún frame. PEERCRED solo no basta: en contenedores con uids compartidos o mapeados, el uid no identifica al servicio.

3.3 In-process

Sin criptografía (mismo proceso), pero misma policy y misma validación de contrato. Así mover un handler fuera de proceso no cambia su semántica ni abre un hueco.

4. Authz deny-by-default en el SDK

  • Registrar un handler exige { contrato, policy, handler }, y policy no tiene default:
    • Zig: spire.register(comptime Contract, comptime policy: Policy, handler). Si falta la policy es un error de compilación, porque el parámetro no es opcional y no hay Policy.none.
    • TS: defineHandler({ contract, policy, handle }). El tipo exige policy, y spire.start() no arranca si algún subject suscrito no tiene policy en el registro. Un test lo cubre con un mutante que quita una policy.
  • La policy evalúa dos cosas: (a) la identidad de servicio emisora está en la allowlist del subject, y (b) si el mensaje actúa en nombre de un usuario, can(actor, acción, recurso) con el motor único de dec-0118 §3.
  • Los clientes también declaran lo que envían: un servicio no puede publicar un subject que no figure en su contrato de cliente. Lo aplican la ACL de NATS y el propio SDK.

5. Sobre seguro

5.1 Campos

MessageEnvelope v2, que se codifica igual en TS y en Zig, con vectores de conformidad:

CampoUso
idUUIDv7 del mensaje (idempotencia y dedupe)
subject, contractVersionenrutado y versión del contrato (r18)
sourceidentidad de servicio emisora (debe coincidir con la NKey o el peer autenticado)
actor?access JWT del usuario en nombre del que se actúa (§5.2)
ts, nonceanti-replay (§5.3)
deadlinepropagación de deadline
correlationId, causationId, traceparentcorrelación y trazas
kid, sigEd25519 de la clave del servicio sobre la codificación canónica de la cabecera + SHA-256(payload)

5.2 Actor

Un servicio no afirma quién es el usuario: reenvía el access JWT (dec-0113 §1) y el receptor lo verifica contra el JWKS de identity-svc (offline, con revocation-aware para mutaciones). Esto cierra el confused deputy: un servicio comprometido no puede fabricar un actor. Como mucho reutiliza JWTs de vida corta que ya le han llegado.

5.3 Anti-replay

  • ts dentro de ±30 s del reloj del receptor (reversible) y nonce de 128 bits.
  • Caché anti-replay por (source, nonce) durante la ventana. Es local en memoria para unix/inproc, y en Valkey para nats con varias réplicas consumidoras.
  • JetStream: una redelivery legítima trae el mismo id y el mismo nonce. El consumidor la trata como duplicado idempotente (dedupe por id), no como ataque. Un nonce repetido con otro id sí es replay: se rechaza y se audita.

5.4 Coste

El control plane mueve mensajes pequeños a baja frecuencia (los bytes de media van fuera). Una firma y una verificación Ed25519 por mensaje entran en el presupuesto. El ADR del SDK debe medirlo (p50/p99 por mensaje en TS y Zig) antes de cerrar su gate. Si alguna ruta caliente (señales de transporte a alta frecuencia) no cabe, puede usar MAC de sesión (HMAC con clave derivada en el handshake de §3.2) sólo en unix, y eso queda registrado en el audit de configuración.

6. Validación de contrato en cada frontera

  • Entrada y salida: el SDK valida el payload contra el validador generado de @styx/api-contracts (TypeBox compilado en TS, dec-0116; codec Zig generado desde el mismo JSON Schema). La salida también se valida: así se atrapan bugs propios y no se filtran campos internos.
  • Fuente de verdad: @styx/api-contracts (r18, notas de la tanda 3: "codegen en spire pero fuente de verdad = @styx/api-contracts").
  • El codec Zig es un parser de entrada no confiable: cumple dec-0117 I5 (BoundedReader, aritmética checked, sin unreachable, ReleaseSafe, fuzz).

7. Límites por identidad

  • Tamaño máximo por subject, declarado en el contrato. Por defecto 64 KiB y techo de 1 MiB (el mismo que el frame IPC actual).
  • Rate por (source, subject) con token bucket, y concurrencia máxima de handlers por subject.
  • Backpressure: en NATS, max_pending del consumer, y un consumer lento se desconecta y se audita. En unix, frames en vuelo por peer.
  • Deadline: un mensaje que llega caducado se descarta sin ejecutar el handler.

8. Audit

spire emite evt.security.comms.* (firmado, como todo) y logs OTel con atributos security.* para: handshake fallido, firma inválida, kid desconocido, replay, denegación de policy, violación de contrato (entrada o salida), límite superado y subject no autorizado. Los campos son los de dec-0118 §9. Nunca payloads ni tokens.

9. Capabilities en spire-zig

  • spire-zig incluye el verificador de SCT de dec-0117 §4.1 (Ed25519 de std.crypto, formato binario fijo, kid con rotación, aud, nbf/exp con skew, jti de un solo uso para publish/ingest). Lo usan los transportes WT/MoQT/H3 del daemon y la ingesta. No hay otra implementación en el daemon.
  • La emisión vive en spire-ts (@spire/…), la usa sólo playback-svc y la clave privada es de playback-svc (dec-0117 A4).
  • Vectores de conformidad compartidos: SCT válidas, caducadas, con otro aud, con otro kid, con firma alterada, truncadas y con campos fuera de rango. TS y Zig deben dar el mismo veredicto en todas.

10. Fuzzing y conformidad

  • Fuzz del codec Zig (sobre, SCT y handshake) con zig build test --fuzz y corpus versionado.
  • Property tests del codec TS (fast-check).
  • Fuzz diferencial TS ↔ Zig: la misma entrada tiene que dar el mismo veredicto (acepta o rechaza, con el mismo código) y el mismo valor decodificado.
  • Mínimo 10 min por target en el gate del SDK y en el AppSec final (misma regla que dec-0117 I5).

Qué fija el lock y qué queda reversible

  • Fija el lock: todo lo que cruza un proceso va por spire; identidad por servicio en los tres transportes (NKey/JWT con ACL generada; PEERCRED + handshake firmado en unix); policy obligatoria (no compila en Zig, no arranca en TS); sobre firmado con anti-replay; actor como JWT verificable; validación de entrada y salida desde @styx/api-contracts; límites por identidad; verificador de SCT único en spire-zig; audit; fuzz diferencial.
  • Reversible sin ADR: ventana de skew, tamaños por defecto, algoritmo del MAC de sesión si hiciera falta, nombres de paquetes y API concreta (los fija el ADR del SDK).

Lo que este ADR NO decide

  • La API, el framing y el codegen de spire, el patrón snapshot+delta ni el orden de migración de servicios: ADR del SDK spire (tanda SDK, siguiente número libre ≥ dec-0120).
  • Qué transporte usa cada par concreto de servicios: lo decide el SDK con los consumidores reales (criterio load-bearing de las notas de la tanda 3).

Plan

Los tickets que dependen de spire están en los dos planes de seguridad: docs/track/byte-runtime/plans/security-part1-data-plane.plan.md (spire-zig, SCT, unix) y docs/track/identity/plans/security-part2-web-identity.plan.md (NATS NKeys/ACL, spire-ts).