Skip to content

Row-Level Security

All tenant isolation in KVM Fleet is enforced at the database level using Postgres Row-Level Security (RLS).

Design

The API connects to Postgres as the kvmfleet_app role — a non-superuser with FORCE ROW LEVEL SECURITY enabled. This means RLS policies apply even if the role owns the tables.

ALTER ROLE kvmfleet_app SET row_security = on;
ALTER TABLE devices FORCE ROW LEVEL SECURITY;
ALTER TABLE audit_log FORCE ROW LEVEL SECURITY;
-- ... (all tenant-scoped tables)

How it works

Before each request, the API sets a session variable with the authenticated user's org ID:

SET LOCAL app.current_org_id = 'org_abc123';

RLS policies filter all queries automatically:

CREATE POLICY org_isolation ON devices
    USING (org_id = current_setting('app.current_org_id')::uuid);

CREATE POLICY org_isolation ON audit_log
    USING (org_id = current_setting('app.current_org_id')::uuid);

What this means

  • A bug in the API code cannot leak data across organizations
  • Even raw SQL injection would only return data for the attacker's own org
  • No application-level WHERE org_id = ? filters needed (but they're there anyway as defense-in-depth)

Tables with RLS

All tenant-scoped tables have RLS enabled:

  • devices
  • audit_log
  • users (scoped to org membership)
  • alert_rules
  • alert_history
  • console_sessions
  • invites
  • reports

Audit log protection

The audit_log table has an additional restriction: the kvmfleet_app role only has INSERT and SELECT — no UPDATE or DELETE. Combined with the hash chain, this provides strong tamper evidence.

GRANT SELECT, INSERT ON audit_log TO kvmfleet_app;
-- No UPDATE or DELETE granted

Testing RLS

RLS policies are tested in CI by:

  1. Inserting data for two different orgs
  2. Setting app.current_org_id to org A
  3. Asserting that queries only return org A's data
  4. Attempting cross-org access and asserting zero rows returned