Auth & Security
Security in RelataDB is in the query path. Every read runs through an ABAC engine, every write is audit-logged with a tamper-evident hash chain, and classified types are redacted at egress before serialisation — regardless of whether the query itself succeeded.
Dev mode warning
The free profile with no token set runs completely unauthenticated. pgwire is disabled, the admin surface (/admin/*) is open, and no auth is checked. This is intentional for local dev. If the node is reachable beyond localhost, secure it now:
# Minimum viable secure setup
RELATA_PROFILE=server \
RELATA_BEARER_TOKEN=$(openssl rand -hex 32) \
relata serveThe server and cluster profiles refuse to start without RELATA_BEARER_TOKEN. There is no way to accidentally run them unauthenticated.
Step 1 — Set a bearer token
Generate a token and set it before starting the server:
export RELATA_BEARER_TOKEN=$(openssl rand -hex 32)
export RELATA_ADMIN_TOKEN=$(openssl rand -hex 32)
RELATA_PROFILE=server relata serveEvery request to every endpoint (HTTP, gRPC, Arrow Flight, pgwire, all protocol doors) now requires:
Authorization: Bearer <token>
Test that auth is working:
# Should return 401
curl -s -o /dev/null -w "%{http_code}" http://localhost:9090/query
# Should return 200
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
http://localhost:9090/healthThe admin token gates all /admin/* management operations separately. On server and cluster profiles, RELATA_ADMIN_TOKEN is required — if unset, every /admin/* route returns 503 Service Unavailable. The regular bearer token (RELATA_BEARER_TOKEN) is not a fallback for admin access (privilege separation).
# Provision a tenant token via admin API
curl -X POST http://localhost:9090/admin/tokens \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description":"acme-prod","expires_in_days":365}'
# → { "token": "rlt_7f3a9b2c...", "id": "tok_..." }On the server and cluster profiles the data-plane HTTP listener (RELATA_HTTP_BIND) and gRPC listener (RELATA_GRPC_BIND) bind 0.0.0.0 by default — auth/TLS posture is uniform across every profile. The env bearer token (RELATA_BEARER_TOKEN) is accepted on every interface; protect it with TLS (RELATA_TLS_CERT/RELATA_TLS_KEY) or a reverse proxy / sidecar that terminates auth before traffic reaches the node. The admin surface (/admin/*, /platform/*) is on a separate, loopback-only listener (RELATA_ADMIN_BIND, default 127.0.0.1:9091 — Zero-Trust control plane) and is never mounted on the data-plane listener. For network-exposed client traffic, prefer per-tenant registry tokens (rotatable, revocable, narrowly scoped) over reusing the env bearer token.
Token lifecycle
Tokens support expiry, self-service rotation, and per-tenant audit:
# Create a token with a 30-day expiry
curl -X POST http://localhost:9090/admin/tokens \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description":"short-lived","expires_in_days":30}'
# Rotate your own token (tenant self-service — no admin token needed)
curl -X POST http://localhost:9090/tokens/rotate \
-H "Authorization: Bearer rlt_7f3a9b2c..."
# → { "token": "rlt_new...", "old_token_revoked": true }
# View your token's last-use audit log (last 100 uses)
curl http://localhost:9090/tokens/audit \
-H "Authorization: Bearer rlt_7f3a9b2c..."
# Platform-wide audit (admin only)
curl http://localhost:9090/admin/tokens/audit \
-H "Authorization: Bearer $RELATA_ADMIN_TOKEN"Tokens within 30 days of expiry are logged at WARN on each use as a rotation reminder.
Step 2 — Wire OIDC (optional)
For production SSO, configure OIDC. The oidc mode trusts a front-proxy's verified principal; oidc-verify validates the JWT signature against the provider's JWKS endpoint in-process.
RELATA_AUTH_MODE=oidc \
RELATA_OIDC_ISSUER=https://auth.example.com \
RELATA_OIDC_CLIENT_ID=relata \
RELATA_OIDC_JWKS_URI=https://auth.example.com/.well-known/jwks.json \
RELATA_OIDC_AUDIENCE=relata \
RELATA_PROFILE=server \
relata serveFor in-process token signature verification (recommended):
RELATA_AUTH_MODE=oidc-verify \
RELATA_OIDC_ISSUER=https://auth.example.com \
RELATA_OIDC_JWKS_URI=https://auth.example.com/.well-known/jwks.json \
RELATA_OIDC_AUDIENCE=relata \
RELATA_PROFILE=server \
relata serveVerify OIDC is working by obtaining a token from your provider and querying:
TOKEN=$(curl -s -X POST https://auth.example.com/token \
-d "grant_type=client_credentials&client_id=relata&client_secret=$SECRET" \
| jq -r .access_token)
curl http://localhost:9090/health/ready \
-H "Authorization: Bearer $TOKEN"Step 3 — Wire mTLS (optional)
For service-to-service auth where client certificates are already managed by your mesh:
RELATA_AUTH_MODE=mtls \
RELATA_MTLS_CA_CERT_PATH=/etc/relata/tls/ca.crt \
RELATA_PROFILE=server \
relata servemTLS requires a CA cert (RELATA_MTLS_CA_CERT_PATH); client-cert requirement defaults to on (RELATA_MTLS_REQUIRE_CLIENT_CERT=true). To terminate TLS in-process on the listener, also set RELATA_TLS_CERT and RELATA_TLS_KEY.
Clients must present a certificate signed by the configured CA. No bearer token is required when mTLS is the auth mode — the client cert is the credential.
Test with curl:
curl --cert client.crt --key client.key --cacert ca.crt \
https://localhost:9090/health/readyPurpose enforcement
PURPOSE is optional at the SQL layer. When you declare it, it is recorded in the audit log and evaluated by the ACL engine.
-- With purpose (recorded in audit, ACL-evaluated)
PURPOSE 'analytics' SELECT name, email FROM Person LIMIT 10;
-- Without purpose (valid — purpose is optional)
SELECT name FROM Person LIMIT 10;In production, lock down to a registered list:
RELATA_PURPOSE_MODE=strict \
RELATA_PURPOSES=analytics,audit,compliance,security_incident \
relata serveQueries declaring an unregistered purpose return 403 (see Error codes — MissingPurpose/UnknownPurpose). Use open mode only in dev.
EXPLAIN POLICY
Before deploying a policy, validate what it does:
EXPLAIN POLICY FOR PURPOSE 'analytics' ON Person;The output shows which rows are visible, which columns are masked, and which deny rules fired. Run this whenever you change ACL policies — it catches overly broad denies before they hit production queries.
Policy engine (ABAC)
RelataDB ships its own Cedar-inspired ABAC engine — deny-wins semantics, bitmap row filtering, and cell masking.
| Rule type | Performance |
|---|---|
| Bitmap row filtering | ~1.0× raw-scan overhead (effectively free) |
| Conditional ACL | ~1.32× raw-scan p50 |
| Cell masking | ~2.6× raw-scan p50 — avoid on hot paths |
Policy example:
permit(
principal == user::"alice",
action == action::"read",
resource in department::"finance"
) when {
resource.purpose == "audit"
};
forbid(
principal,
action == action::"read",
resource
) when {
resource.classification == "restricted"
};Deny-wins means any matching forbid overrides all permit rules. Always test with EXPLAIN POLICY after adding a deny.
Egress filtering
Classified types are redacted at serialisation regardless of whether the query succeeded. This applies uniformly across HTTP, gRPC, Arrow Flight, pgwire, SPARQL, and every protocol door.
Redacted types include SourceTrueIdentity, SigintIntercept, AccessScopedIntercept, and LawfulInterceptRecord. These never appear in tool results, query rows, or SDK responses.
Rate limits
Rate limits are per-IP and enforced on every request path:
# Production defaults on server/cluster (per-IP token bucket)
# RELATA_RATE_LIMIT_RPS=100000 (default)
# RELATA_RATE_LIMIT_AUTH_FAIL_RPS=10 (default)On exhaustion the server returns 429 Too Many Requests with a Retry-After header. Setting AUTH_FAIL_RPS=0 is treated as 1 — use 99999 to effectively disable.
Auth-failure rate limiting is a brute-force guard. Keep it low in production.
GDPR Art. 17 erasure
The ERASE SUBJECT operator performs a governed right-to-erasure: shreds rows, destroys orphaned blobs, destroys the per-subject DEK via KMS, and returns a signed Art. 17 receipt.
ERASE SUBJECT 'person-42' REASON 'gdpr-art17' CERTIFY;The same operation is available via CLI, SDK, and MCP:
# CLI
relata query "ERASE SUBJECT 'person-42' REASON 'gdpr-art17' CERTIFY"
# SDK (Python)
client.identity.erase_subject("person-42", reason="gdpr-art17")
# MCP tool
# { "tool": "erase_subject", "subject_id": "person-42", "reason": "gdpr-art17" }The returned receipt is content-addressed and verifiable against the audit chain. Store it — regulators may ask for it.
Audit hash chain
Every write is recorded in an append-only log with principal, timestamp, purpose, cost units, and a hash linking each entry to the previous one.
# Check chain validity
curl -H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
http://localhost:9090/audit/count
# { "entries": 1248, "chain_valid": true }
# Deep health check (chain + WAL + object-store)
relata checkchain_valid: false is a security event. Treat it as a potential breach: isolate the node, preserve the WAL, and investigate.
Protocol door security
The protocol-compatibility doors bind to 127.0.0.1 by default; override per-door with RELATA_<DOOR>_BIND on any profile (no license needed). pgwire fails closed without RELATA_BEARER_TOKEN. Put other doors behind a network policy or mTLS sidecar before exposing them beyond localhost. Every door presents a distinct Cedar principal (s3-client, pgwire-client, mongo-client, …) so you can grant least privilege per integration and audit-log which protocol wrote each row — see Per-Door ACL. For full Docker/Kubernetes door wiring, see Deploying Protocol Doors.
See also
- Per-Door ACL — least privilege per integration via Cedar door principals
- Multi-Tenancy — org isolation, per-tenant DRK, quotas
- Configuration — all auth env vars
- Observability — audit trail, correlation IDs