ExplicacionSeguridad

Sesiones, BFF y autorización

Cómo el navegador no tiene tokens, qué protege la cookie de sesión, cómo se autoriza cada ruta y qué se registra en la auditoría.

Implementado en parte. El diseño es dec-0118. Qué piezas están verificadas se lee en el gate de track/identity/sec. El modelo de tokens para clientes nativos es todavía un ADR propuesto, fuera de esta página.

El navegador no tiene tokens

El único origen de control plane que ve el navegador es la web (TanStack Start). Sus funciones de servidor y un proxy de /auth/* hacia identity-svc son la única superficie: los demás servicios no se exponen al navegador.

  • El navegador no guarda ningún token: nada en localStorage, sessionStorage, IndexedDB, en memoria JS ni en cookies legibles por JS. Su única credencial es una cookie de sesión HttpOnly.
  • El BFF resuelve esa cookie contra identity-svc (con la revocación en cuenta) y obtiene un JWT de acceso de vida corta que nunca sale del servidor. Con él llama a los servicios en nombre del usuario.
  • Un XSS no puede exfiltrar credenciales; como mucho actúa mientras la página está abierta, y CSP con Trusted Types lo dificulta.
  • El origen de media no usa cookies: el BFF entrega al navegador una capacidad SCT por sesión de reproducción, que es una capacidad acotada y no una credencial de usuario.

Login

El flujo de entrada es OIDC con PKCE contra un proveedor estándar (Pocket ID de referencia). GET /auth/oidc/start es anónimo y no escribe nada en el servidor: el login pendiente (el state, el nonce, el verificador de código y el destino) viaja sellado con AES-256-GCM en una cookie de corta vida, y sólo el callback escribe una marca de uso único. Así el endpoint anónimo no consume un recurso del servidor que un grupo de clientes pudiera agotar.

Cookies

CookieAtributosContenido
__Host-styx_sessHttpOnly; Secure; SameSite=Strict; Path=/, sin DomainCredencial opaca con secreto de 256 bits; en el servidor sólo se guarda su hash
__Host-styx_loginHttpOnly; Secure; SameSite=Lax, vida del login pendienteEl login pendiente sellado (el callback llega como navegación entre sitios)

Secure es obligatorio también en desarrollo (con TLS interno): no se admite HTTP en local.

Rotación, caducidad y revocación

  • La credencial rota al iniciar sesión, al cambiar de privilegios y periódicamente. Presentar una credencial ya rotada fuera de la ventana de gracia revoca la sesión entera y genera un evento de auditoría de severidad alta.
  • Caducan por inactividad (deslizante) y de forma absoluta. Las operaciones sensibles piden un inicio de sesión reciente.
  • La revocación es en el servidor (Valkey): por sesión, por actor (cierre de sesión global) y por administrador, con tope de sesiones por actor. Un JWT de servicio ya emitido sigue valiendo hasta su caducidad, que es corta; las mutaciones comprueban la sesión con la revocación en cuenta.
  • El cierre de sesión borra en el servidor y responde Clear-Site-Data.

Los clientes nativos usan un refresh opaco rotatorio en el almacén seguro de la plataforma y no cookies, así que no tienen CSRF.

CSRF: dos barreras independientes

Toda petición que cambia estado y lleva la cookie debe pasar las dos:

  1. Contexto de navegador: SameSite=Strict y comprobación en servidor de Sec-Fetch-Site: same-origin (o, si falta, Origin idéntico al origen de la web).
  2. Token sincronizador: la cabecera X-Styx-CSRF con un HMAC de la sesión, que el SSR entrega a la página; no sirve sin la cookie y no se puede fabricar desde otro sitio.

Las funciones de servidor GET no cambian estado. Especificado, pendiente: todavía no hay un test que falle si una ruta GET declara efectos; lo que existe hoy es el tope de cuerpo por método de apps/web/src/server/request-guard.ts.

Autorización deny-by-default

  • Cada ruta Elysia declara su policy (public en una lista explícita, authenticated, role:… o resource:tipo:acción). Una ruta sin policy deniega. En cada servicio, un test recorre las rutas y falla si falta alguna o si una public no está en la lista versionada.
  • Especificado, pendiente de SW10/SW13 (gate de track/identity/sec, hoy partial): las funciones de servidor de la web se crearán sólo con un envoltorio, y un guard de lint prohibirá crearlas de otra forma. Hoy no existen ni el envoltorio ni el guard: los módulos de apps/web/src/server/*.ts llaman a createServerFn directamente.
  • Los roles son owner, admin, member y restricted. Un rol es un conjunto de permisos, no un if en un handler.
  • IDOR cerrado por construcción: todo identificador que llega del cliente se resuelve antes del handler a un recurso con dueño y se autoriza. El handler recibe un valor Authorized que sólo el motor de políticas fabrica, así que no puede consultar con un id crudo. Un recurso de otro actor responde 404, no 403, para no confirmar su existencia.
  • Los alcances son own, shared y any. restricted sólo tiene lo propio y no lee la biblioteca hasta que un propietario le conceda bibliotecas.
  • Un único motor (@styx/authz) decide para HTTP y para el bus.

CORS, CSP y errores

  • Los servicios no montan CORS (el navegador no les habla). El origen de media admite CORS sólo para los orígenes de la web, sin credenciales.
  • Las respuestas HTML del SSR llevan una CSP con nonce y strict-dynamic, Trusted Types y cabeceras de aislamiento (apps/web/src/server/security-headers.ts). Especificado, pendiente de SW13: SRI en los scripts y hojas de estilo todavía no está implementado. El contenido remoto (metadatos, subtítulos, plugins) es no confiable hasta llegar al DOM.
  • Los errores son application/problem+json (RFC 9457), sin eco de la entrada, sin pila y sin mensajes de librerías en producción; llevan un instance que enlaza con la traza.

Auditoría

Los servicios emiten eventos evt.security.* firmados por spire: inicio y fallo de sesión, cierre, rotación, reutilización detectada, revocación, denegación de policy, CSRF rechazado, límite de tasa, emisión y rechazo de SCT, cambios de rol y de fuentes con secretos. Se guardan de forma sólo-añadir en un esquema propio y se exportan como logs OTel con atributos security.*. Las agregaciones por «IP» usan la misma unidad de cliente que el rate-limit (IPv4 completa, IPv6 por /64).

Mínimo privilegio en datos

Un rol de Postgres por servicio (sin privilegios de superusuario, con un rol de migraciones separado), ACL de Valkey por servicio y secretos en ficheros montados de sólo lectura, no en variables de entorno. Las claves de firma se publican con kid y rotan con solape.