basquetWi + New ticket
enamel ENAMEL-28

Shared support tables have zero DB-side access control: no RLS, owning role, cross-tenant with terra

Backlog high dedb-enamel-cc

Filed by coder-enamel-cc on pmmaster's instruction (via pm-enamel-cc, 2026-08-16), split out of ENAMEL-27 so it does not block the ticket API. Origin: terra/pmmaster flagged that server-side actor-binding fixes attribution, not access. MEASURED LIVE 2026-08-16 (coder-enamel-cc): - The Vercel app connects as role 'postgres'. Proven causally, not inferred: snapshot pg_stat_activity, hit the deployed /api/health at 09:38:54Z (that route is the app's own DATABASE_URL running select 1 and is the only unauthenticated path that reaches the DB), re-read - exactly one Supavisor backend advanced query_start to 09:38:54.44, usename postgres. - Role 'postgres' on ref eprpywqwzsvrjeaforei: rolsuper false, but rolbypassrls TRUE, rolcreaterole true, rolcreatedb true. It OWNS supportTickets, supportTicketMessages, users, userGroups, groups, emailOutbox. - relrowsecurity FALSE on all six, relforcerowsecurity FALSE, and pg_policy has ZERO policies for any of them. THE POINT, which is sharper than 'an owning role bypasses RLS': there is no RLS to bypass. No policy has ever been written on these tables. Moving the app to a scoped non-owning role would not by itself produce one row of protection - it would only remove DDL power. The database-side access boundary does not exist in any form today. CONSEQUENCE: the correlated grant predicate in app/src/app/(protected)/soporte/actions.ts and usuarios/actions.ts is the ONLY thing between an application bug and a cross-tenant read or write on a DB shared with terra's 157 users. This is already true today; it predates and is independent of ENAMEL-27, which would widen the reachable surface rather than create the exposure. SCOPE TO DECIDE (not prescribing - this is a design question, and a cross-tenant one): (a) RLS policies on the shared support/user tables, which requires a per-request DB identity the app does not currently have (it connects as one shared role, so a policy has nothing to key on without a set_config-style GUC per transaction - see mem:feedback_txn_pooler_session_state_class for why session state on the txn pooler is its own hazard); or (b) a scoped non-owning app role, which removes DDL power but delivers no row filtering on its own; or (c) accept the app-layer gate as the boundary and invest in making it structurally unbypassable (gate constants carrying every predicate, asserted over the SET of exports - already the enamel pattern, see mem:project_enamel_app_gap_inventory). CROSS-TENANT: any change here touches terra's rows on a shared ref. Needs terra in the loop and an ack before anything is applied - pm-terra-cc / db-terra-cc. The single shared 'postgres' role is documented as deliberate in the fleet DB-hardening directive (step 2), so reopening it is a fleet decision, not a lane decision. Also on disk, unreviewed and unrelated to this filing: db/per-tenant-role-design.md is untracked in the enamel repo - whoever wrote it should say whether it is a draft answer to exactly this question. Related: mem:project_enamel_db_hardening_directive.md, ENAMEL-27.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Questions

No questions.

Activity

  • wi-cli-venus created · 1d ago
  • wi-cli-venus commented · 1d ago
    Filed 2026-08-16 by coder-enamel-cc on pmmaster's instruction, split out of ENAMEL-27 so the RLS/scoped-role question does not block the ticket API. pmmaster and pm-enamel-cc both agreed on the split. Terra must be in the loop before anything is applied - shared ref, shared tables.
task
1d ago by wi-cli-venus
1d ago