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 serve

The 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 serve

Every 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/health

The 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 serve

For 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 serve

Verify 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 serve

mTLS 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/ready

Purpose 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 serve

Queries declaring an unregistered purpose return 403 (see Error codesMissingPurpose/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 typePerformance
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 check

chain_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