The audit log is hash-chained, refused writes are audited, and the daemon journals every row.
- Migration 0064: each audit row stores the hash of
[prev, id, actor_ref, action, created_at, data], written in the same transaction that reads the previous hash; the timestamp is taken under the write lock. Rows from before the migration are unchained. Verification detects an edited, removed or reordered row, a changedactor_id, and chained rows posing as unchained. - Retention deletes audit rows by id (
id <= MAX(id) older than the cutoff), so a clock step cannot punch a hole. - The daemon writes every audit row to its journal (
INFO audit id=… hash=…). - Refused mutating commands (exit 3 or 4, not read-only) and refused pushes are audited as
refused <path>, with flag names and the first positional only; ten a minute per account and 600 across the instance, counted per process, then onerefused.throttledrow. gitbayd admin audit verifyprints rows, unchained, first, last and the last hash, and exits 1 on a break or when every row is unchained.- Removing the newest rows, or reusing their ids afterwards, is not detectable from the database; comparing verify's last id and hash with the journal shows it. Under
ssh.mode = "system"(bay1 runsembedded)gitbayd shellhas no journal copy and its own refusal count. Both are in Known-Gaps. - Admin, Threat-Model and Architecture pages; CHANGELOG.
Stacked on !494 (hook-socket-auth).
Closes #275