DDR-005 — Autenticación opt-in para el transporte Streamable HTTP¶
Versión: 0.1.0 | Fecha: 2026-09-16 | Estado: Propuesto
Contexto¶
Engrama publica el transporte Streamable HTTP desde 0.12, y desde 0.13.0
cada nodo y cada relación pertenecen a una identidad (org_id, user_id)
con lecturas fail-closed. resolve_scope(), en
engrama/adapters/mcp/server.py, deriva esa identidad por petición a
partir de las cabeceras X-Engrama-Org-Id / X-Engrama-User-Id, y
ENGRAMA_REQUIRE_IDENTITY hace que una petición sin cabeceras falle en
cerrado en lugar de caer en la identidad standalone compartida.
Lo que falta es la otra mitad: nada autentica a quien llama. El
transporte valida Origin y Host como protección anti DNS-rebinding
(engrama/adapters/mcp/http.py) y ahí se acaba. Las cabeceras de
identidad son, por tanto, entrada confiada: correcto cuando lo único que
alcanza al servidor es una pasarela que autentica, y peligroso en
cualquier otra topología, porque quien llegue a /mcp puede declararse
la identidad que quiera.
Eso deja el autoalojamiento remoto como "problema del operador", que es justo de donde viene #121: una plataforma europea se ofreció a publicar una receta de despliegue en un clic para Engrama. La respuesta honesta fue que un despliegue en un clic de un servidor de memoria sin autenticación es un pie de plomo, y que poner un único token estático compartido en el borde convertiría ese token en toda la capa de autenticación de la memoria a largo plazo de alguien. La misma petición reaparece con otras formas: quien quiere una instancia remota privada para un cliente móvil, o un equipo que quiere un solo host de agentes accesible desde varias máquinas.
La preparación ya está en el árbol, a propósito: --auth-issuer /
ENGRAMA_AUTH_ISSUER ya se parsea, y
/.well-known/oauth-protected-resource (RFC 9728) ya devuelve 404 hasta
que se configura un issuer, con un comentario que promete que "la fase
de auth solo tendrá que poner una variable de entorno, no tocar código".
Este DDR es esa fase.
El diseño está acotado por dos restricciones. La autenticación no debe convertirse en una dependencia, un paso de configuración ni un cambio de comportamiento para la mayoría de instalaciones, que son locales y de un solo usuario: por eso todo aquí es opt-in e inerte por defecto. Y Engrama no debe convertirse en un proveedor de identidad: verificar un token entra en el alcance, emitirlo no.
Decisión¶
Añadir dos modos de autenticación opt-in al transporte Streamable HTTP, ambos desactivados por defecto, y dejar de confiar en las cabeceras de identidad siempre que cualquiera de los dos esté activo. El transporte stdio no se ve afectado.
En concreto¶
-
Modo resource server OAuth 2.1.
engrama/adapters/mcp/auth.pyvalida un JWT bearer RS256 contra el issuer configurado: firma (JWKS, obtenido de forma perezosa y cacheado porkid),iss,aud,expy, opcionalmente, un conjunto de scopes requeridos. Engrama es un resource server puro: no emite nada, no registra clientes y no implementa ningún flujo. Cada cual trae su IdP (Keycloak, Authentik, Auth0, Entra, …). -
Configuración nueva, toda sin valor por defecto.
ENGRAMA_AUTH_ISSUER— ya se parsea hoy; ahora, además, activa la validación de tokens, no solo los metadatos de descubrimiento.ENGRAMA_AUTH_AUDIENCE— obligatoria cuando hay issuer.ENGRAMA_AUTH_JWKS_URI— override opcional; si falta se deriva de los metadatos OIDC del issuer.ENGRAMA_AUTH_SCOPES— CSV opcional de scopes requeridos.-
ENGRAMA_AUTH_ORG_CLAIM— claim opcional que nombra la organización de quien llama. -
Claims → identidad.
user_ides siempre el claimsub, que es la frontera de aislamiento por usuario.org_ides el claim de organización cuando está configurado y presente (primer elemento si el claim es una lista), y si nosub, de modo que un despliegue de un solo usuario obtieneorg_id == user_idsin configurar nada. Un token que resolviera cualquiera de los dos valores al centinela reservado__entity__(ámbito compartido de organización) se rechaza de plano: un token nunca debe poder nombrar ese ámbito. -
La confianza en las cabeceras se invierte con auth activa. Con autenticación activada, las cabeceras
X-Engrama-*entrantes se eliminan antes de que la petición llegue a las tools, y se sustituyen por el par derivado de los claims validados. Hay exactamente una autoridad de identidad por despliegue: el token si hay auth, la pasarela que va delante si no. Nunca se mezclan, porque aceptar ambas permitiría a cualquiera falsificar el ámbito de otro inquilino añadiendo una cabecera. -
Modo de token de servicio estático.
ENGRAMA_AUTH_TOKENprotege el endpoint con un único secreto compartido, comparado en tiempo constante, para despliegues monoinquilino o efímeros donde todas las llamadas son el mismo principal. Se mapea a una identidad estable (ENGRAMA_AUTH_TOKEN_SUB, por defecto la identidad standalone). El modo es honesto sobre lo que es: un principal, sin aislamiento por usuario y sin revocación que no sea rotar el secreto. Es mutuamente excluyente con el modo OAuth: configurar ambos falla al arrancar en lugar de dejar la precedencia ambigua. -
Descubrimiento y desafíos. Con issuer configurado,
/.well-known/oauth-protected-resourcesirve el documento RFC 9728 que ya sabe construir, y una petición rechazada responde401conWWW-Authenticate: Bearer resource_metadata="<url>", para que un cliente MCP descubra dónde autenticarse en vez de adivinarlo./healthy/.well-known/*siguen sin autenticación. -
Dependencia opcional. La validación de tokens vive tras un extra nuevo,
engrama[auth](PyJWT + cryptography). La instalación base sigue ligera, y un despliegue que configure issuer sin el extra falla al arrancar con un mensaje accionable. -
Modo de fallo. La mala configuración falla en cerrado y de forma ruidosa al arrancar: issuer sin audiencia, JWKS inalcanzable en el primer uso, ambos modos configurados a la vez. Un servidor que no puede validar tokens no debe empezar a servir
/mcpsin autenticación.
Consecuencias¶
-
Positivas. El autoalojamiento remoto pasa a ser una configuración soportada en vez de un riesgo documentado, lo que desbloquea recetas de despliegue en plataformas (#121) y todas las peticiones de "quiero mi instancia privada" que hay detrás. La documentación de seguridad deja de tener que decir "sin autenticación, mantenlo en loopback". La defensa en profundidad mejora incluso para despliegues que ya viven tras una pasarela, porque el servidor interno puede verificar los mismos tokens en vez de confiar en cabeceras inyectadas.
-
Negativas. El proyecto pasa a publicar código crítico para la seguridad: un fallo en la validación de tokens es una vulnerabilidad, no una molestia, y sube el listón de revisión y publicación en esa área para siempre. La superficie de configuración crece en seis variables, y con ella la matriz de despliegue: sin autenticación, con token y con OAuth son tres caminos que hay que cubrir en tests.
-
Neutras. El comportamiento por defecto no cambia: sin issuer y sin token, el servidor de hoy byte a byte, y stdio no consulta nada de esto. Los despliegues que autentican en una pasarela e inyectan cabeceras de identidad siguen funcionando igual, porque con auth desactivada la ruta de cabeceras queda intacta; migrarlos a verificación de token es opcional e independiente de este cambio.
Seguimientos abiertos (no bloqueantes)¶
- Introspección de tokens opacos (RFC 7662) para issuers que no usan JWT.
- Mapeo de scopes por tool, para que un token pueda ser de solo lectura sobre el grafo en vez de todo-o-nada en el endpoint.
- Límite de tasa y rastro de auditoría para fallos de autenticación: hoy un intento de fuerza bruta es invisible más allá de los logs normales.
- mTLS como alternativa a los tokens bearer en despliegues máquina-a-máquina.
- El registro dinámico de clientes y la emisión de tokens quedan fuera de alcance de forma permanente. Engrama es un resource server; todo lo que emite tokens pertenece a un proveedor de identidad o a una pasarela delante.
Referencias¶
- #121 — feat: add deploy to Dockhold — la petición que lo motiva.
docs/security.md— aislamiento entre inquilinos y la advertencia actual sobre exposición de red.docs/saas/streamable-http.md— el transporte al que aplica.- RFC 9728 — OAuth 2.0 Protected Resource Metadata.
- RFC 6750 / RFC 9068 — uso de tokens bearer y JWT de acceso.
- Especificación MCP — Authorization.