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 solves this by giving every wire door a distinct Cedar principal. A request arriving over the S3 door is principal s3-client; over pgwire it's pgwire-client; over MongoDB it's mongo-client. One Cedar policy can say "the S3 door may read Person but not write it" — and that holds regardless of which credentials the S3 client used, because the principal is the door, not the user.

Why this matters: turn "this read-only S3 scraper can't write even if compromised" into a one-liner, and make every audit-log row forensically attributable to the protocol that wrote it.

The 11 door principals

Every request is classified into exactly one door principal before the ACL evaluates:

DoorCedar principal
HTTP RESThttp-client
gRPCgrpc-client
PostgreSQL wire (psql, psycopg2, pgvector)pgwire-client
MongoDB wiremongo-client
Redis RESPredis-client
Neo4j HTTP (Cypher)neo4j-client
Boltbolt-client
ClickHouse HTTP / nativeclickhouse-client
S3-compatibles3-client
Arrow Flightflight-client
MCP (/mcp)mcp-client
Server-internalsystem

The door principal is independent of the user principal (the bearer token's identity) and the session principal — Cedar sees all three, so a policy can express "the S3 door acting for user U in tenant T."

Writing per-door policies

The door principal is exposed in Cedar as principal == User::"<door>-client". Combine with the user, action, and resource as usual:

// The S3 door may read Person and S3Object but never write Person.
permit(
  principal == User::"s3-client",
  action   == Action::"read",
  resource in Resource::"Person"
);
permit(
  principal == User::"s3-client",
  action   == Action::"read",
  resource in Resource::"S3Object"
);
// No permit clause for s3-client + Action::"write" + Person → denied by default.
// pgwire gets full Person read/write — your BI tool's psql connection.
permit(
  principal == User::"pgwire-client",
  action in [Action::"read", Action::"write"],
  resource in Resource::"Person"
);
// The Mongo door is write-only (ingest only, no exfiltration).
permit(
  principal == User::"mongo-client",
  action    == Action::"write",
  resource  in Resource::"MongoDocument"
);

Deny-wins: an explicit forbid overrides any permit, so a compliance lock-down is one line:

// No door may read the `ssn` column on Person, regardless of who asks.
forbid(
  principal,
  action == Action::"read",
  resource == Resource::"Person.ssn"
);

Faster than Cedar — the env-var grant shorthand

For common cases, skip the policy file entirely and use the RELATA_ACL_GRANT env var (now targets ALL_DOOR_ROLES):

# Person: read for every door, write only for pgwire + http
RELATA_ACL_GRANT="Person:read+write"  RELATA_ACL_GRANT_PGWIRE="Person:write" \
relata serve

This compiles into Cedar at startup; you can mix and match with hand-written policy files.

Audit attribution — the forensic payoff

Every governed write/flip carries the door principal in the audit log as the purpose field — so a later investigation can answer "did this row come in over Mongo, S3, or HTTP?":

# Every row the S3 door touched:
curl 'http://127.0.0.1:9090/audit/entries?purpose=s3-client&limit=50' \
  -H 'Authorization: Bearer <token>'
-- Which doors have written to Person in the last 24h?
SELECT purpose AS door, COUNT(*) AS writes
FROM _audit
WHERE resource_type = 'Person'
  AND system_at > now() - INTERVAL '24' hours
GROUP BY purpose;

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 door. A new integration gets a new door principal with read only; escalate to write only when the integration needs it. This is the single biggest containment win for the "compromised scraper" threat.
  • The door principal is the first factor, not the only one. Combine with user principal (who) and tenant (where) for full ABAC — principal == User::"s3-client" && resource.tenant == "org-acme".
  • Use forbid for compliance lock-downs. Cell-level forbid on Person.ssn survives any future permit someone adds — deny-wins is your safety net.
  • Audit by door to spot anomalies. "Why is the Redis door writing to Person?" is a one-query check once every door's writes are tagged.
  • Door principals + RELATA_TENANCY_MODE=multi. In multi-tenant mode the door principal still applies within each tenant's scope — the per-tenant policy can override per-door defaults.

See also