Published

Runbook · F4·3 — el bridge OPA: la autorización baja al MOTOR

Connect any source, model it as an ontology, transform it, and operationalize it, analytics, automation and machine learning, under one governed, self-hostable roof. --- Most teams stitch the...

Runbook · F4·3 — el bridge OPA: la autorización baja al MOTOR

🏁 F4·3 COMPLETADO — 2026-08-07 · la autorización vive en el motor

GateResultado
SHOW CATALOGScarbon · system · tpch — los unmanaged siguen visibles
⭐⭐ SELECT *[[1,"norte",10.5],[2,"sur",20.5],[3,"norte",30.5]]decidido por OPA → Lakekeeper → OpenFGA
⭐⭐⭐ revocar el grant → misma query🛑 Access Denied: Cannot access catalog carbon
⭐⭐⭐ devolver el grant → misma query[[3]]

El tercer gate es el que vale, y por dos razones

① El ciclo completo, no una foto. Revocar → denegado; conceder → permitido. Un
permitido aislado no prueba que alguien esté decidiendo; que la respuesta cambie
con el grant, sí.

② El mensaje delata QUIÉN deniega. Access Denied: Cannot access catalog carbon
es de Trino, no de la cara (que dice «lakekeeper-trino no puede loadTable —
falta…»
). ⇒ la query se corta EN EL MOTOR, antes de llegar al catálogo. Eso es
literalmente el viraje cumplido: el punto de aplicación baja al catálogo y el motor lo
ejecuta.

⚠️ Ojo con el gate «schema sin grant» de F2: sigue denegando, pero por la cara,
no por OPA. Los dos caminos deniegan y conviene no confundirlos al diagnosticar.

F4·3a + F4·3b COMPLETADOS.


🏁 Lo que está hecho y verificado

OPA desplegado en GKE con las 13 políticas Rego del bridge oficial de Lakekeeper (authz/opa-bridge de la v0.13.0 — Apache-2.0, a diferencia de Cedar). Manifiesto: infra/gke/f4-opa-bridge.yaml.

   Trino ──(nativo)──▶ OPA ──(POST /management/v1/action/batch-check)──▶ Lakekeeper
                                                                          └─▶ OpenFGA

Vive en GKE, no en Railway, porque está en el camino de cada decisión de cada query: un salto a otra nube por decisión sería el peaje más caro del stack.

El gate, y es de los buenos: la decisión CAMBIÓ al cambiar el grant

Consulta al bridgeResultado
/health desde el coordinador200
ExecuteQuery para trino-junctiontrue
SelectFromColumns sobre el canario — sin grantfalse 🛑
después de conceder select en OpenFGAtrue

Eso es lo que prueba que el bridge está VIVO y no devolviendo un default. Un
true solo no dice nada; un false que se vuelve true al conceder el permiso
demuestra la cadena entera: OPA → Lakekeeper → OpenFGA → decisión.


🔴 El hallazgo que decide F4·3b: hay DOS vocabularios de identidad

El bridge construye el principal así (trino_user.rego, literal):

trino_user_id      := input.context.identity.user
lakekeeper_user_id := concat("", ["oidc~", trino_user_id])

el principal es oidc~<el usuario que Trino diga>. Y con el principal-field: azp que fijó F3, ese usuario es trino-junction (el client_id).

CapaCómo identifica
Index (principal_grants)el client_idtrino-junction
El bridge OPAoidc~<client_id>oidc~trino-junction
Lakekeeper nativooidc~<sub>oidc~337c8749-31d2-…

⭐ Y la decisión de F3 resultó ser la correcta también aquí — por poco

Elegir azp (y no preferred_username ni sub) hace que Index y el bridge
hablen el mismo idioma
. El que diverge es el principal nativo de Lakekeeper.

Medido, y por eso está escrito: conceder select a oidc~337c8749-… (el sub
real del service account) NO cambió la decisión — seguía false. Sólo al conceder
a oidc~trino-junction pasó a true. Un grant al principal "correcto" según
Lakekeeper no protege ni habilita nada en el camino de Trino.

[confirmar] antes de F4·3b: para un usuario humano entrando por OIDC (F3·b), el principal-field será otro (preferred_username), y entonces el bridge buscará oidc~<usuario>. Los grants hay que sembrarlos con ese mismo vocabulario, o el usuario entrará autenticado y sin poder leer nada.


⏸️ F4·3b — cablear Trino, y por qué NO se hace en el mismo paso

Cablear es trivial:

access-control.name=opa
opa.policy.uri=http://opa-bridge.trino.svc.cluster.local:8181/v1/data/trino/allow
opa.policy.batched-uri=http://opa-bridge.trino.svc.cluster.local:8181/v1/data/trino/batch

Lo que no es trivial es lo que pasa después: a partir de ese momento todas las decisiones de Trino las toma el bridge, y hoy sólo trino-junction tiene grants, y sólo en un warehouse. Todo lo demás pasaría a denegarse.

Los tres prerrequisitos, y ninguno es código:

  1. Sembrar los grants con el vocabulario del bridge (oidc~<client_id>) para cada cliente que use Trino, y en cada warehouse que deba ver.
  2. Decidir qué pasa con system y tpch. Hoy se cubren con TRINO_ALLOW_UNMANAGED_CATALOGS=true — sin eso, SHOW CATALOGS deja de funcionar. Es un default permisivo y conviene que sea una decisión, no un descuido.
  3. Entender qué se gana, porque no es «más seguridad» a secas. Hoy la cara ya deniega por schema (medido en F2). Lo que añade OPA es aplicar la política del USUARIO FINAL en el motor — la cara sólo ve la identidad del motor (lakekeeper-trino), no quién preguntó. Ése es el salto, y es el que trino#27917 no puede dar en el catálogo.

Y una advertencia que la propia doc de Lakekeeper hace: «ensure that no users have
root access to trino or OPA, as those contain credentials to Lakekeeper with very high
permissions»
. El bridge habla con Lakekeeper como lakekeeper-operator. Es la
misma frontera de confianza que el runbook ya documenta (compute enforces
permissions
), no una nueva — pero ahora tiene una caja más donde vive el secreto.


Las CUATRO trampas del despliegue

① OPA no arranca desde un ConfigMap sin --ignore

rego_type_error: multiple default rules data.trino.allow found at
  …/..2026_08_07_…/trino_main.rego:7, …/..data/trino_main.rego:7, …/trino_main.rego:7

Kubernetes monta los ConfigMaps con symlinks (..data/, ..<timestamp>/) apuntando al mismo contenido, y opa run /policies recorre el directorio recursivamente: carga cada política tres veces. Se lee como un fallo de las políticas y es del montaje. Lo cierra --ignore=.*.

② La imagen de OPA es distroless

No tiene shell, así que no se puede diagnosticar con kubectl exec … sh. Se prueba desde el coordinador de Trino — que además es desde donde preguntará de verdad, así que el diagnóstico y el camino real coinciden.

③ 🔴 access-control.name NO va en configOverrides

Ponerlo en config.properties deja el coordinador en CrashLoopBackOff. El log del entrypoint lo explica sin nombrarlo:

cp -RL /stackable/config/catalog /stackable/config/config.properties \
       /stackable/config/jvm.config /stackable/config/log.properties \
       /stackable/config/node.properties /stackable/config/security.properties …

Stackable copia una lista FIJA de ficheros, y access-control.properties no está en ella — sólo aparece cuando se usa clusterConfig.authorization.opa, que es el campo del CRD hecho para esto. Con él, Stackable genera además las URIs de columnMask y rowFilters gratis, que es justo la capacidad por la que se adopta OPA.

⚠️ Y configMapName no es el Service: es un ConfigMap de descubrimiento con la clave OPA = la URL base. Normalmente lo publica el opa-operator de Stackable; como aquí OPA es nuestro, se escribe a mano.

ERROR: already running as 9 — un PID huérfano, no un fallo de config

Tras varios reinicios rápidos, el launcher de Trino encuentra el PID file del proceso anterior y sale con exit 0 (Completed), lo que se lee como «la configuración sigue mal» cuando ya estaba bien. Se cierra con kubectl delete pod … --force --grace-period=0.


Rollback

F4·3a no tiene rollback porque no cambia nada: OPA está desplegado e inerte mientras Trino no lo apunte. Para retirarlo: kubectl delete -f infra/gke/f4-opa-bridge.yaml.

Cuando F4·3b entre, el rollback es quitar access-control.name=opa del configOverrides del TrinoCluster — Trino vuelve a su control de acceso por defecto y la cara sigue gobernando como hoy.


Cruza con: f4-1-poblar-openfga.md · openfga-standup.md · f3-identidad-approach.md (de donde sale azp) · trino-absorcion.md §6.