DocsAdministrationCardinal 0.7.0

The audit journal

Every change to the directory, its policy and its configuration is journaled in the same transaction, hash-chained so an edit is detectable.

llms.txt

Every change to the directory, its policy and its configuration appends an entry to an immutable journal, in the same transaction as the change itself. Each entry carries the hash of the one before it, so removing or editing any entry breaks the chain from that point on and is detectable.

What is deliberately not journaled is the machinery: sign-in protocol state that lives for minutes, delivery queues, rate-limit counters and last-used stamps. A build check keeps the two apart. It fails when any write is not journaled and not on that list, each entry with its reason.

Up to 0.5.1 the journal missed some administrative changes: publishing and activating a policy, security event streams, the groups an application is shown, mail templates and SCIM external ids. From 0.5.2 they are journaled. Changes made before that left no entry, and no upgrade can add one.

cardinal audit verify

That is not event sourcing: the state tables remain authoritative, and the journal is a tamper-evident record beside them rather than the source of truth they are rebuilt from. The full ledger approach is a large complexity tax for a property this gets most of.

The chain is worth the most in a place people do not expect: after a restore. A normal PostgreSQL restore tells you the backup was consistent. Run cardinal audit verify against a restored database and you get cryptographic confirmation that its history has not been edited.

A fingerprint kept elsewhere

audit verify checks the chain against itself. Somebody who can write to the database can rewrite the whole chain, recomputing every hash, and it still passes. What catches that is a copy of the chain's head kept where the database credential does not reach: the journal's fingerprint.

cardinal journal head
cardinal-journal v1 seq=48213 hash=9f2c…e41a at=2026-10-03T08:14:07.512Z

cardinal journal verify 'cardinal-journal v1 seq=48213 hash=9f2c…e41a at=2026-10-03T08:14:07.512Z'
Verdict Exit What it means
intact 0 The kept event still has that hash, and every event since chains from it
rewritten 3 The kept event, or one after it, is not what it was. Treat it as an incident
ends before 4 The journal now stops before the kept point: events were removed from its end
no longer held 5 The point is older than what the journal keeps. Check a newer fingerprint

The same two exist in the API, GET /api/journal/head and POST /api/journal/verify with {"fingerprint": "…"}, for directory and security administrators signed in, and for their access tokens carrying the journal scope. A scheduled job copying the head off the host every hour is the intended use; reading the head is one index lookup per yearly partition.

Every administrator also receives the fingerprint by mail, in the digest.

What a fingerprint proves stops at its own point. History up to the newest fingerprint kept elsewhere cannot be rewritten without verify saying so; events after it can. The more often one is kept, the shorter that window. verify re-reads every event from the kept point to the head, so check the newest one you hold rather than the oldest.

In the console

Audit is the journal: every change in order, with what it was, who did it and when, filterable by action. The chain check is on that page too: the same verification cardinal audit verify performs, behind a button.

Who did it, honestly

Changes made through the console, the API or a command that signs in record the person who made them. Changes made by a command working on the database directly record direct-database, a system account that means exactly that: whoever held the connection string, with no authenticated person behind it. Before 0.5.0 such a change recorded nobody, or a grant named the member as its own granter; those entries remain as they were written.

Every direct-database change is mailed to the administrators (administrator alerts). Two ways in explains why the two paths differ at all.

Limits

  • No log shipping. The journal lives in PostgreSQL; there is no built-in export to a SIEM. Security events carry a specific set of things outward, and that is not the same as the journal.
  • audit verify walks the whole chain. Fine at the size an internal directory reaches, and not incremental.
  • Nothing anchors the chain automatically. A fingerprint is worth what its keeper makes of it: kept off the host, and checked.