CortexDB Docs
Docs for CortexDB 0.12.8, the version running on CortexDB Cloud (api.cortexdb.ai).
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.

Set CORTEX_V1_AUDIT_PATH or the audit log is lost on every restart

By default the audit log is kept in memory only: a graceful restart (docker stop -t 120, then start) empties GET /v1/audit, and the default boot log says nothing about it. Set CORTEX_V1_AUDIT_PATH to a file on the data volume (for example /data/audit.jsonl) and the server writes the rows there as JSONL and keeps them across restarts; the boot log then says v1 audit log: persistent (JSONL) path=…. A path the server can't open falls back to in-memory with an ERROR log line. This applies with or without an API key. Treat the setting as required for any deployment that relies on the audit trail.

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; a derived-data re-derivation or unmerge row records its outcome instead (for example DERIVED_REDERIVE:unmerge_dry_run)
decided_by_tierWhich policy tier decided: deployment, tenant, scope, or actor. A row a route writes for its own decision (a forget's, a re-derivation's or an unmerge's) says handler, which the schema's enum doesn't list
request{ method, path, body_bytes, body_hash, headers_hash, tenant_pepper_version }
response_statusHTTP status code returned
request_idThe request's X-Request-Id (yours when you sent one). The authentication layer's row and a forget's gdpr row carry the id the response returned, which is also on the request's log line; a re-derivation or unmerge row still carries a fresh id of its own
elapsed_msWall-clock processing time
gdprtrue on the row POST /v1/forget writes for its own decision; the authentication layer's rows, /v1/erasures included, say false
row_hash / prev_hashThe SHA-256 hash chain — each row's prev_hash = the prior row's row_hash

Row shape

The authentication layer's rows carry no scope; the rows a route writes for its own decision do (a forget's, a re-derivation's, an unmerge's). No row carries event_id. client_ip appears on the authentication layer's row only when the request carries X-Forwarded-For (its first address), and only callers that may read other actors' rows see it. 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 "").

A POST /v1/forget writes two rows with the same request_id: the authentication layer's row (capability: "", gdpr: false) and the forget's own decision (capability: "memory.forget", decided_by_tier: "handler", gdpr: true, with the forget's scope). Send your own X-Request-Id on a forget to find both, and the request's log line, by one id.

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"

Filtering rows

The list filters are actor, decision and capability. To narrow by endpoint or time, filter on the rows' endpoint and ts fields on your side.

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/deployment → defaults (flat dotted names such as "audit.retention": "P7D"):

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 one audit environment variable

The only audit environment variable is CORTEX_V1_AUDIT_PATH (above). Retention and body hashing come from the deployment preset, so names such as CORTEX_AUDIT_RETENTION_DAYS, CORTEX_AUDIT_INCLUDE_BODY_HASH or CORTEX_AUDIT_SIEM_ENDPOINT have no effect.

SIEM forwarding and data-subject requests

For SIEM forwarding use the [compliance.siem] config (see Security & Compliance); there is no one-shot drain route. For a data-subject deletion request, use /v1/erasures.

With CORTEX_V1_AUDIT_PATH pointing inside the data directory (for example /data/audit.jsonl), a cold backup of the data directory covers the audit log too.

On this page