ADR-0017: Audit-log rotation with per-segment chain re-anchoring
- Status: accepted
- Date: 2026-07-31
- Deciders: fabriziosalmi
- Tags: audit, observability, operations, security
Context
The HMAC-SHA256-chained audit log (ADR-0004) is append-only JSON-Lines. In memory it is a bounded mpsc holding one constant FD, so the RSS/FD stability soak does not flag it — but on disk it was unbounded: an operator who enables audit gets a file that grows until the volume fills (issue #288). A full disk is a production outage, and the audit writer itself would then start dropping events. The log needs a size ceiling.
Decision
Add size-based rotation to the writer task, on by default when audit is enabled.
- Config (
[audit]):max_size_mb(default 100;null/0= unbounded, the pre-v0.7 behavior) andmax_files(default 10;0= keep all). The on-disk ceiling ismax_size_mb × (max_files + 1)≈ 1.1 GB with the defaults. - Rotate when the active segment crosses the byte cap: flush + close it,
renameit to<path>.<epoch_nanos>, open a fresh<path>, and prune the oldest rotated segments (by mtime) beyondmax_files.
Per-segment re-anchor (the load-bearing choice)
Each new segment re-anchors the HMAC chain at genesis and opens with a chain_rotate marker, rather than carrying prev_hash across the rotation boundary. This is deliberate and matches ADR-0004's existing model: the chain already re-anchors at every process restart (a fresh chain from the genesis tag + a chain_init marker), because continuing from an on-disk tail would mean trusting an unverified value and so defeats tamper-evidence. Rotation is just another boundary of the same kind.
Consequences of re-anchoring:
- Each segment verifies independently from genesis — a reader can validate one rotated file without holding the others.
- Full-history verification concatenates the segments in timestamp order, exactly as it already concatenates across restarts.
- Cross-segment deletion is not self-detecting within a single segment's chain — but this is no weaker than the pre-rotation guarantee, which already reset per restart. Operators who need append-only durability across the whole history ship segments to write-once storage (the
max_files = 0retention mode exists for this).
Failure handling
- Rename fails (read-only dir, cross-device): logged once, and the writer reopens the current segment and continues unbounded rather than crashing or spinning (re-anchor → still over cap → rename fails → repeat). Disk-hygiene degrades to the old behavior with a loud
audit log rotation failedwarning; audit events are never dropped for a rotation problem. - Prune fails: non-fatal and best-effort — retention slippage never blocks a write.
- Reopen after a successful rename fails: the writer exits (same terminal posture as an un-openable log at boot).
Consequences
- Positive: audit is safe to enable in production; disk is bounded by config; the tamper-evidence model is unchanged (same re-anchor semantics as restarts).
- Negative: verifying a full history requires ordering the segments (a one-line
catin timestamp order). A rotated boundary is achain_rotatemarker, not a continuous hash link — documented in the wire format. - Neutral: rotation is size-based only; time-based rotation (daily segments) is a possible follow-up if a compliance regime requires calendar alignment.
Relationship to ADR-0004
A follow-up, not a supersession. ADR-0004 (HMAC-chained audit log) stays accepted; its chain, redaction, and non-blocking-emission properties are untouched. This ADR only bounds the file on disk and formalizes that a rotation boundary re-anchors exactly like a process restart.
References
- ADR-0004 (HMAC-chained audit log)
src/audit.rs(writer_loop,open_and_anchor,rotate_paths,prune_old_segments)docs/guide/observability.md(Rotation and disk usage)- Issue #288