Tenant Isolation
The product's core promise to regulated buyers - one customer's data can never reach another, with defense in depth.
The product's core promise to regulated buyers: one customer's data can never reach another. Defense in depth — isolation at compute, data, network, and query layers.
The silo model
Each tenant gets a fully dedicated stack (the full-stack-silo deployment model): its own Cloud Run service, Cloud SQL Postgres, Mongo database, GCS bucket, secrets, KMS key, and (optionally) custom domain. Nothing in the data path is shared between tenants. This is the costliest model operationally — and the easiest to reason about for compliance and the smallest blast radius if one tenant is compromised.
Layers of isolation
| Layer | Mechanism |
|---|---|
| Compute | Cloud Run service per tenant, own service account; no shared process memory |
| Data (PG) | Cloud SQL instance per tenant + row-level security as defense-in-depth |
| Data (Mongo) | DB-per-tenant, scoped Mongo user; private networking |
| Blobs | GCS bucket per tenant, CMEK with the tenant's key |
| Secrets | Secret Manager namespace + service account scoped per tenant |
| Network | per-tenant VPC connector; private IPs for SQL/Mongo; no public DB listeners |
| Query/RAG | every query injects tenant_id; vector search filters on tenant_id (22, ai/31) |
| AuthZ | hard cross-tenant deny in the policy engine (security/41) |
Row-level security (even in a silo)
Even though each tenant has its own database, RLS is enforced so a forgotten WHERE can't leak across the in-tenant boundaries that matter (e.g. wargame cells, classification):
ALTER TABLE observations ENABLE ROW LEVEL SECURITY;
CREATE POLICY obs_visibility ON observations USING (
tenant_id = current_setting('app.tenant_id')::uuid
AND ( classification = 'OPEN'
OR current_setting('app.cell', true) IS NULL
OR cell_id = current_setting('app.cell', true)::uuid )
);The application sets app.tenant_id / app.cell per transaction (security/40). RLS is not the primary tenant boundary (the separate DB is) — it's the layer that holds when application code is wrong.
The wargame lesson (a real isolation bug class)
Faculty need a visibility bypass (the umpire sees all cells) but must not inherit an approval bypass. Keep visibility and approval as independent policies; never let one imply the other. Each is enforced in the DB/policy engine, not in app code.
Cross-tenant surfaces (audit them)
The only components that legitimately span tenants are the control plane (reads aggregate telemetry, never raw data without logged break-glass) and shared infra (the connector catalog, the container image). Audit these especially: jobs that "scan all data," caches without a tenant key, admin queries that don't understand placement.
Verification
- Automated tests that attempt cross-tenant reads/writes and assert denial (in CI).
- A periodic "isolation audit" job: sample queries with a wrong
tenant_idmust return zero rows. - Pen-test scope includes tenant-escape attempts (
security/44).
Gaps to close
- Confirm VPC/private-endpoint topology for Cloud SQL + Atlas.
- Decide Redis isolation (prefix vs. instance-per-tenant).
- Write the cross-tenant negative tests and the isolation-audit job.
Secrets & Key Management
Per-tenant secrets and envelope encryption on Google Cloud - no secret ever lives in code, the app database, or the browser.
Security Program & Scanning
How you keep the software highly secure over time - secure SDLC, CI scanning, threat model, runtime defenses, IR, and compliance.