TL;DR
- Hay una forma tan correcta como poco emocionante de implementar la autorización en SaaS multi-tenant, y la mayoría de los equipos experimentados terminan llegando a ella: un gateway aplica las decisiones, un servicio externo decide y los servicios posteriores se mantienen ligeros y confían en el token.
- En el lenguaje de Zero Trust (NIST SP 800-207), esta es la separación PEP/PDP: el Policy Enforcement Point es el guardia en la puerta; el Policy Decision Point es el cerebro que devuelve la decisión de permitir o denegar.
- El flujo es:
forwardAuthdel API gateway → autorizador externo → el autorizador externo consulta al motor de decisiones → el autorizador emite un JWT interno de corta duración → el servicio posterior, ligero, confía en ese JWT. - Este artículo construye el patrón con Heimdall como pipeline de aplicación de políticas (autenticar → autorizar → enriquecer → emitir token) y OpenFGA como motor de decisiones ReBAC al estilo Zanzibar.
- Heimdall y OpenFGA son elecciones modernas y deliberadas. Los equivalentes más populares son Envoy/Istio + OPA y oauth2-proxy + SpiceDB. La estructura de la arquitectura es idéntica; solo cambian los componentes.
- El resultado: la lógica de autorización vive en un solo lugar, los servicios posteriores dejan de cargar con el código espagueti de
if user.role == ...y obtienes un límite de seguridad claro, auditable y que deniega por defecto ante fallas. - Esta es una guía de topología, no de modelado de permisos. Si quieres aprender a diseñar modelos de OpenFGA, consulta el artículo complementario sobre OpenFGA para permisos granulares.
Qué aprenderás aquí
- El vocabulario que usan los arquitectos: Zero Trust, PEP vs PDP, ReBAC, Zanzibar
- Por qué el patrón de «gateway → autorizador externo → JWT interno → servicios ligeros» aparece una y otra vez
- Cómo el pipeline de Heimdall (autenticadores, autorizadores, contextualizadores y finalizadores) encaja con el rol de PEP
- Cómo actúa OpenFGA como PDP y la diferencia entre
CheckyListObjects - Un ejemplo completo y ejecutable: gateway, Heimdall, OpenFGA, un servicio upstream ligero y las reglas que los conectan
- Por qué los servicios posteriores deben confiar en un token interno recién emitido, no en el token de inicio de sesión original
- Alternativas sin rodeos (Envoy/Istio + OPA, SpiceDB, oauth2-proxy) y cuándo encajan mejor
- Los aspectos de producción que separan una demo de un despliegue
«Sí, así es como se supone que debes hacerlo»
Aquí tienes una prueba divertida. Descríbele esto a cualquier arquitecto de plataforma o de seguridad:
«El API gateway hace un
forwardAuthhacia un autorizador externo. El autorizador valida a quien hace la solicitud, consulta a un motor de decisiones al estilo Zanzibar para saber si la acción está permitida y, de ser así, emite un JWT interno de corta duración. Los servicios posteriores son ligeros y simplemente confían en ese token interno».
Verás que asiente y dice alguna versión de «sí, así es como se supone que debes hacerlo».
Esa reacción no se debe a que el diseño sea ingenioso. Se debe a que es convergente. Google (BeyondCorp / Identity-Aware Proxy), Auth0/Okta (FGA) y la mayoría de los SaaS multi-tenant maduros llegan de forma independiente a la misma estructura, porque las alternativas envejecen mal:
- La lógica de autorización copiada y pegada en cada servicio termina desincronizándose.
- Pasar los tokens de inicio de sesión directamente a cada servicio le da acceso al alcance completo de la identidad del usuario y amplía el impacto de una falla.
- Las comprobaciones de roles enterradas en el código de negocio son imposibles de auditar o modificar de forma centralizada.
El resto de este artículo trata sobre esa estructura convergente y cómo construir una versión viable con Heimdall y OpenFGA.
El vocabulario, en breve
No necesitas un doctorado en Zero Trust, pero cuatro términos te permiten entender cualquier diagrama de arquitectura en este ámbito.
| Término | Significado en pocas palabras |
|---|---|
| Zero Trust | No se confía en ninguna solicitud por dónde se originó. Cada solicitud se autentica y autoriza. (NIST SP 800-207) |
| PEP — Policy Enforcement Point | El guardián en la ruta de datos. Intercepta cada solicitud y aplica la decisión. No decide. |
| PDP — Policy Decision Point | El cerebro. Dados un sujeto + acción + recurso + contexto, devuelve permitir / denegar. No se encuentra en la ruta de datos. |
| ReBAC — Relationship-Based Access Control | Permisos derivados de relaciones (user:anne es reader de document:1234). El modelo popularizado por el artículo Zanzibar de Google. |
La idea más importante de NIST SP 800-207 es la separación entre la decisión y su aplicación. El componente que intercepta el tráfico (PEP) no es el que decide (PDP). Esa separación es la clave, porque permite centralizar las políticas y distribuir su aplicación.
decision ── PDP (the brain) ── "allow / deny"
^
| asks
|
enforcement ── PEP (the muscle) ── sits in the request path
Así se relaciona esto con nuestras herramientas:
- Heimdall = PEP (más un paso de emisión de tokens al que llegaremos).
- OpenFGA = PDP (específicamente, un motor de decisiones ReBAC).
La arquitectura de referencia
Aquí está el patrón completo en un diagrama. Tenlo presente durante el resto del artículo.
(1) request + original token
Client ───────────────────────────────────────────────► API Gateway
(Traefik / Caddy / NGINX / Envoy)
│
(2) forwardAuth: "is this allowed?"
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Heimdall (PEP + token broker) │
│ │
│ authenticate ──► authorize ──► (enrich) ──► finalize │
│ verify login ask the PDP add context MINT internal JWT │
│ token │ │
└───────────────────────┼────────────────────────────────────────────────────┘
│ (3) Check: user:anne reader document:1234 ?
▼
┌───────────────┐
│ OpenFGA │ (PDP / ReBAC decision engine)
│ model+tuples│ ──► { "allowed": true }
└───────────────┘
│
(4) 200 OK + Authorization: Bearer <internal JWT>
▼
API Gateway
│
(5) original request, now carrying the internal JWT
▼
Thin Upstream Service
(just verifies the JWT signature)
Léelo como cinco pasos:
- El cliente envía al gateway su token de inicio de sesión original.
- El gateway hace una pausa y pregunta a Heimdall (
forwardAuth) si debe permitir la solicitud. - Heimdall valida el token y luego le hace a OpenFGA la pregunta precisa sobre la relación.
- Si OpenFGA responde que sí, Heimdall emite un JWT interno nuevo y de corta duración y lo devuelve con
200 OK. - El gateway reenvía la solicitud original al servicio upstream, ahora con el JWT interno. El servicio upstream solo tiene que verificar una firma.
El servicio upstream contiene cero lógica de autorización. Esa es la gran ventaja.
Por qué usar un pipeline en el gateway + un autorizador externo
Antes de hablar de las herramientas, veamos el porqué. Tres factores empujan a todo sistema en crecimiento hacia este diseño.
1. Deja de dispersar sentencias if. La primera versión de cualquier aplicación comprueba los permisos directamente en el código:
if (user.role !== "admin" && doc.ownerId !== user.id) {
throw new ForbiddenError();
}
Multiplica eso por 200 endpoints en 12 servicios y tendrás un desorden imposible de auditar. Centralizar la decisión significa tener un solo lugar donde analizarla, probarla y cambiarla.
2. Deniega por defecto ante fallas en el perímetro. Zero Trust exige que, si no se puede contactar al punto de decisión, el punto de aplicación deniegue. Un PEP a nivel de gateway te da un único punto de control para garantizarlo, en lugar de esperar que todos los servicios manejen correctamente las fallas.
3. Reduce el alcance del impacto. Si los servicios reciben el token de inicio de sesión original, un servicio comprometido o con errores puede reutilizar la identidad completa del usuario en cualquier lugar. Si, en cambio, cada servicio recibe un token interno limitado y de corta duración, emitido para este salto, el daño que puede causar un solo servicio queda acotado.
El patrón que satisface las tres condiciones es: aplicar las decisiones en un gateway, decidir en un servicio dedicado y entregar a los servicios posteriores un token nuevo y mínimo.
Heimdall como PEP + intermediario de tokens
Heimdall se describe como un Identity-Aware Proxy y servicio de decisiones de control de acceso cloud-native. Para nuestros fines, es el pipeline de aplicación de políticas: autentica, autoriza, enriquece opcionalmente y finaliza cada solicitud.
Se ejecuta en Decision Operation Mode (serve decision) cuando está detrás de otro gateway. El gateway usa un middleware del tipo forwardAuth: si Heimdall devuelve un 2XX, el gateway reenvía la solicitud original al servicio upstream; de lo contrario, devuelve el error de Heimdall al cliente.
El pipeline de Heimdall se construye con cuatro tipos de mecanismos, que encajan muy bien con los roles de Zero Trust:
| Mecanismo de Heimdall | Función | Rol en el patrón |
|---|---|---|
| Authenticator | Verificar credenciales y establecer el sujeto (p. ej., validar un JWT contra un endpoint JWKS) | «¿Quién es?» |
| Authorizer | Decidir si el sujeto puede realizar la acción (expresión CEL, OPA u OpenFGA) | Llama al PDP |
| Contextualizer | Enriquecer el sujeto con datos adicionales de otro servicio | Agregar contexto para la decisión o el servicio upstream |
| Finalizer | Generar la salida para el servicio upstream; normalmente, emitir un JWT firmado | El paso de intermediación de tokens |
Los defines una vez en un catálogo de mecanismos y luego haces referencia a ellos por ruta en las reglas. Un catálogo reducido:
mechanisms:
authenticators:
- id: jwt_auth
type: jwt
config:
jwks_endpoint: http://idp:8080/.well-known/jwks
assertions:
issuers:
- demo_issuer
authorizers:
- id: openfga_check
type: remote
config:
endpoint: http://openfga:8080/stores/{{ .Values.store_id }}/check
payload: |
{
"authorization_model_id": "{{ .Values.model_id }}",
"tuple_key": {
"user": "user:{{ .Subject.ID }}",
"relation": "{{ .Values.relation }}",
"object": "{{ .Values.object }}"
}
}
expressions:
- expression: |
Payload.allowed == true
finalizers:
- id: create_jwt
type: jwt
config:
signer:
secret:
source: jwt_key_source
ttl: 5m
Tres cosas que conviene observar:
- El autenticador
jwtconvierte el token de inicio de sesión entrante en unSubjectverificado. - El autorizador
remotees simplemente una llamada HTTP al endpoint/checkde OpenFGA; la regla completarárelationyobject. La decisión es «¿Payload.allowed == true?». - El finalizador
jwtemite un token completamente nuevo, firmado con la propia clave de Heimdall y con un TTL corto. Este es el JWT interno. El servicio upstream lo verifica contra el endpoint JWKS de Heimdall.
Ese último punto es el corazón de la arquitectura: Heimdall no reenvía el token de inicio de sesión del usuario. Emite uno nuevo, limitado a este salto.
OpenFGA como PDP
OpenFGA es un motor de autorización de código abierto inspirado en Zanzibar de Google. Almacena un modelo de autorización (los tipos y relaciones posibles) junto con tuplas de relaciones (los hechos), y responde preguntas sobre permisos.
Para este patrón, te interesan dos tipos de consulta:
Check— «¿user:annetiene la relaciónreadersobredocument:1234?». Devuelve{ "allowed": true }. Esta es la llamada que hace el autorizador para una sola acción protegida.ListObjects— «¿Qué documentos puede leeruser:anne?». Devuelve una lista. Es lo que usa un contextualizador para alimentar endpoints de listado o incluir el conjunto permitido en el token emitido.
Un modelo mínimo para una API de documentos:
model
schema 1.1
type user
type document
relations
define reader: [user]
define writer: [user]
define owner: [user]
Y un hecho:
user:anne reader document:1234
OpenFGA también admite el estándar AuthZEN (una especificación de un grupo de trabajo de OpenID para la interoperabilidad de autorización). Esto importa porque permite que un gateway compatible con AuthZEN use OpenFGA directamente como PDP mediante /access/v1/evaluation. Para Heimdall hoy, el endpoint nativo /check mostrado arriba es la opción más directa.
Un PDP, dos patrones de acceso:
Checkprotege una sola acción;ListObjectsresponde «¿qué puedo ver?». Nunca uses los resultados deListObjectscomo límite de seguridad para una sola acción: vuelve a ejecutarCheckal acceder.
De extremo a extremo: un ejemplo funcional
Armemos todo el sistema. La configuración tiene cuatro contenedores: un proxy que actúa como gateway (aquí, por brevedad, Heimdall se ejecuta en modo proxy, pero la regla es idéntica detrás de Traefik/Caddy), un servicio upstream que devuelve la solicitud, OpenFGA y un IdP pequeño que expone un endpoint JWKS. Esto sigue la guía oficial de Heimdall para OpenFGA.
# docker-compose.yaml
services:
heimdall:
image: dadrus/heimdall:dev
ports:
- "9090:4456"
volumes:
- ./heimdall-config.yaml:/etc/heimdall/config.yaml:ro
- ./rules:/etc/heimdall/rules:ro
- ./signer.pem:/etc/heimdall/signer.pem:ro
command: serve proxy -c /etc/heimdall/config.yaml --insecure
upstream:
image: traefik/whoami:latest
command:
- --port=8081
openfga:
image: openfga/openfga:latest
command: run
ports:
- "8080:8080"
idp:
image: nginx:1.25.4
volumes:
- ./idp.nginx:/etc/nginx/nginx.conf:ro
- ./jwks.json:/var/www/nginx/jwks.json:ro
La configuración de Heimdall conecta los mecanismos entre sí y le indica al finalizador qué clave de firma usar:
# heimdall-config.yaml
secret_management:
jwt_key_source:
type: pem
config:
path: /etc/heimdall/signer.pem
mechanisms:
authenticators:
- id: jwt_auth
type: jwt
config:
jwks_endpoint: http://idp:8080/.well-known/jwks
assertions:
issuers:
- demo_issuer
authorizers:
- id: openfga_check
type: remote
config:
endpoint: http://openfga:8080/stores/{{ .Values.store_id }}/check
payload: |
{
"authorization_model_id": "{{ .Values.model_id }}",
"tuple_key": {
"user": "user:{{ .Subject.ID }}",
"relation": "{{ .Values.relation }}",
"object": "{{ .Values.object }}"
}
}
expressions:
- expression: |
Payload.allowed == true
finalizers:
- id: create_jwt
type: jwt
config:
signer:
secret:
source: jwt_key_source
providers:
file_system:
src: /etc/heimdall/rules
watch: true
Ahora la regla: la política por ruta que indica qué relación y objeto comprobar según el método HTTP:
# rules/demo.yaml
version: "1beta1"
rules:
- id: access_document
match:
routes:
- path: /document/:id
methods: [GET, POST, DELETE]
forward_to:
host: upstream:8081
execute:
- authenticator: jwt_auth
- authorizer: openfga_check
config:
values:
store_id: 01HSXG2XSZJMQG99EVXB4QQX8P
model_id: 01HSXG7TBQEJ7GBPKQR2VYH24G
relation: >
{{- if eq .Request.Method "GET" -}} reader
{{- else if eq .Request.Method "POST" -}} writer
{{- else if eq .Request.Method "DELETE" -}} owner
{{- else -}} unknown
{{- end -}}
object: >
document:{{- .Request.URL.Captures.id -}}
- finalizer: create_jwt
Esta regla compacta hace mucho:
- El método se asigna a una relación:
GETnecesitareader,POSTnecesitawriter,DELETEnecesitaowner. Esta es una política expresada de forma declarativa, no enterrada en el código del servicio. - El objeto se obtiene de la URL:
document:1234. - Si la comprobación tiene éxito, el finalizador emite el JWT interno y la solicitud pasa a
upstream:8081.
Crea el almacén, el modelo y una tupla para que anne pueda leer el documento 1234:
# 1. create the store
curl -X POST http://127.0.0.1:8080/stores \
-H "content-type: application/json" \
-d '{"name": "FGA Demo Store"}'
# 2. create the authorization model (returns an authorization_model_id)
curl -X POST http://127.0.0.1:8080/stores/<store_id>/authorization-models \
-H "content-type: application/json" \
-d '{"schema_version":"1.1","type_definitions":[
{"type":"user"},
{"type":"document","relations":{
"reader":{"this":{}},"writer":{"this":{}},"owner":{"this":{}}},
"metadata":{"relations":{
"reader":{"directly_related_user_types":[{"type":"user"}]},
"writer":{"directly_related_user_types":[{"type":"user"}]},
"owner":{"directly_related_user_types":[{"type":"user"}]}}}}]}'
# 3. grant anne the reader relationship on document:1234
curl -X POST http://127.0.0.1:8080/stores/<store_id>/write \
-H "content-type: application/json" \
-d '{"authorization_model_id":"<model_id>",
"writes":{"tuple_keys":[
{"user":"user:anne","relation":"reader","object":"document:1234"}]}}'
Ahora una solicitud de anne (un JWT con sub: anne) a GET /document/1234 tiene éxito:
curl -X GET -H "Authorization: Bearer <anne-login-jwt>" \
127.0.0.1:9090/document/1234
El servicio upstream devuelve la solicitud y, lo más importante, el encabezado Authorization que recibe no es el token de inicio de sesión de Anne. Es un JWT nuevo emitido por Heimdall ("iss": "heimdall"), con sub: anne y una expiración corta. Una solicitud DELETE /document/1234 de Anne falla, porque ella solo es reader, no owner. La decisión ocurrió en OpenFGA; Heimdall la aplicó; el servicio upstream nunca tuvo que saber nada de eso.
El intercambio del JWT interno (y por qué importa)
Este es el paso que muchos omiten, y es el más importante.
La versión ingenua de un gateway simplemente reenvía el token de inicio de sesión original a todos los servicios:
Client ──[login JWT]──► Gateway ──[same login JWT]──► Service A
──[same login JWT]──► Service B
El problema: ahora cada servicio tiene una credencial con toda la autoridad del usuario. El servicio B, que solo necesita mostrar un perfil, ahora tiene un token que podría reutilizar contra los endpoints de administración del servicio A. Eso es lo opuesto al mínimo privilegio.
La versión correcta emite un token interno nuevo en el límite de seguridad:
Client ──[login JWT]──► Gateway+Heimdall
│ verify login JWT
│ ask PDP
│ MINT internal JWT (iss: heimdall, ttl: 5m, sub: anne)
▼
──[internal JWT]──► Thin Service (verify signature, done)
Por qué esto es mejor:
- Corta duración. Un token de 5 minutos que se filtra es mucho menos peligroso que una sesión de inicio de sesión de larga duración.
- Limitado y adaptado al servicio upstream. Heimdall puede incluir exactamente los claims que necesita el servicio (y el contextualizador incluso puede incorporar el resultado de
ListObjectsde OpenFGA), y nada más. - Proveedores de identidad desacoplados. El servicio upstream verifica un solo emisor —Heimdall—, independientemente de si los usuarios inician sesión mediante Okta, Auth0, Keycloak o tres de ellos a la vez. Cambiar de IdP se convierte en un cambio de configuración de Heimdall, no en una migración de todos los servicios.
- Un límite de confianza real. La regla del servicio upstream es muy simple: confiar en una firma válida del JWKS de Heimdall y rechazar todo lo demás.
Esto no es un invento de Heimdall. Es exactamente lo que hace Identity-Aware Proxy de Google con su encabezado firmado x-goog-iap-jwt-assertion: IAP elimina los encabezados de identidad proporcionados por el cliente e inyecta una aserción firmada criptográficamente que verifica el backend. Toda la premisa de BeyondCorp es que el backend confía en el claim firmado del proxy, no en la red. Heimdall + OpenFGA es una versión de esa idea que puedes alojar por tu cuenta.
Alternativas sin rodeos
Heimdall y OpenFGA son buenas elecciones modernas, pero no son la única forma de construir este diagrama. Lo que importa es la arquitectura; los componentes son intercambiables. Estas son las alternativas populares.
| Rol | Este artículo | Alternativa más popular | Otras opciones |
|---|---|---|---|
| Gateway / PEP | Heimdall (detrás de cualquier proxy) | Envoy / Istio ext_authz | NGINX, Traefik, Kong, oauth2-proxy |
| Motor de decisiones / PDP | OpenFGA (ReBAC) | OPA (políticas como código, Rego) | SpiceDB, Ory Keto, Cedar |
| ReBAC específicamente | OpenFGA | SpiceDB (AuthZed) | Ory Keto |
Algunas notas para que puedas elegir bien:
- Envoy/Istio + OPA es la opción predeterminada en equipos que usan Kubernetes y service mesh de forma intensiva. El filtro
ext_authzde Envoy es el PEP y OPA es el PDP. Rego de OPA es extremadamente flexible, pero se basa en políticas como código (lógica de atributos y reglas), no en relaciones de forma nativa; modelar grafos ReBAC profundos en Rego es posible, pero resulta poco práctico. - OPA vs OpenFGA es la decisión clave: elige OPA cuando tus decisiones se basan en atributos o condiciones («¿la solicitud proviene de un dispositivo administrado durante el horario laboral?»); elige OpenFGA/SpiceDB cuando se basan en grafos de relaciones («¿puede este usuario editar este documento por un permiso heredado de esta carpeta en esta organización?»). Muchos sistemas maduros ejecutan ambos.
- SpiceDB vs OpenFGA: ambos son implementaciones de Zanzibar. SpiceDB (AuthZed) es el más popular; OpenFGA es un proyecto sandbox de CNCF con respaldo directo de Auth0/Okta. Se parecen más de lo que se diferencian; elige según su compatibilidad con tu ecosistema y tus preferencias operativas.
- oauth2-proxy es la opción ligera favorita para «solo poner SSO delante de esta aplicación». Maneja bien la autenticación, pero no es un pipeline de decisiones completo; no emite tokens internos con alcance limitado ni consulta a un PDP ReBAC como lo hace Heimdall.
La razón para elegir Heimdall + OpenFGA en lugar de la combinación más popular suele ser que quieres el modelo de pipeline claro + emisión de tokens sin adoptar todo un service mesh, y quieres autorización basada de forma nativa en relaciones desde el primer día.
Aspectos de producción
La demo se hace en un fin de semana. La producción exige disciplina en los puntos donde se encuentran la identidad, las decisiones y los tokens.
| Aspecto | Qué debes hacer bien |
|---|---|
| Denegar por defecto ante fallas | Si no se puede contactar a OpenFGA, el autorizador debe fallar y el gateway debe denegar. Verifica esto explícitamente; nunca permitas que un timeout del PDP se convierta en una autorización implícita. |
| Latencia | Ahora cada solicitud hace una llamada de decisión. Usa la caché de Heimdall, aprovecha el rendimiento de Check en OpenFGA y mantén el PDP cerca (mismo clúster o zona). Reserva un presupuesto de latencia de un solo dígito en milisegundos. |
| Consistencia | Después de un cambio de permisos (escritura de una tupla), las lecturas pueden estar desactualizadas brevemente. Para flujos sensibles, usa las opciones de mayor consistencia de OpenFGA y diseña para garantizar lecturas posteriores a la escritura donde importe. |
| Gestión de claves | La clave de firma de Heimdall es ahora una de tus joyas de la corona. Rótala, publícala mediante JWKS para que los servicios upstream obtengan las nuevas claves y mantén los TTL cortos para que la rotación sea sencilla. |
| Auditoría | Registra las decisiones en el PDP y su aplicación en el PEP: quién, qué relación, qué objeto, permitido o denegado. Este es tu registro de auditoría. |
| TTL del token | Lo bastante corto para limitar la reutilización (minutos), pero lo bastante largo para evitar emitirlo de nuevo en cada llamada. Guarda los tokens emitidos en caché usando el sujeto + claims como clave. |
| Proxies confiables | Si Heimdall está detrás de Traefik/Caddy, configura las IP de los proxies confiables para que lea correctamente X-Forwarded-*. No dejes esto en 0.0.0.0/0 en producción. |
| Verificación del servicio upstream | Ligero no significa ingenuo: cada servicio upstream todavía debe verificar la firma, el emisor y la expiración del JWT interno. Una comprobación deshabilitada no constituye un límite de confianza. |
Limitaciones: lo que este stack no resuelve
Sé honesto contigo mismo sobre los límites del patrón.
- No es autenticación. Heimdall valida tokens, pero todavía necesitas un IdP (Okta, Auth0, Keycloak) que emita los tokens de inicio de sesión.
- No aplica límites de acceso a los datos. OpenFGA indica si una acción está permitida; tu base de datos todavía necesita filtros por tenant, seguridad a nivel de fila y roles con alcance limitado. Trata el PDP y la base de datos como dos cerraduras independientes (defensa en profundidad).
ListObjectsno es un filtro de contenido para tenants enormes. A gran escala todavía necesitas índices de búsqueda y paginación; no conviertas cada carga de página en una consulta de autorización enorme.- No modela los permisos por ti. Diseñar correctamente el modelo de OpenFGA (herencia, grupos, condiciones) es una disciplina por sí misma, cubierta en el artículo complementario sobre modelado.
- No gestiona la ejecución de alto riesgo. «Permitido» no significa «seguro para ejecutar automáticamente». Los movimientos de dinero, los despliegues en producción y las exportaciones de datos todavía pueden requerir aprobación humana además de la decisión de permitir.
- El gateway se convierte en parte de la ruta crítica. Has centralizado la aplicación de políticas, que es el objetivo, pero ahora la disponibilidad del gateway + PDP determina la disponibilidad de tu sistema. Planifica la redundancia en consecuencia.
Modelo mental final
Si recuerdas una sola cosa, que sea la división de responsabilidades:
IdP / OAuth proves who you are (authentication)
Gateway (PEP) intercepts and enforces (Heimdall, in the path)
Decision engine decides allow/deny (OpenFGA, the PDP brain)
Token broker mints a scoped internal JWT (Heimdall finalizer)
Thin service verifies signature, executes (no auth logic)
Database / RLS enforces the data blast radius (independent lock)
Audit log explains the decision later (accountability)
El patrón no es emocionante, y por eso mismo los arquitectos lo aprueban. Pone la autorización en un solo lugar auditable, entrega a cada servicio la credencial mínima que necesita y deniega por defecto ante fallas en un único límite bien entendido. Heimdall y OpenFGA son una forma clara de construirlo y alojarlo por tu cuenta; y si más adelante los cambias por Envoy + OPA u oauth2-proxy + SpiceDB, el diagrama en la pared no cambia. Esa estabilidad es el objetivo.
Fuentes
- Heimdall — Identity Aware Proxy cloud-native (GitHub)
- Documentación de Heimdall: modos de operación (Decision vs Proxy)
- Documentación de Heimdall: mecanismos (autenticadores, autorizadores, contextualizadores, finalizadores)
- Documentación de Heimdall: finalizadores (emisión de JWT)
- Documentación de Heimdall: regla regular
- Guía de Heimdall: integración con OpenFGA
- Guía de Heimdall: integración de Traefik ForwardAuth
- Guía de Heimdall: integración de Caddy forward_auth
- Documentación de OpenFGA: ¿qué es la autorización granular?
- Documentación de OpenFGA: consultas de relaciones (Check, ListObjects)
- Documentación de OpenFGA: API AuthZEN (interoperabilidad de PDP)
- Zanzibar: el sistema de autorización global y consistente de Google
- NIST SP 800-207: arquitectura Zero Trust (PEP/PDP/PE/PA)
- Google BeyondCorp: seguridad empresarial Zero Trust
- Google Identity-Aware Proxy: protege tu aplicación con encabezados firmados
- Open Policy Agent (OPA)
- Filtro de autorización externa de Envoy (ext_authz)
- SpiceDB (AuthZed) — ReBAC inspirado en Zanzibar
- oauth2-proxy