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.
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):
| Door | Principal |
|---|---|
| HTTP REST (also Neo4j/Cypher-over-HTTP) | http-client |
| gRPC | grpc-client |
| PostgreSQL wire (psql, psycopg2, pgvector) | pgwire-client |
| S3-compatible | s3-client |
| Arrow Flight | flight-client |
| ClickHouse HTTP / native | clickhouse-client |
| MongoDB wire | mongo-client |
| Redis RESP | redis-client |
| Bolt (Neo4j binary) | bolt-client |
MCP (/mcp) | mcp-client |
| Internal cluster shard-to-shard reads | cluster-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
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:
- Type-level grants —
RELATA_ACL_GRANT="TypeA:read+write,TypeB:read"grants every door role on the named types (and registers the types). BareType= read-only. Permissions:read,write,override. - Column masking —
RELATA_CELL_POLICIES="Person.ssn=mask,BankAccount.iban=tokenize"hides or tokenizes columns at egress — including for the door principals, regardless of row-level allows.tokenizerequiresRELATA_TOKENIZE_KEY. - Purpose gating —
RELATA_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.
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_POLICIESmasking 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
- Access Control & Permissions — the entry-level, copy-paste how-to for grants, masking, and 403 troubleshooting
- Auth & Security — bearer tokens, OIDC, mTLS, the full ACL model
- Governance — Cedar-inspired ABAC, PURPOSE tracking, cell masking
- Compatibility & Doors — the wire doors this principal set covers
- Deploying Protocol Doors — door bind/publish/enable wiring
- Audit & Provenance — the audit chain that records every door's writes