Saltar a contenido

Tenant Security Sandbox

Fecha: 2026-06-02

Objetivo de seguridad

Definir e implementar una capa sandbox interna para APIs multi-tenant basada en JWT, claims, tenant_id, roles y RLS, manteniendo PostgREST sin exposicion publica y sin tocar datos reales.

Que valida

  • firma y expiracion de JWT sandbox
  • issuer sandbox esperado
  • claims minimos requeridos para lectura interna
  • mapeo de claim role hacia roles SQL aprobados
  • aislamiento entre alpuntodeventa, ladirecta y global
  • refuerzo de RLS con el claim tenant_id
  • enforcement granular de scope por recurso sandbox
  • uso de un secreto JWT fuera de Git
  • continuidad del sandbox sobre pg-sandbox-internal

Que no valida

  • publicacion publica de APIs
  • Scalar
  • Kong
  • API keys productivas
  • escritura de datos
  • datos reales

Definiciones clave

tenant_id

Identifica el alcance real de los datos del request. En esta etapa los valores aprobados son:

  • alpuntodeventa
  • ladirecta
  • global

role

Es el rol SQL que PostgREST asume durante el request cuando el token es valido.

Roles sandbox aprobados:

  • app_alpuntodeventa_reader
  • app_ladirecta_reader
  • app_global_reader

scope

Es el alcance funcional declarado por el token. En 15.4b ya se valida en runtime por recurso mediante db-pre-request, mientras que RLS sigue siendo la barrera final del aislamiento por fila.

claim

Es un dato contenido dentro del JWT. En esta etapa se usan para transportar tenant_id, role, scope, aud, iss y exp.

API key

Es una credencial futura pensada para mapearse a claims o a un JWT derivado. No se implementa todavia en este sandbox.

JWT

Es el token firmado que PostgREST valida antes de aceptar el request.

Contrato JWT sandbox

Claims minimos:

  • tenant_id
  • role
  • scope
  • aud
  • iss
  • exp

Claims operativos adicionales emitidos por el generador:

  • iat

Ejemplo conceptual APV

  • tenant_id: alpuntodeventa
  • role: app_alpuntodeventa_reader
  • scope: read:clientes read:productos read:ventas
  • aud: openclaw-sandbox-api
  • iss: openclaw-sandbox

Ejemplo conceptual La Directa

  • tenant_id: ladirecta
  • role: app_ladirecta_reader
  • scope: read:clientes read:productos read:ventas
  • aud: openclaw-sandbox-api
  • iss: openclaw-sandbox

Ejemplo conceptual Global

  • tenant_id: global
  • role: app_global_reader
  • scope: read:all
  • aud: openclaw-sandbox-api
  • iss: openclaw-sandbox

No se documentan tokens reales, no se imprimen secretos y no se guardan ejemplos reutilizables en Git.

Como se evita acceso cruzado entre tenants

La defensa se apoya en capas:

  1. PostgREST solo acepta tokens firmados con el secreto sandbox.
  2. El claim role solo puede mapear a roles SQL lectores ya aprobados.
  3. RLS sigue siendo la barrera real sobre cada tabla.
  4. Las policies de RLS se refuerzan con el claim tenant_id.

Ejemplo:

  • si un token intenta usar rol APV con tenant_id=ladirecta, RLS bloquea la lectura
  • si un token no es valido o esta expirado, PostgREST rechaza antes de llegar a la base

Relacion con PostgreSQL RLS

RLS conserva la autoridad final del aislamiento.

La etapa 15.4 no reemplaza RLS por la URL ni por el token. El token solo transporta identidad y contexto; la base decide que filas son visibles.

Relacion con PostgREST

PostgREST hace tres cosas principales:

  • valida el JWT
  • asume el rol SQL indicado por el claim role
  • expone los claims del request para que RLS pueda leer tenant_id

Ademas, una funcion db-pre-request valida:

  • issuer esperado
  • presencia de claims obligatorios
  • consistencia entre role y tenant_id
  • contrato de recurso permitido para el sandbox
  • scope requerido por recurso

Scope Enforcement Sandbox

Hallazgos de diagnostico

  • el sandbox sigue exponiendo internamente tenants, clientes, productos, ventas y la raiz OpenAPI
  • la funcion db-pre-request ya validaba iss, role, tenant_id y presencia de scope
  • el claim scope se emite como string separado por espacios
  • RLS sigue aislando por tenant_id, pero no distingua todavia entre read:clientes, read:productos y read:ventas

Que es un scope

Un scope es un permiso funcional puntual que limita a que recurso puede acceder un token ya autenticado.

Que problema resuelve

Evita que un token lector de un tenant use el mismo rol SQL para leer recursos que no fueron aprobados para ese caso de uso.

Diferencia entre tenant isolation y scope enforcement

  • tenant isolation responde a la pregunta "de que empresa son las filas"
  • scope enforcement responde a la pregunta "que recurso puede leer este token"

Ambos controles son complementarios:

  • scope frena el acceso al recurso equivocado
  • RLS frena el acceso a filas de otro tenant aunque el recurso sea correcto

Donde se valida el scope

Se valida dentro de PostgreSQL en sandbox_private.enforce_postgrest_claims, ejecutada por PGRST_DB_PRE_REQUEST.

La funcion usa current_setting('request.path', true) para mapear el recurso interno al scope requerido y rechaza:

  • token sin scope
  • scope desconocido
  • recurso fuera del contrato del sandbox
  • scope insuficiente para el recurso solicitado

Contrato inicial de scopes

  • /clientes requiere read:clientes o read:all
  • /productos requiere read:productos o read:all
  • /ventas requiere read:ventas o read:all
  • /tenants requiere read:all como catalogo global interno
  • / queda accesible con token valido para conservar el OpenAPI interno
  • cualquier otro recurso queda fuera de alcance y se rechaza

Que queda fuera de alcance

  • escritura
  • RPC
  • versionado de permisos mas fino que lectura por recurso
  • exposicion publica
  • datos reales

Se mantiene interno en:

  • red pg-sandbox-internal
  • sin PortBindings
  • sin proxy-network

Relacion futura con Scalar y API Docs

developers.alpuntodeventa.com.ar queda reservado para 15.6 como DNS futuro de Scalar y de la API Documentation Platform.

En 15.4:

  • no se usa ese DNS
  • no se instala Scalar
  • el OpenAPI runtime sigue accesible solo en la red interna

Secreto JWT y gestion segura

Reglas:

  • vivir fuera de Git
  • cargarse desde archivo local no versionado
  • no aparecer en logs
  • no aparecer en docs
  • no commitearse en .env.example

Ubicacion sugerida:

  • infra/data-foundation/postgrest-sandbox/secrets/postgrest_jwt_secret

Generador de tokens sandbox

Script aprobado:

  • infra/data-foundation/postgrest-sandbox/tools/generate-sandbox-jwt.py

Reglas:

  • no trae secretos hardcodeados
  • lee desde archivo o variable local
  • soporta presets por tenant y por recurso
  • permite override explicito incluso con --scope ''
  • permite expiracion corta
  • imprime solo el token

Estado de scopes en 15.4b

Situacion actual:

  • los scopes ya forman parte del contrato
  • el generador puede emitir presets por recurso
  • db-pre-request aplica el contrato por recurso en runtime
  • RLS sigue siendo la barrera final de aislamiento por fila

Limitaciones restantes:

  • el contrato cubre solo recursos de lectura del sandbox
  • no hay API keys productivas
  • no hay auditoria de consumo por endpoint mas alla del runtime actual

Evidencia runtime final en VPS

Validacion ejecutada el 2026-06-02 sobre openclaw-postgrest-sandbox en pg-sandbox-internal, sin publicar puertos al host.

Resultados confirmados:

  • openclaw-postgres-sandbox -> healthy
  • openclaw-postgrest-sandbox -> running
  • PortBindings -> {}
  • red efectiva -> solo pg-sandbox-internal
  • token apv-clientes -> /clientes permitido solo para alpuntodeventa; /ventas y /productos rechazados con 403
  • token apv-ventas -> /ventas permitido solo para alpuntodeventa; /clientes rechazado con 403
  • token ladirecta-productos -> /productos permitido solo para ladirecta; /ventas rechazado con 403
  • token global-all -> /clientes, /productos, /ventas y /tenants permitidos con visibilidad de alpuntodeventa y ladirecta
  • token sin scope -> 403
  • token con scope desconocido -> 403
  • token invalido -> 401
  • token expirado -> 401
  • token con iss invalido -> 403
  • raiz / -> 200 solo con token valido
  • revision de logs operativos -> sin secretos ni tokens

API keys como estrategia futura

Se documentan pero no se implementan todavia.

Modelo esperado:

  • una API key identificaria owner y tenant
  • la key se rotaria con vencimiento controlado
  • la key podria intercambiarse por un JWT corto o mapearse a claims
  • quedaria asociada a tenant_id, scope, owner, vencimiento y auditoria

No se implementa ahora porque:

  • el sandbox todavia es interno
  • la prioridad de 15.4 es validar JWT y RLS
  • falta cerrar politica de exposicion y de ciclo de vida de credenciales