Multi-tenancy
Every tenant-owned table has a tenant_id. Isolation is enforced by two
independent layers, so a bug in one does not leak data.
Layer 1: the application
- Tenant routes contain
{tenantID}.TenantScopechecks that the caller belongs to that tenant (tenant_memberships) and answers 404 otherwise; global admins may use any active tenant. A user can belong to several tenants with a different role in each: permissions are checked against the role in the tenant of the URL, and suspended tenants drop out of the user's tenants. - Repository methods take the tenant first (
ListDevices(ctx, tenantID, filter)), and sqlc queries embedtenant_id = $1. - Cross-tenant listings exist only in
internal/adminand require a global admin. - An authorization fuzz test (82 cross-tenant cases) and the RBAC matrix test (every route × every kind of caller) keep this true.
Layer 2: PostgreSQL row-level security
Migration 0007 enables and forces RLS on every tenant-owned table.
- The server runs as the role
rmm_app(NOBYPASSRLS), connecting as it directly or switching to it withSET ROLE(RMM_DATABASE_ROLE). - The scope travels in the request context:
db.WithTenant(ctx, id): set byAuthorizefor tenant routes and by the agent and connector authentication;db.WithGlobal(ctx): workers, admin routes and CLI commands.
- A pgx
PrepareConnhook sets the session settingsapp.tenant_idorapp.bypassbefore each use of a pooled connection. Anrmm_appsession without them sees no rows. - Policies call
rmm_tenant_ok(tenant_id). Shared rows (tenant_id IS NULL: global packages, scripts, alert rules) are readable by every tenant. - Child tables check their parent. Composite foreign keys
(tenant_id, x_id) → parent(tenant_id, id)keep references inside one tenant, because foreign-key checks bypass RLS. audit_logis append-only forrmm_app; purging goes through theSECURITY DEFINERfunctionpurge_audit_log.COPYis not allowed under RLS, so bulk inserts stage into a temporary table (db.StagedCopy).- Non-leakproof operators cannot use indexes under RLS, so software filters
go through the
SECURITY DEFINERfunctionrmm_devices_with_software, which applies the same tenant rule.
Ad-hoc SQL as the application
SET ROLE rmm_app;
SET app.tenant_id = '<tenant uuid>';
SELECT count(*) FROM devices;
Consequences for operators
- PgBouncer must run in session mode: transaction pooling would mix the session settings of different tenants.
- Migrations and backups run as the owner
rmm, which bypasses RLS.