ibexharness
DocsBlogReleasesRoadmap
GitHub
ibexharness

Documentation

Security overviewAuthenticationTenant isolationSecrets and keys
Security›Tenant isolation
Security

Tenant isolation

Row-level security, Redis namespacing, and defense-in-depth for multi-tenant data.

Org A must never read or mutate Org B's data — via API, Postgres, Redis, logs, or analytics. IBEX Harness enforces isolation at multiple layers so a single regression cannot bypass tenancy. Phase 1 protects core identity tables; memory and ClickHouse guards expand in later phases.

P1 incident

Any suspected cross-tenant data leak is severity P1. Freeze deploys, enable enhanced audit logging, and follow the incident response runbook.

Defense in depth

Mermaid diagram: graph TB
  +-------------------------------+                                   
  |                               |                                   
  |             Client            |                                   
  |                               |                                   
  +-------------------------------+                                   
                  |                                                   
                  |                                                   
                  |                                                   
                  |                                                   
                  |                                                   
+-----------------|--------------------------------------------------+
|                 |       Isolation layers                           |
|                 |                                                  |
|                 v                                                  |
| +-------------------------------+                                  |
| |                               |                                  |
| | HTTP: org from verified token |                                  |
| |                               |                                  |
| +-------------------------------+                                  |
|                 |                                                  |
|                 |                                                  |
|                 |                                                  |
|                 |                                                  |
|                 v                                                  |
| +-------------------------------+                                  |
| |                               |                                  |
| |    gRPC: org_id in metadata   |                                  |
| |                               |                                  |
| +-------------------------------+                                  |
|                 |                                                  |
|                 |                                                  |
|                 |                                                  |
|                 |                                                  |
|                 v                                                  |
| +-------------------------------+                                  |
| |                               |                                  |
| |  Application: org_id in WHERE |                                  |
| |                               |                                  |
| +-------------------------------+                                  |
|                 |                                                  |
|                 |                                                  |
|                 +-----------------------------------+              |
|                 |                                   |              |
|                 v                                   v              |
| +-------------------------------+     +--------------------------+ |
| |                               |     |                          | |
| |     Postgres: RLS policies    |     | Redis: org-prefixed keys | |
| |                               |     |                          | |
| +-------------------------------+     +--------------------------+ |
|                                                                    |
+--------------------------------------------------------------------+

Each layer is independent. If RLS is misconfigured, application queries still filter by org. If application code regresses, RLS denies cross-tenant rows.

PostgreSQL row-level security

Every tenant table in ibex_core includes org_id. RLS policies enforce:

SQL
org_id = current_setting('app.current_org_id')::uuid
1

Set org context per transaction

Every request runs SET LOCAL app.current_org_id = '{org_id}' inside a transaction scope — never globally on the pooled connection.

2

Fail closed on missing context

If org context cannot be set, the operation is denied rather than running unscoped queries.

3

Verify policies in dev

After make db-migrate, confirm RLS is enabled on core tables.

Phase 1 protected tables

TablePolicy
ibex_core.organizationsOrg-scoped reads
ibex_core.tokensOrg-scoped CRUD
ibex_core.agentsOrg-scoped CRUD
ibex_core.usersOrg-scoped membership

See Multi-tenant RLS for migration and verification commands.

Use SET LOCAL, not SET

SET app.current_org_id without LOCAL can leak org context across pooled connections. Always scope context to the current transaction.

Application layer

Even with RLS enabled, every store query includes org_id in the WHERE clause:

  • Makes intent obvious in code review
  • Reduces blast radius if a policy is disabled during migration
  • Required for services that do not yet have RLS (ClickHouse)

Cross-tenant access attempts return 403 PERMISSION_DENIED, not 404 Not Found. Returning 404 tells an attacker the resource UUID exists in another org.

Redis namespacing

Phase 1 proxy uses Redis for org-level rate limiting and readiness checks. Keys must include org_id as a segment:

PatternExample
Rate limitratelimit:{org_id}:minute:{unix_minute}
Hot memory (Phase 2+){org_id}:hot_memories:{agent_id}
Memory cache (Phase 2+){org_id}:memory:{memory_id}

Global keys are allowed only for explicitly labeled shared metadata (e.g. token revocation broadcast). Never store tenant data under a global key.

Phase 1 Redis scope

Only the proxy service connects to Redis in Phase 1. Memory and context caching adopt the same namespacing rules when those services ship.

ClickHouse and analytics

ClickHouse has no row-level security. Every query must include an explicit org_id filter. Production configs enable CLICKHOUSE_ORG_FILTER_ENFORCEMENT=true so the query guard rejects unscoped statements.

Per-org breakdowns belong in ClickHouse analytics, not Prometheus labels — high-cardinality labels like org_id are forbidden on metrics (ADR-0021).

Audit isolation

Audit logs are append-only and org-scoped at the application layer. Cross-tenant access attempts (should be impossible) emit audit warnings for forensic analysis.

Verification checklist

1

Integration tests

Cross-org read tests must return empty or 403 — never another org rows. CI security-integration job covers proxy auth boundaries.

2

RLS enabled

pg_tables shows rowsecurity = true on ibex_core tenant tables after migrations.

3

Redis key review

New cache or rate-limit keys include org_id; no tenant payload under global prefixes.

4

Error semantics

Cross-tenant agent or resource access returns 403, not 404, across proxy and auth paths.

Related

  • Auth org and project model
  • ADR-0005: Postgres migration strategy
  • Security overview

Was this page helpful?

Edit on GitHub

Last updated on

PreviousAuthenticationNextSecrets and keys

On this page

  • Defense in depth
  • PostgreSQL row-level security
  • Phase 1 protected tables
  • Application layer
  • Redis namespacing
  • ClickHouse and analytics
  • Audit isolation
  • Verification checklist
  • Related
0%