Published

P4-bis · La máquina de acuñar, como superficie propia

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...

P4-bis · La máquina de acuñar, como superficie propia

Approach. Nada implementado. Cierra las dos excepciones que quedaron abiertas en
storage-tenancy-approach.md §6·P4: warehouse-writer y
duck-server conservan llaves estáticas de R2.

Y nace de una crítica al diseño actual, no de una funcionalidad nueva: al preguntarse
«¿cómo resolvería esto AWS?» aparecieron dos errores nuestros de encuadre. Están en §2 y
§3, y los dos hacen el problema más pequeño de lo que parecía.


1 · Lo que AWS prescribe, y qué parte no teníamos

Su patrón —token vending machine— se resume en una frase: ningún proceso que sirva a un inquilino sostiene una credencial duradera. El rol de la tarea no abre S3 en absoluto; para tocar bytes hay que pedir una credencial acotada (prefijo y acciones, con TTL). El aislamiento deja de ser una convención y pasa a ser estructural: no es que el motor no deba salirse de su inquilino — es que no puede.

Nosotros tenemos la máquina… colgada del sitio equivocado:

AWSNosotros hoy
Qué es la TVMun endpoint propio: «credencial para inquilino T, prefijo P, operaciones O»un efecto secundario de loadTable con X-Iceberg-Access-Delegation
Quién puede pedirlacualquiera que se autentique y pase la políticasólo quien haga loadTable por la cara Iceberg REST
Lectura y escriturala session policy nombra las accionessólo lectura

Ése es el hallazgo. «El writer no tiene a quién pedirle la llave» no es una
limitación del patrón: es una consecuencia de dónde colgamos la máquina. Extraerla a
su propia superficie convierte a loadTable en un cliente más, y abre la puerta a los
otros dos.


2 · 🔴 Primer error de encuadre: metadata y bytes son DOS problemas

Se dijo que duck-server no podía soltar su llave porque tendría que atarse a la cara, y que eso chocaba con P1 —«DuckDB ata un catálogo por conexión, y ahora hay un warehouse por inquilino»—. Eso es cierto para la METADATA y falso para los BYTES:

  • qué tablas existen y quién puede verlas → lo resuelve el catálogo (atarse a la cara);
  • quién puede leer los bytes → lo resuelve la credencial acuñada.

Separados, la colisión desaparece: duck puede seguir resolviendo metadata como hoy y pedir credencial por consulta para los bytes. DuckDB lo soporta —secretos con SESSION_TOKEN y con SCOPE por prefijo, que ya usamos desde P4— así que no hay que re-atar nada.

⚠️ El bypass de catálogo de duck sigue siendo un problema real —se ata a Lakekeeper y se salta la cara, que es el mismo agujero que cerramos para Trino— pero es otro, y no bloquea quitarle la llave. Va por su propio approach.

3 · 🔴 Segundo error: el writer NO es infraestructura

Se ofreció «aceptar que el writer conserve su llave, igual que Lakekeeper». Con el criterio de AWS no se sostiene, y la distinción es limpia:

Puede sostener credencial duradera
Plano de control — la propia TVM, el catálogo✅ sí: es quien la reparte
Cómputo por cuenta de un inquilino — writer, duck, Trino, Karmano, es la categoría que el patrón elimina

warehouse-writer está en la segunda fila. Que hoy tenga llave no es una excepción justificada: es la excepción que este approach existe para quitar.


4 · ⚡ La pregunta de la latencia: ¿no es caro pedir credencial en cada escritura?

Es la objeción correcta, y la respuesta tiene dos mitades.

La primera: no se pide por consulta — se pide por PREFIJO y se cachea. Acuñar cuesta una llamada a Cloudflare (~100-300 ms, medido) y el TTL por defecto es de 1 hora, con la caché sirviendo hasta 60 s antes de caducar. Una tabla que se escribe cien veces en una hora paga una vez. Y en el writer la comparación es aún más favorable: una ingesta mueve filas, hace commit y toca R2 durante segundos — 200 ms una vez por tabla y hora es ruido, no coste.

La segunda, y es la que decide el diseño: se puede acuñar SIN LLAMAR A NADIE. La documentación de R2 describe una alternativa a su API — firmar localmente: se construye un JWT, se firma con HS256 usando el secreto del token padre, y el secreto temporal sale de un SHA-256 de ese JWT. Tres consecuencias:

  • latencia ≈ 0: es criptografía en proceso, sin viaje de red;
  • no consume el presupuesto de 1.200 peticiones cada 5 minutos por cuenta — que es el techo que de verdad nos preocupa cuando haya cientos de inquilinos;
  • es la única vía que soporta actionsver el resultado de F1.

La escritura debe ir por firmado local desde el principio. Es lo que hace que la objeción de la latencia deje de existir.

✅ F1 corrido — 5 verdes / 0 rojos (2026-08-04), y con una promesa caída

① acuñada en 0,94 ms — sin viaje de red
② 200 — lee un objeto DENTRO del prefijo (1.726 bytes)
③ ⭐ AccessDenied — y NO puede leer uno FUERA
④ ⭐⭐ AccessDenied — y con `scope` de lectura NO puede escribir ni en su propio prefijo

0,94 ms frente a 100-300 ms. La objeción de la latencia deja de existir: acuñar por escritura es más barato que el propio JSON.parse de la petición.

🔴 Pero actions NO funciona. La documentación la anuncia como exclusiva del firmado
local («currently supported via local signing only») y era el argumento principal de
esta fase. Medido: R2 devuelve InvalidArgument: X-Amz-Security-Token en las cinco
variantes probadas
— nombres del propio ejemplo (GetObject, HeadObject), con
prefijo s3:, sola y junto a scope.

Es el mismo género de hallazgo que Cedar: documentado, anunciado en las notas de
versión, y ausente en el producto. Y se caza igual — intentándolo con un control
negativo, no leyendo.

Qué cambia: nada de lo que importa. El preset scope distingue
object-read-only de object-read-write, que es exactamente la granularidad que F3 y F4
necesitan — un motor que lee frente a un escritor que commitea. Lo que se pierde es el
refinamiento por operación, que era un nice to have. signR2Credential lanza si
alguien pasa actions, para que el próximo que lea la documentación no repita la media
hora.

🔎 Y una trampa de forma, por si alguien la reimplementa: las dos listas de paths
prefixPaths y objectPathsvan siempre, aunque una esté vacía. Omitir la vacía
produce el mismo InvalidArgument opaco, que no menciona el campo que falta.


5 · Dónde el patrón de AWS NO se puede portar

Lo que AWS daEn R2
STS + session policy con accionesno hay STS. Presets + prefijos — y actions tampoco por firmado local, pese a estar documentado (F1)
Bucket policies como segunda red («deny salvo que el prefijo coincida»)no existen
Session tags → atribución por inquilino en CloudTrail🔴 no existe. R2 devuelve el accessKeyId del token padre: en el almacenamiento todos los accesos se ven iguales

La tercera es la que más pesa, y está medida. Nuestra única atribución vive en access_events, así que cada camino que lea o escriba sin pasar por la puerta es un acceso que nadie podrá atribuir jamás, ni a posteriori. Eso sube el listón de este approach: no es higiene, es la condición para que la auditoría signifique algo.


6 · Las fases

F1 · El firmado local, medido — ✅ HECHO (2026-08-04) · 5 verdes

Implementar la alternativa sin API y comprobarla con el mismo control negativo que el spike: leer dentro del prefijo, y que leer fuera dé AccessDenied.

Gate: 3 verdes como el spike de la API, más uno nuevo — que una credencial firmada localmente con actions de sólo lectura no pueda escribir.

F2 · La máquina, como superficie propia — ✅ HECHO (2026-08-04) · 7 verdes

Extraer mintTableCredential a una entrada interna con su política: TABLE_READ_DATA para leer, TABLE_WRITE_DATA para escribir — el privilegio existe desde C3 y hoy no abre nada. loadTable con delegación pasa a ser un cliente más.

Gate negativo: un principal con TABLE_READ_DATA y sin TABLE_WRITE_DATA que pida credencial de escritura no la recibe. Sin esto sólo se ha movido código de sitio.

F3 · El writer pide RW al commitear

Acotada al prefijo de su tabla. Su llave estática se retira del entorno.

Gate: facade-smoke verde sin LAKEHOUSE_S3_* en el entorno del writer. Las dos mitades cuentan: sin la primera, la segunda es una avería en diferido.

F4 · Duck pide RO por consulta

Instalando el secreto con SESSION_TOKEN y SCOPE al prefijo. Su llave estática se retira.

Gate: el SQL Editor lee, y grep '^LAKEHOUSE_S3_' en el entorno de duck da cero.

F5 · El trinquete, medido donde vive la verdad

Ningún servicio puede tener LAKEHOUSE_S3_ACCESS_KEY_ID salvo el que sostiene la credencial padre. Y se comprueba contra Railway, no contra el repo — la lección de hoy con AUTHZ_BACKEND=allowall: el repo dice lo que se quiso; el proceso dice lo que hay.


7 · Lo que este approach NO cubre

  • El bypass de catálogo de duck-server (se ata a Lakekeeper, no a la cara). Es el mismo agujero que se cerró para Trino, y va aparte — §2.
  • La atribución por inquilino en el almacenamiento. R2 no la da y no se puede arreglar desde aquí; lo único que se puede hacer es que ningún acceso ocurra fuera de la puerta.
  • El cifrado por inquilino. Otra frontera, otro approach.