Skip to main content

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}. TenantScope checks 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 embed tenant_id = $1.
  • Cross-tenant listings exist only in internal/admin and 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 with SET ROLE (RMM_DATABASE_ROLE).
  • The scope travels in the request context:
    • db.WithTenant(ctx, id): set by Authorize for tenant routes and by the agent and connector authentication;
    • db.WithGlobal(ctx): workers, admin routes and CLI commands.
  • A pgx PrepareConn hook sets the session settings app.tenant_id or app.bypass before each use of a pooled connection. An rmm_app session 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_log is append-only for rmm_app; purging goes through the SECURITY DEFINER function purge_audit_log.
  • COPY is 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 DEFINER function rmm_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.