Vista generada de dec-0119: spire, punto único de seguridad de las comunicaciones
docs/decisions/dec-0119-spire-punto-unico-seguridad-comunicaciones.mdVista generada desde
docs/decisions/dec-0119-spire-punto-unico-seguridad-comunicaciones.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED |
| Fecha | 2026-09-28 |
| Fichero | docs/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)
Leído de docs/decisions/dec-0119-spire-punto-unico-seguridad-comunicaciones.md, el fichero canónico.
dec-0117 (data plane) y dec-0118 (web/TS). Ambos delegan aquí todo lo que cruza un
proceso.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.54e997a)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.apps/*/src/service/nats/*.ts,
apps/*/src/transport/nats/*.ts) y valida el sobre por su cuenta.native/zig/media-daemon/ipc/control.zig:1-30,82).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.
inproc, unix y nats. Los tres llevan el mismo sobre, las mismas políticas
y los mismos límites.nats/@nats-io/* fuera de @spire/*. En Zig, el audit de dec-0117 §6
prohíbe sockets unix crudos fuera del transporte de spire.| Amenaza | Ejemplo | Cierre |
|---|---|---|
| S: un proceso se hace pasar por un servicio | publicar cmd.playback.* como si fuera la web | identidad de servicio (§3) + firma del sobre (§5) |
| T: modificar un mensaje en tránsito o en JetStream | cambiar el actorId de un comando | firma sobre cabecera + digest del payload (§5) |
| R: negar un comando | admin que borra una biblioteca | sobre firmado + audit (§8) |
| I: leer subjects ajenos | un servicio escucha evt.identity.* sin necesitarlo | ACL de subscribe por servicio (§3.1) |
| D: flood, mensajes gigantes, consumidor lento | reventar un handler con payloads de 1 GiB | límites por identidad y por subject (§7) |
| E: confused deputy | un servicio usa su propia autoridad para hacer lo que el usuario no puede | el actor viaja como JWT verificable, no como claim del emisor (§5.2), y se evalúa la policy del actor (§4) |
| Replay | reenviar un cmd capturado | ts + nonce + caché anti-replay (§5.3) |
nsc): un account Styx y un user por
servicio, con NKey propia. La semilla se monta como fichero de secreto de sólo lectura.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.qry.identity.session y qry.identity.browserSession sólo los pueden publicar los servicios
que los declaran como cliente (web BFF, playback, catalog…).0750, socket 0660 y grupo dedicado (dec-0117 §4.2).SO_PEERCRED/getpeereid en cada accept: uid/gid en la allowlist.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.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.
{ contrato, policy, handler }, y policy no tiene default:
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.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.can(actor, acción, recurso) con
el motor único de dec-0118 §3.MessageEnvelope v2, que se codifica igual en TS y en Zig, con vectores de conformidad:
| Campo | Uso |
|---|---|
id | UUIDv7 del mensaje (idempotencia y dedupe) |
subject, contractVersion | enrutado y versión del contrato (r18) |
source | identidad 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, nonce | anti-replay (§5.3) |
deadline | propagación de deadline |
correlationId, causationId, traceparent | correlación y trazas |
kid, sig | Ed25519 de la clave del servicio sobre la codificación canónica de la cabecera + SHA-256(payload) |
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.
ts dentro de ±30 s del reloj del receptor (reversible) y nonce de 128 bits.(source, nonce) durante la ventana. Es local en memoria para
unix/inproc, y en Valkey para nats con varias réplicas consumidoras.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.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.
@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.@styx/api-contracts (r18, notas de la tanda 3: "codegen en spire pero
fuente de verdad = @styx/api-contracts").BoundedReader,
aritmética checked, sin unreachable, ReleaseSafe, fuzz).(source, subject) con token bucket, y concurrencia máxima de handlers por subject.max_pending del consumer, y un consumer lento se desconecta y se
audita. En unix, frames en vuelo por peer.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.
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.@spire/…), la usa sólo playback-svc y la clave privada es de
playback-svc (dec-0117 A4).aud, con otro kid,
con firma alterada, truncadas y con campos fuera de rango. TS y Zig deben dar el mismo
veredicto en todas.zig build test --fuzz y corpus versionado.fast-check).@styx/api-contracts; límites por
identidad; verificador de SCT único en spire-zig; audit; fuzz diferencial.dec-0120).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).