Production AI Infrastructure

El stack de autorización Zero Trust que los arquitectos aprueban: Heimdall + OpenFGA

Cómo funciona el patrón de forwardAuth en el gateway, un autorizador externo, un JWT interno y servicios ligeros, construido con Heimdall como PEP y OpenFGA como PDP al estilo Zanzibar.

22 min de lectura Actualizado el 17 jun 2026

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: forwardAuth del 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 Check y ListObjects
  • 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 forwardAuth hacia 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érminoSignificado en pocas palabras
Zero TrustNo se confía en ninguna solicitud por dónde se originó. Cada solicitud se autentica y autoriza. (NIST SP 800-207)
PEP — Policy Enforcement PointEl guardián en la ruta de datos. Intercepta cada solicitud y aplica la decisión. No decide.
PDP — Policy Decision PointEl cerebro. Dados un sujeto + acción + recurso + contexto, devuelve permitir / denegar. No se encuentra en la ruta de datos.
ReBAC — Relationship-Based Access ControlPermisos 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:

  1. El cliente envía al gateway su token de inicio de sesión original.
  2. El gateway hace una pausa y pregunta a Heimdall (forwardAuth) si debe permitir la solicitud.
  3. Heimdall valida el token y luego le hace a OpenFGA la pregunta precisa sobre la relación.
  4. Si OpenFGA responde que sí, Heimdall emite un JWT interno nuevo y de corta duración y lo devuelve con 200 OK.
  5. 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 HeimdallFunciónRol en el patrón
AuthenticatorVerificar credenciales y establecer el sujeto (p. ej., validar un JWT contra un endpoint JWKS)«¿Quién es?»
AuthorizerDecidir si el sujeto puede realizar la acción (expresión CEL, OPA u OpenFGA)Llama al PDP
ContextualizerEnriquecer el sujeto con datos adicionales de otro servicioAgregar contexto para la decisión o el servicio upstream
FinalizerGenerar la salida para el servicio upstream; normalmente, emitir un JWT firmadoEl 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 jwt convierte el token de inicio de sesión entrante en un Subject verificado.
  • El autorizador remote es simplemente una llamada HTTP al endpoint /check de OpenFGA; la regla completará relation y object. La decisión es «¿Payload.allowed == true?».
  • El finalizador jwt emite 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:anne tiene la relación reader sobre document:1234?». Devuelve { "allowed": true }. Esta es la llamada que hace el autorizador para una sola acción protegida.
  • ListObjects — «¿Qué documentos puede leer user: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: Check protege una sola acción; ListObjects responde «¿qué puedo ver?». Nunca uses los resultados de ListObjects como límite de seguridad para una sola acción: vuelve a ejecutar Check al 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: GET necesita reader, POST necesita writer, DELETE necesita owner. 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 ListObjects de 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.

RolEste artículoAlternativa más popularOtras opciones
Gateway / PEPHeimdall (detrás de cualquier proxy)Envoy / Istio ext_authzNGINX, Traefik, Kong, oauth2-proxy
Motor de decisiones / PDPOpenFGA (ReBAC)OPA (políticas como código, Rego)SpiceDB, Ory Keto, Cedar
ReBAC específicamenteOpenFGASpiceDB (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_authz de 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.

AspectoQué debes hacer bien
Denegar por defecto ante fallasSi 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.
LatenciaAhora 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.
ConsistenciaDespué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 clavesLa 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íaRegistra 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 tokenLo 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 confiablesSi 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 upstreamLigero 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).
  • ListObjects no 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