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.
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 verifywalks 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.