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
Gate Resultado 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
permitidoaislado 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.
Continúa f4-1-poblar-openfga.md. El porqué está en
catalogo-como-origen-del-grafo.md.
🏁 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 bridge | Resultado |
|---|---|
/health desde el coordinador | 200 |
ExecuteQuery para trino-junction | true |
SelectFromColumns sobre el canario — sin grant | false 🛑 |
…después de conceder select en OpenFGA | true ✅ |
Eso es lo que prueba que el bridge está VIVO y no devolviendo un default. Un
truesolo no dice nada; unfalseque se vuelvetrueal 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).
| Capa | Cómo identifica |
|---|---|
Index (principal_grants) | el client_id — trino-junction |
| El bridge OPA | oidc~<client_id> — oidc~trino-junction |
| Lakekeeper nativo | oidc~<sub> — oidc~337c8749-31d2-… |
⭐ Y la decisión de F3 resultó ser la correcta también aquí — por poco
Elegir
azp(y nopreferred_usernamenisub) 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
selectaoidc~337c8749-…(elsub
real del service account) NO cambió la decisión — seguíafalse. Sólo al conceder
aoidc~trino-junctionpasó atrue. 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:
- Sembrar los grants con el vocabulario del bridge (
oidc~<client_id>) para cada cliente que use Trino, y en cada warehouse que deba ver. - Decidir qué pasa con
systemytpch. Hoy se cubren conTRINO_ALLOW_UNMANAGED_CATALOGS=true— sin eso,SHOW CATALOGSdeja de funcionar. Es un default permisivo y conviene que sea una decisión, no un descuido. - ⭐ 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 comolakekeeper-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.