HMAC audit chain
Chain construction
Section titled “Chain construction”Every audit row stores:
id, ts, actor_user_id, project_id, action, target_type, target_id, details,row_hmac, prev_row_hmacWhere:
row_hmac = HMAC-SHA256( Z4J_SECRET, canonical_json({ id, ts, actor_user_id, project_id, action, target_type, target_id, details, prev_row_hmac }))The audit chain uses Z4J_SECRET itself; there is no separate audit secret.
canonical_json is sorted-keys, no whitespace, UTF-8. prev_row_hmac is the row_hmac of the previous row in id order. Genesis row has prev_row_hmac = null.
Properties
Section titled “Properties”- Tamper-evident - change any field →
row_hmacno longer verifies. - Insertion-evident - inserting a row in the middle breaks the next row’s
prev_row_hmaclink. - Deletion-evident - deleting a row breaks the chain at the gap.
Not prevented:
- Append-only suffix replacement - someone with
Z4J_SECRETand DB access can truncate trailing rows and re-sign. Mitigation: periodically export row_hmac externally (e.g. to an append-only S3 bucket or a second DB).
Verification
Section titled “Verification”z4j audit verifyWalks the full log; returns (ok, rows_verified, first_broken_id). Runs on demand (CLI / API / dashboard button). For a periodic check, wrap the CLI invocation in cron or a Kubernetes CronJob — the brain does not loop the verification internally.
Secret rotation
Section titled “Secret rotation”Rotating Z4J_SECRET would break verification of pre-rotation rows under a naive key change. z4j supports a dual-key window for exactly this case: set the previous value(s) in Z4J_PREVIOUS_SECRETS (comma-separated) so the verifier accepts rows signed under any of those keys while writes use the new Z4J_SECRET. Once every retained row is provably re-anchored (or aged out), drop the old key from Z4J_PREVIOUS_SECRETS.
If you would rather start fresh: export old rows with their HMACs (still verifiable using the old secret externally), rotate, begin a new chain.
Export integrity
Section titled “Export integrity”CSV / NDJSON export includes row_hmac + prev_row_hmac. Downstream verification requires Z4J_SECRET (share it with the verifier out-of-band).
Performance
Section titled “Performance”HMAC-SHA256 on a small canonical payload is microseconds; the chain does not meaningfully slow writes. Verification of a million rows takes ~10 seconds on modest hardware.