Observed
In a long-running compose lab (Solmara), two Relay containers came back from an unclean stop with a torn last line in their audit.jsonl. From that point every audited request failed closed:
audit.write_failed: audit chain verification failed: line 103 prev_hash mismatch
and callers saw 5xx (audit.write_failed from the relay; upstream Notary evaluations surfaced it as source.unavailable). The services stayed in that state indefinitely; health checks reported the containers healthy while every real request failed.
Why (code path on main)
JsonlFileSink recomputes the retained chain tail before appending (tail_hash_from_files / verify_chain in crates/registry-platform-audit/src/lib.rs). Any corruption in the retained JSONL set makes every subsequent append return AuditError::ChainVerification, which the relay maps to audit.write_failed (crates/registry-relay/src/audit/mod.rs). Size-based rotation exists, but there is no path that ever moves a corrupt segment aside.
What's missing
Failing closed is the right default for audit integrity; the gap is operability:
- No supported recovery. The only way out today is manual surgery inside the audit volume (stop the relay, move
audit.jsonl aside, restart). Nothing documents this, and doing it silently discards the operator-visible record of the discontinuity. A supported operation (CLI or admin endpoint) should quarantine the corrupt segment, record an explicit chain-break event (why, when, tail hash before/after), and restart the chain, ideally anchored via the existing ChainVerificationAnchors mechanism.
- No startup detection. The relay only discovers the broken chain on the first audited request. Verifying the retained chain at startup (and reflecting it in readiness/health) would turn a confusing per-request 5xx into an actionable operational signal.
- Crash consistency. The corruption source was an unclean container stop, which is routine in container platforms. The append path could be hardened against torn writes (e.g. tolerate and quarantine a partial final line as a distinct, explicitly-logged case rather than a generic prev_hash mismatch).
Notes
Audit integrity is a security-sensitive area per CONTRIBUTING; any fix needs explicit review notes on how the chain break is itself made tamper-evident.
Observed
In a long-running compose lab (Solmara), two Relay containers came back from an unclean stop with a torn last line in their
audit.jsonl. From that point every audited request failed closed:and callers saw 5xx (
audit.write_failedfrom the relay; upstream Notary evaluations surfaced it assource.unavailable). The services stayed in that state indefinitely; health checks reported the containers healthy while every real request failed.Why (code path on main)
JsonlFileSinkrecomputes the retained chain tail before appending (tail_hash_from_files/verify_chainincrates/registry-platform-audit/src/lib.rs). Any corruption in the retained JSONL set makes every subsequent append returnAuditError::ChainVerification, which the relay maps toaudit.write_failed(crates/registry-relay/src/audit/mod.rs). Size-based rotation exists, but there is no path that ever moves a corrupt segment aside.What's missing
Failing closed is the right default for audit integrity; the gap is operability:
audit.jsonlaside, restart). Nothing documents this, and doing it silently discards the operator-visible record of the discontinuity. A supported operation (CLI or admin endpoint) should quarantine the corrupt segment, record an explicit chain-break event (why, when, tail hash before/after), and restart the chain, ideally anchored via the existingChainVerificationAnchorsmechanism.Notes
Audit integrity is a security-sensitive area per CONTRIBUTING; any fix needs explicit review notes on how the chain break is itself made tamper-evident.