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
JWTsandbox - issuer sandbox esperado
- claims minimos requeridos para lectura interna
- mapeo de claim
rolehacia roles SQL aprobados - aislamiento entre
alpuntodeventa,ladirectayglobal - refuerzo de
RLScon el claimtenant_id - enforcement granular de
scopepor recurso sandbox - uso de un secreto
JWTfuera de Git - continuidad del sandbox sobre
pg-sandbox-internal
Que no valida¶
- publicacion publica de APIs
ScalarKongAPI keysproductivas- 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:
alpuntodeventaladirectaglobal
role¶
Es el rol SQL que PostgREST asume durante el request cuando el token es
valido.
Roles sandbox aprobados:
app_alpuntodeventa_readerapp_ladirecta_readerapp_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_idrolescopeaudissexp
Claims operativos adicionales emitidos por el generador:
iat
Ejemplo conceptual APV¶
tenant_id:alpuntodeventarole:app_alpuntodeventa_readerscope:read:clientes read:productos read:ventasaud:openclaw-sandbox-apiiss:openclaw-sandbox
Ejemplo conceptual La Directa¶
tenant_id:ladirectarole:app_ladirecta_readerscope:read:clientes read:productos read:ventasaud:openclaw-sandbox-apiiss:openclaw-sandbox
Ejemplo conceptual Global¶
tenant_id:globalrole:app_global_readerscope:read:allaud:openclaw-sandbox-apiiss: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:
PostgRESTsolo acepta tokens firmados con el secreto sandbox.- El claim
rolesolo puede mapear a roles SQL lectores ya aprobados. RLSsigue siendo la barrera real sobre cada tabla.- Las policies de
RLSse refuerzan con el claimtenant_id.
Ejemplo:
- si un token intenta usar rol APV con
tenant_id=ladirecta,RLSbloquea la lectura - si un token no es valido o esta expirado,
PostgRESTrechaza 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
RLSpueda leertenant_id
Ademas, una funcion db-pre-request valida:
- issuer esperado
- presencia de claims obligatorios
- consistencia entre
roleytenant_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,ventasy la raizOpenAPI - la funcion
db-pre-requestya validabaiss,role,tenant_idy presencia descope - el claim
scopese emite como string separado por espacios RLSsigue aislando portenant_id, pero no distingua todavia entreread:clientes,read:productosyread: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 isolationresponde a la pregunta "de que empresa son las filas"scope enforcementresponde a la pregunta "que recurso puede leer este token"
Ambos controles son complementarios:
scopefrena el acceso al recurso equivocadoRLSfrena 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 scopedesconocido- recurso fuera del contrato del sandbox
scopeinsuficiente para el recurso solicitado
Contrato inicial de scopes¶
/clientesrequiereread:clientesoread:all/productosrequiereread:productosoread:all/ventasrequiereread:ventasoread:all/tenantsrequiereread:allcomo catalogo global interno/queda accesible con token valido para conservar elOpenAPIinterno- 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
OpenAPIruntime 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
scopesya forman parte del contrato - el generador puede emitir presets por recurso
db-pre-requestaplica el contrato por recurso en runtimeRLSsigue siendo la barrera final de aislamiento por fila
Limitaciones restantes:
- el contrato cubre solo recursos de lectura del sandbox
- no hay
API keysproductivas - 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->healthyopenclaw-postgrest-sandbox->runningPortBindings->{}- red efectiva -> solo
pg-sandbox-internal - token
apv-clientes->/clientespermitido solo paraalpuntodeventa;/ventasy/productosrechazados con403 - token
apv-ventas->/ventaspermitido solo paraalpuntodeventa;/clientesrechazado con403 - token
ladirecta-productos->/productospermitido solo paraladirecta;/ventasrechazado con403 - token
global-all->/clientes,/productos,/ventasy/tenantspermitidos con visibilidad dealpuntodeventayladirecta - token sin
scope->403 - token con
scopedesconocido ->403 - token invalido ->
401 - token expirado ->
401 - token con
issinvalido ->403 - raiz
/->200solo 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 keyidentificaria owner y tenant - la key se rotaria con vencimiento controlado
- la key podria intercambiarse por un
JWTcorto 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.4es validarJWTyRLS - falta cerrar politica de exposicion y de ciclo de vida de credenciales