Phase 07: Security, Roles & Compliance Auditing¶
Chapter 29: Role & Privilege Architecture¶
29.1 The Unified Role Concept¶
In PostgreSQL, "Users" and "Groups" are the exact same database object: a ROLE.
* A ROLE WITH LOGIN is what other databases call a "User".
* A ROLE WITHOUT LOGIN is what other databases call a "Group".
-- Production Best Practice: Create Group Roles, then assign to Login Roles
CREATE ROLE app_read_only NOINHERIT;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_read_only;
CREATE ROLE app_read_write;
GRANT app_read_only TO app_read_write;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_read_write;
-- Create actual human or service login accounts:
CREATE ROLE svc_web_backend WITH LOGIN PASSWORD 'strong_password';
GRANT app_read_write TO svc_web_backend;
29.2 Default Privileges (Preventing Permission Drifts)¶
- The Annoying Gotcha: When a migration creates a new table in a schema, existing roles cannot read it, even if you previously ran
GRANT SELECT ON ALL TABLES. - The Permanent Fix:
29.3 pg_hba.conf (Host-Based Authentication)¶
Controls who is permitted to connect to what database from where:
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
hostssl all all 10.0.0.0/16 scram-sha-256
host all all 0.0.0.0/0 reject
scram-sha-256: Obsoletes md5 (vulnerable to rainbow tables). Prevents password sniffing over the wire even if TLS is compromised.
* Require hostssl: Reject unencrypted plaintext TCP connections.
Chapter 30: Row-Level Security (RLS) & Multi-Tenancy¶
30.1 Building Multi-Tenant SaaS with RLS¶
Row-Level Security allows you to enforce multi-tenancy at the database engine level, ensuring tenant \(A\) can never view or modify tenant \(B\)'s data—even if an application bug omits WHERE tenant_id = ....
-- 1. Enable RLS on the table
ALTER TABLE customer_invoices ENABLE ROW LEVEL SECURITY;
-- 2. Force RLS even for the table owner (Critical!)
ALTER TABLE customer_invoices FORCE ROW LEVEL SECURITY;
-- 3. Define the tenant isolation policy
CREATE POLICY tenant_isolation_policy ON customer_invoices
AS PERMISSIVE
FOR ALL
TO app_user
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
Application Session Context Setup:¶
Before running queries on a pooled connection, the application sets the session variable:
SET LOCAL app.current_tenant_id = 'a54f89d2-3b2e-4a6f-9e2c-123456789abc';
-- All subsequent queries are automatically filtered:
SELECT * FROM customer_invoices;
-- PostgreSQL transparently appends: WHERE tenant_id = 'a54f89d2...'
30.2 RLS Performance Traps & Optimization¶
- Index Requirement: Every table with an RLS policy must have a high-performance index on the policy key (
tenant_id), or every query degrades into a sequential scan. - Subquery Traps: If an RLS policy includes an expensive subquery (e.g., checking user role memberships in another table), Postgres must execute that subquery for every evaluated tuple.
- Fix: Mark policy helper functions as
STABLEorLEAKPROOFso the query planner can optimize and cache evaluation across the scan.
Chapter 31: Compliance Auditing with pgaudit¶
31.1 Why Standard Postgres Logging is Insufficient¶
Standard logging (log_statement = 'all') logs raw SQL text strings. It produces gigabytes of unformatted text, captures passwords in plain text, and lacks structured compliance categorization required by SOC2, HIPAA, and PCI-DSS.
31.2 pgaudit Implementation¶
The pgaudit extension provides structured, tamper-evident audit logs directly to syslog:
-- postgresql.conf settings:
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'read, write, ddl, role'
pgaudit.log_relation = on
pgaudit.log_catalog = off
pgaudit.log_level = 'log'
READ, WRITE, DDL).
* Target relation names.
* Exact parameters and executed statements.