You're reading the v2.4.1 docs. View the latest (v2.5.8) →

Per-Door ACL — least privilege per integration

When your MongoDB ETL scraper, your Postgres BI tool, your S3 archive job, and your HTTP app all connect to one database, how do you grant each integration the least privilege it needs — and nothing more? In a traditional database the answer is "you can't, really" — every connection from the same user shares one principal, and an attacker who compromises the read-only scraper can write if the shared user can.

Relata gives every wire door its own ACL principal. A request arriving over the S3 door is principal s3-client; over pgwire it's pgwire-client; over MongoDB it's mongo-client. The principal is the door, not the user — so policy and audit are attached to the protocol a request actually used, regardless of which credentials the client presented.

Why this matters: every audit-log row is forensically attributable to the protocol that wrote it, and no door can slip past a policy another door obeys — adding a new protocol integration can't create a new bypass.

Note
Honest scope note: per-door identity and audit attribution are fully live today. Per-door differential grants ("S3 read-only but HTTP read-write") are a roadmap item — today's operator surface grants per type, reaching every door equally. See Writing per-door policies below.

The door principals

Every request is classified into exactly one door principal before the ACL evaluates. These are the roles the engine seeds and grants at startup (ALL_DOOR_ROLES):

DoorPrincipal
HTTP REST (also Neo4j/Cypher-over-HTTP)http-client
gRPCgrpc-client
PostgreSQL wire (psql, psycopg2, pgvector)pgwire-client
S3-compatibles3-client
Arrow Flightflight-client
ClickHouse HTTP / nativeclickhouse-client
MongoDB wiremongo-client
Redis RESPredis-client
Bolt (Neo4j binary)bolt-client
MCP (/mcp)mcp-client
Internal cluster shard-to-shard readscluster-shard-client

Beyond the doors, the engine knows a small set of non-door principals: system (trusted internal writers — never mintable from the wire), analyst and break-glass (read roles), admin (tenant membership management), and the irrevocable oversight/auditor audit-read roles (ADR-0261).

The door principal is independent of the user principal (the bearer token's identity) — both are recorded, so audit rows can answer "which protocol, acting for whom."

Writing per-door policies

Warning
Read this before copying Cedar examples from elsewhere. The policy engine is Cedar-inspired (custom implementation in relata-acl, with the cedar-policy crate available as a secondary evaluator), but as of the current release there is no operator surface to load Cedar permit/forbid policy documents — no env var, no route. Cedar syntax examples you may have seen do not apply. The sections below describe what you can actually configure today.

Today's operator surface is the engine's built-in policy table, seeded at startup and extended by environment variables. It evaluates deny-wins: any matching deny overrides all allows, so a compliance lock-down survives future grants someone adds.

Three levers cover real deployments:

  1. Type-level grantsRELATA_ACL_GRANT="TypeA:read+write,TypeB:read" grants every door role on the named types (and registers the types). Bare Type = read-only. Permissions: read, write, override.
  2. Column maskingRELATA_CELL_POLICIES="Person.ssn=mask,BankAccount.iban=tokenize" hides or tokenizes columns at egress — including for the door principals, regardless of row-level allows. tokenize requires RELATA_TOKENIZE_KEY.
  3. Purpose gatingRELATA_PURPOSE_MODE=strict + RELATA_PURPOSES=… rejects queries that don't declare a registered purpose, for every door equally.

Full copy-paste walkthroughs (including the fail-closed startup behavior on typos): Access Control & Permissions.

Note
What this means for least privilege: because one grant reaches every door, per-door differential access ("S3 read-only but HTTP read-write") is not expressible via env vars today — it's a tracked roadmap item. What you get today: per-type read/write granularity, column masking that no door can bypass, and audit rows that attribute every access to the door it arrived on.

Audit attribution — the forensic payoff

Every governed access and write lands in the hash-chained audit log with the principal (the door role it arrived as — s3-client, mongo-client, http-client, …) plus the authenticated caller identity and any declared purpose. A later investigation can answer "did this row come in over Mongo, S3, or HTTP?":

# Everything the S3 door touched:
curl 'http://127.0.0.1:9090/audit/entries?principal=s3-client&limit=50' \
  -H 'Authorization: Bearer <token>'
 
# All access to one type, ok outcomes only:
curl 'http://127.0.0.1:9090/audit/entries?type=Person&outcome=ok&limit=50' \
  -H 'Authorization: Bearer <token>'

Server-side filters: principal, purpose, type (substring matches), since/until (RFC 3339 or epoch-ns bounds), outcome=ok|error, limit (max 1000), offset/cursor for paging. /audit/count gives totals; /audit/proof returns a cryptographic inclusion proof that an entry is really in the chain.

This is tested in production paths (crates/relata-cli/tests/serve_hardening.rs — the s3-client door role is asserted to flow through governed_get into the audit log on every S3 door read).

Tips & takeaways

  • Default to least privilege per type. Since grants reach every door, treat your type list as the privilege boundary: new integrations read only what they need (RELATA_ACL_GRANT="TheirType" — read-only), and write grants go only to types that integrations actually write.
  • Hide columns instead of withholding types. RELATA_CELL_POLICIES masking applies to every door equally and survives any future read grant — deny-wins is your safety net for compliance lock-downs (Person.ssn=mask).
  • The door principal is the first factor, not the only one. Every audit entry records both the door role and the authenticated user identity, and in multi-tenant mode the door principal applies within each tenant's scope.
  • Audit by door to spot anomalies. "Why is the Redis door writing to Person?" is one query once you filter /audit/entries?principal=redis-client&type=Person.
  • Watch the startup log after changing grants. Each applied grant logs a line, and a malformed value refuses to start rather than silently weakening your policy.

See also