Multi-Tenant Security
Shoehorn uses PostgreSQL Row-Level Security (RLS) to provide database-level tenant isolation. This ensures that tenants cannot access each other’s data, even if application-level bugs exist.
How RLS Works
Section titled “How RLS Works”- Every data table has a
tenant_idcolumn - RLS policies filter all SELECT, INSERT, UPDATE, and DELETE operations
- Before each query, the session variable
app.current_tenant_idis set - PostgreSQL automatically enforces tenant isolation at the database level
-- Example RLS policyCREATE POLICY tenant_isolation_select ON entities FOR SELECT USING (tenant_id = current_setting('app.current_tenant_id')::text);Database User Model
Section titled “Database User Model”Shoehorn always uses a two-user model for database security. There is no single-user mode.
# Migration user - schema changes (BYPASSRLS)MIGRATION_DATABASE_URL=postgresql://shoehorn_user:pass@host:5432/shoehorn
# Runtime user - application queries (NOBYPASSRLS)DATABASE_URL=postgresql://app_user:pass@host:5432/shoehorn| User | Purpose | Can Bypass RLS |
|---|---|---|
shoehorn_user | Database owner, schema migrations | Yes |
app_user | All runtime queries | No (NOBYPASSRLS) |
The app_user has NOBYPASSRLS, meaning RLS policies are always enforced. Even for single-tenant deployments, RLS runs with a fixed tenant ID injected by the middleware.
Tenant ID Extraction
Section titled “Tenant ID Extraction”The tenant ID is extracted from the authenticated user’s JWT claims. It is never taken from client input.
Extraction Strategies
Section titled “Extraction Strategies”| Strategy | Configuration | Source |
|---|---|---|
provider_claim | TENANT_ID_CLAIM_KEY=azp | JWT claim value |
urn_claim | TENANT_ID_CLAIM_KEY=urn:zitadel:iam:org:id | URN in JWT claim |
group_prefix | TENANT_GROUP_PREFIX=tenant: | Group membership prefix |
env_var | DEFAULT_TENANT_ID=default | Environment variable fallback |
For API Keys
Section titled “For API Keys”The tenant ID is stored alongside the API key in the database. When the key is validated, the tenant is loaded from the key record.
What’s Protected
Section titled “What’s Protected”Every table storing user or tenant-specific data has RLS enabled:
catalog_entities- Service catalogteams- Team definitionsteam_members- Team membershiprepositories- Repository datagovernance_actions- Governance trackinggitops_resources- GitOps statek8s_cluster_instances- Kubernetes workloadsscorecard_definitions- Scorecard rulesapi_keys- API key records- And all other tenant-scoped tables
Compliance
Section titled “Compliance”The RLS model provides:
- SOC 2 compliance: Database-level access control
- HIPAA-ready: Data isolation at the storage layer
- Audit trail: All data access is tenant-scoped
- Defense in depth: Even application bugs cannot leak cross-tenant data