Tenant isolation — Row-Level Security
Every piece of tenant data in Causeloop carries aworkspace_id column. PostgreSQL Row-Level Security (RLS) enforces that a database session can only read and write its own workspace’s rows — even if the application layer had a bug and issued a query without a workspace filter.
How it works
At the start of every request, the application sets a session-local GUC:app_current_tenant() reads this GUC. Every tenant table has four policies:
FORCE ROW LEVEL SECURITY means the table owner is also subject to the policies in the application role’s session.
The two-role model
RLS is only effective if the application connects as a role withNOBYPASSRLS. This is the most critical operational security requirement:
RLS coverage
Migration0001_force_rls.sql enables FORCE ROW LEVEL SECURITY on all tenant tables and the audit log. As of the current codebase, RLS policies cover approximately 18 of ~45 tenant tables; complete coverage across all tables is an active work item. See SOC 2 readiness for the current gap status.
Envelope encryption — KEK / DEK
Secrets stored in the database (connector configurations, webhook signing secrets, MFA factor seeds) are encrypted with AES-256-GCM using a two-layer envelope scheme.Architecture
- KEK (Key Encryption Key): the
CAUSELOOP_MASTER_KEYenvironment variable — a base64-encoded 32-byte AES-256 key that lives outside the database. It never touches the database. - DEK (Data Encryption Key): a per-workspace 256-bit AES key, generated on first use, stored in
workspace_keysin wrapped (encrypted) form. - Ciphertext layout:
version(1 byte) | nonce(12 bytes) | ciphertext+tag
Implementation (app/crypto/envelope.py)
workspace_id is used as Additional Authenticated Data (AAD), preventing ciphertext from one workspace being replayed into another.
DEK lifecycle
DEKs are generated lazily on first encrypt, stored inworkspace_keys, and cached in-process with an lru_cache keyed on the KEK fingerprint. The cache is automatically invalidated when the master key changes.
Key rotation: rotate the KEK by generating a new CAUSELOOP_MASTER_KEY, re-wrapping all DEKs, and updating workspace_keys. This is a planned operator runbook; per-workspace rotation is supported in the schema via the key_rotation_jobs table.
What is encrypted
Note:
mfa_factors.secret_encrypted and connectors.config_encrypted columns exist in the schema. App-layer KMS encryption for these columns is partially implemented; full wiring is an active work item. See SOC 2 readiness.
Authentication and authorization
Token types
RBAC
Causeloop uses a typed permission catalogue. Roles (in descending privilege):
Permissions are checked via
require_scope("scope:action") decorators on route handlers. Example scopes: governance:write, audit:read, connectors:write.
Rate limiting
The application enforces a default token-bucket rate limit of 1,000 requests per minute across all routes, implemented inapp/middleware/rate_limit.py. Limits apply per client IP.
Idempotency
POST endpoints that create resources accept an Idempotency-Key header. Duplicate requests with the same key within the deduplication window return the cached response without re-executing the operation.
Inbound webhook HMAC verification
Inbound webhooks from third-party services are verified using HMAC-SHA256. The verification is fail-closed: if thex-causeloop-signature header is absent or the signature does not match, the request is rejected with 401 Unauthorized.
x-causeloop-signature header. The receiver compares it using hmac.compare_digest (constant-time comparison, prevents timing attacks).
Fail-closed means: if the signature header is missing, or the HMAC secret has not been set for the inbound endpoint, the request is rejected. The application never falls through to process an unverified payload.
Transport security
- TLS 1.3 via Caddy with automatic Let’s Encrypt certificates
Strict-Transport-Security: max-age=31536000response headerX-Content-Type-Options: nosniffX-Frame-Options: DENYServerheader suppressed by Caddy- CORS restricted to an explicit allowlist (
CORS_ORIGINSenv var)
Audit log
Every significant action in the system is recorded in theaudit_log table:
0002_audit_trace_append_only.sql (an UPDATE/DELETE trigger that raises an exception). It is scoped by RLS, so each workspace can only read its own audit trail.
Audit entries are covered by workspace retention policy via workspace_settings.audit_log_retention_days.
Secrets management
In production, treat these as high-sensitivity secrets:
Store these in a secrets manager, not in
.env files checked into source control. For Railway, use the dashboard secrets panel. For Render, mark them as Secret environment variables. For VPS deploys, use Docker secrets or a vault solution.
Related pages
- Database setup — RLS two-role model
- SOC 2 readiness — current control status
- GDPR & data governance