Verify the audit chain
For: tenant admins and governance reviewers.
A chain that shows edits
Each tenant's entries form a chain, and the cluster's own entries another. Every entry has a sequence number, the time the database clock recorded it, and two hashes: the SHA-256 of its content, and the SHA-256 of the previous entry's hash and its own content hash. Change an entry, delete one, or slip one in, and the chain breaks where it happened.
oz0 audit verify
CHAIN STATUS ENTRIES REDACTED HEAD HEAD HASH PROBLEM
(cluster) ok 12 0 12 5be0d6a1…
default BROKEN 1 0 45 9c41fe02… entry 2 (01a1000b-e17e-7627-b463-c144bc77f73d) was edited: its actor does not match its hash
audit verify exits with an error when a chain is broken, so you can run it from a scheduled check. It checks each chain up to the head it finds when it starts, so a busy tenant whose jobs add entries meanwhile verifies as usual. Cluster admins verify every chain; a tenant's admins verify their tenant's with --tenant. Each verification is in the log too.
Redacted entries. An erasure and retention's anonymising blank some entries' actor, target, detail, private part and starter. Each of those parts has its own hash, salted with a random salt the entry keeps until it is redacted, and the content hash covers these hashes and the entry's other fields (sequence number, ID, times, tenant, action, subject, agent) as they are.
A redacted entry keeps its hashes, so everything it still shows is checked, and without the salt its hashes say nothing about what it held. The erasure or retention entry written with the redaction lists the entries it redacted, and audit verify fails on a redacted entry that no such entry lists: marking an entry redacted by hand does not hide an edit.
The chain makes the log append-only in practice: anyone who can write to the database can still rewrite the whole chain from the edit on. Keep the head of each chain somewhere else (an export, a ticket, a signed message) and a later rewrite shows: the head no longer matches.