Por qué elegimos OpenFGA al arrancar una plataforma de 17 microservicios
En el trabajo estamos arrancando una plataforma multi-tenant compuesta por una flota larga de microservicios independientes, organizados en una jerarquía de tenants, equipos y proyectos. Una de las primeras decisiones de arquitectura que tomamos fue cómo iba a funcionar la autorización. La opción por defecto, la que sale sola si no la piensas, es que cada microservicio compruebe sus propios permisos. Decidimos no hacer eso y meter OpenFGA desde el día uno. Este post explica por qué, y qué problema concreto evita.
Doy por hecho que ya conoces OpenFGA y su modelo ReBAC. Si no, en esta otra entrada lo cuento desde cero con modelo, tuplas y la operación Check, y luego vuelves a esta.
El problema que estábamos a punto de tener#
La pregunta "¿puede este usuario hacer X sobre el recurso Y?" aparece en cada endpoint de cada microservicio. La respuesta intuitiva, sobre todo cuando empiezas con dos o tres servicios, es meter la regla dentro del servicio que sirve el endpoint. Funciona durante un tiempo, en parte porque eres tú o tu compañero quien escribe los dos servicios y la cabeza se acuerda de mantener las dos lógicas sincronizadas.
El problema aparece cuando crece el catálogo. Alguien añade el rol de viewer, después aparece guest, más tarde llega el admin de tenant que puede hacer todo excepto facturación, y de repente tienes cinco tablas de roles ligeramente distintas en cinco microservicios distintos. La probabilidad de que el sexto silenciosamente permita lo que los otros cinco bloquean es exactamente la que estás pensando, y cuando ese bug aparece, aparece en el peor momento posible.
La salida intermedia (extraer una librería de autorización compartida) tampoco resuelve el problema, lo desplaza. Acabas con dos servicios en versiones distintas de la librería, migraciones de roles a medio camino, parches que en teoría son para un servicio y en la práctica se cuelan en otros tres porque viven en el mismo monorepo. Sabiendo eso de antemano, y sabiendo que ya íbamos a empezar con más de quince servicios, preferimos no entrar en ese ciclo en absoluto.
Cómo encaja OpenFGA en un sistema de microservicios gRPC#
El patrón habitual en una flota de servicios gRPC pasa por concentrar la comprobación en el borde. La petición HTTP llega al gateway con un JWT, el gateway extrae la identidad del usuario, llama a OpenFGA con la tripleta (sujeto, relación, recurso) y solo si la respuesta es positiva reenvía la llamada al microservicio correspondiente. El microservicio confía en el gateway porque el enlace entre los dos viaja por mTLS o por una red interna que nadie más alcanza, y deja de preocuparse por autorizar porque esa decisión ya está tomada.
El dibujo mental que nos interesaba era este, no uno más complicado:
Hay una trampa sutil que conviene tener clara desde el principio. Si un microservicio puede recibir llamadas de otro microservicio (un worker, un job runner, un fan-out), entonces el gateway no es el único punto de entrada y el microservicio sí tiene que autorizar. La forma limpia de resolverlo es propagar la identidad original del usuario en cada llamada gRPC interna y volver a preguntarle a OpenFGA en cualquier frontera que cruce un límite de confianza. Suena caro y no lo es, porque el servicio aguanta miles de checks por segundo sin despeinarse.
La regla que dejamos escrita fue bastante simple: las peticiones externas se autorizan en el gateway; las llamadas internas conservan el usuario original; y cualquier servicio que pueda recibir trabajo fuera del flujo HTTP normal tiene que tratar esa entrada como una frontera nueva, no como una continuación mágica de la anterior.
El bonus de auditoría#
Hay un efecto secundario que a mí me parece más valioso de lo que parece a primera vista. Las tuplas se crean y se revocan a través de una API, y los cambios quedan en un log. Cuando llega el día (y ese día llega) en el que alguien pregunta "¿quién tenía acceso a este recurso el 5 de marzo a las 14:00?", el log responde por ti. Si tu producto guarda registros sensibles en append-only para trazabilidad, ya tienes la mitad del trabajo hecho porque el mismo principio aplica a los permisos.
En nuestro caso esto pesó bastante porque muchos permisos nacen como consecuencia de eventos de negocio: se crea un proyecto, alguien entra en un equipo, se revoca una invitación, cambia el owner de un recurso. Si cada evento escribe o borra una tupla explícita, la historia de acceso deja de estar escondida en cinco bases de datos y pasa a vivir en un sitio que puedes inspeccionar.
Por qué metimos OpenFGA desde el primer commit#
Cuando arrancas un sistema con esta forma, hay dos momentos en los que puedes introducir OpenFGA: ahora, antes de que exista un solo endpoint, o más adelante, cuando ya tienes permisos cableados en quince sitios y toca migrarlos. Lo primero es escribir un modelo. Lo segundo es un proyecto de varios meses con riesgo de regresión en cada servicio. La diferencia de coste entre los dos caminos es lo que nos hizo decidir antes de escribir el primer microservicio.
La promesa que estamos comprando es esta: en una plataforma con un puñado largo de microservicios que comparten identidad y jerarquía, OpenFGA reduce una familia entera de bugs a un único enunciado mucho más fácil de razonar, que es "¿te has acordado de escribir la tupla cuando creaste el recurso?". Es un problema acotado, con una respuesta clara y un sitio único donde ponerla. El problema alternativo, "¿desplegó el tercer servicio la nueva tabla de roles, y el cuarto interpreta el mismo enum de la misma forma, y el quinto?", es uno del que nunca tienes una respuesta del todo tranquilizadora.
La primera tarde toca pelearse con el modelo, con el from y con los or, y conviene asumirlo desde el principio. A cambio, escribir un endpoint nuevo deja de incluir el paso "y ahora voy a copiar la lógica de permisos del endpoint vecino con la esperanza de no haber roto nada". En un sistema que va a crecer hasta diecisiete servicios, ese paso que desaparece es exactamente la deuda técnica que no queremos contraer.
No elegimos OpenFGA porque saliera gratis en complejidad. Lo elegimos porque preferíamos asumir esa complejidad una vez en el modelo antes que repartirla entre diecisiete servicios distintos y descubrir las diferencias cuando ya hubiese datos reales pasando por el sistema.