CortexDB Docs
Enterprise

Audit Trail

Tamper-evident, hash-chained audit logging for every policy decision, write, and forget.

CortexDB logs every API operation and every policy decision into an append-only, SHA-256-chained audit log. The log is always-on (no flag to disable) and exposed via three endpoints under /v1/audit. For the endpoint shapes, see the Audit API reference.

What's logged

Every request produces an AuditRow:

FieldDescription
idId of the audit row
tsWhen the request was decided
actorCaller's ActorId (e.g. user:alice, service:slack-connector)
token_jtiThe PASETO token's jti claim (for revocation correlation)
tenant_idTenant binding from the token's aud claim
endpointHTTP method + path (e.g. POST /v1/experience)
capabilityThe capability the request required
decisionallow or deny
decided_by_tierWhich policy tier decided: deployment, tenant, scope, or actor
request{ method, path, body_bytes, body_hash, headers_hash, tenant_pepper_version }
response_statusHTTP status code returned
request_idThe same X-Cortex-Request-ID returned in the response
elapsed_msWall-clock processing time
gdprtrue if this row is part of a GDPR workflow (/v1/erasures)
row_hash / prev_hashThe SHA-256 hash chain — each row's prev_hash = the prior row's row_hash

Row shape corrections (v0.9.9)

The audit row does not carry a scope, event_id, or client_ip field, and the request digest is request.body_hash (value "sha256:…"), not body_sha256. The row_hash / prev_hash chain fields are the tamper-evidence the page is about — they're present on every row. Denials are first-class: every deny row cites the tier and the missing capability, so there are no opaque 403s (on a dev_local instance, deployment-tier allows short-circuit before a capability is attributed, so capability can be "").

Querying the audit log

audit = client.audit_list(
    actor="user:alice",
    capability="forget.gdpr",
    decision="deny",
    limit=100,
)
for row in audit["items"]:
    print(f"{row['ts']}  {row['endpoint']}{row['decision']} "
          f"(by {row['decided_by_tier']}: {row['capability']})")
curl "https://api-v1.cortexdb.ai/v1/audit?actor=user:alice&decision=deny&limit=100" \
  -H "Authorization: Bearer $CORTEX_TOKEN" \
  -H "X-Cortex-Actor: $CORTEX_ACTOR"

# Fetch a single row
curl "https://api-v1.cortexdb.ai/v1/audit/audit_01HX..." \
  -H "Authorization: Bearer $CORTEX_TOKEN" -H "X-Cortex-Actor: $CORTEX_ACTOR"

The endpoint filter is a no-op

Filter by actor and decision (both work). The endpoint filter is ignored on v0.9.9 — rows for other endpoints come back regardless.

Tamper checks

POST /v1/audit/verify lets external auditors confirm a row hasn't been altered — pass the row id plus the exact body you have on file; the server recomputes the SHA-256 chain and reports a match.

result = client.audit_verify(
    audit_id="audit_01HX...",
    body="<canonicalized JSON of the row you have>",
)
print(result["match"], result["algorithm"])   # True sha256

match: false doesn't necessarily mean tampering — it can also indicate the canonical JSON differs (use the body you fetched from GET /v1/audit/{id} verbatim).

Streaming changes via lifecycle

Audit rows are also surfaced as policy_changed / policy_revoked events on the lifecycle stream, so you can plug your SIEM into the SSE stream rather than polling.

Configuration

Audit settings are deployment-preset defaults, not environment variables. Read the values actually in force with GET /v1/policy/deploymentdefaults (verified live on v0.9.9, dev_local preset):

Preset defaultdev_local valueNotes
audit.retentionP7DISO-8601 duration; rows older than this are pruned.
audit.store_body_bytestrueWhether the request-body digest is recorded for tamper checks.
audit.pepper_rotation_periodP3MHow often the hashing pepper rotates.
audit.pepper_retention_bufferP3MGrace window kept across a pepper rotation.

Change them by selecting a different preset with CORTEX_DEPLOYMENT_PRESET — see Deployment Presets.

The CORTEX_AUDIT_* env vars in the v1 docs do not exist

v1 documents CORTEX_AUDIT_RETENTION_DAYS (365), CORTEX_AUDIT_INCLUDE_BODY_HASH and CORTEX_AUDIT_SIEM_ENDPOINT. None of those names is read by the server — verified against the shipped v0.9.8 and v0.9.9 binaries, which contain no CORTEX_AUDIT_* variable at all. Setting them does nothing, and the "365 days" default was never in force (dev_local resolves to P7D). There is likewise no SIEM endpoint setting — forward audit rows off the lifecycle stream instead.

SIEM one-shot drain and /v1/admin/dsar are not shipped

POST /v1/admin/siem/drain and /v1/admin/dsar both return 404 on v0.9.9 — the documented one-shot SIEM flush and the dsar endpoint do not exist. For real-time SIEM forwarding use the [compliance.siem] config (see Security & Compliance); the GDPR erasure path is /v1/erasures only.

All audit rows live on the same RocksDB column family as events, so a verified cold backup of the data directory covers the audit log too.

On this page