Audit trail integrity
Audit history is only useful if it cannot be quietly edited. DenialOS records customer-bound actions in append-only audit records that are chained per organization, and each chain is anchored outside the chain itself.
Chaining
Every audit record carries the hash of the record before it, forming a per-organization SHA-256 chain. Two chains are maintained: one over the operational audit log and one over the security event stream. A record cannot be removed, reordered, or rewritten without breaking the links that follow it.
Anchoring
A chain that only links forward can still be truncated at its newest end: deleting the last few records leaves a shorter history that still verifies. Each organization's chain therefore has a separate anchor record holding the chain's sequence, newest hash, and recorded entry count. Anchors are written by a dedicated database function, a guard trigger refuses any update that would move an anchor backwards, and the application's runtime role cannot rewrite them.
What verification reports
Verification returns a specific reason rather than a boolean, so an operator can tell what was detected:
| Reason | Meaning |
|---|---|
| TAIL_TRUNCATED | the log is shorter than its anchor records |
| ANCHOR_MISSING | entries exist but no anchor does |
| ANCHOR_MISMATCH | the newest hash disagrees with the anchor |
| ANCHOR_COUNT_MISMATCH | the number of entries disagrees with the anchor |
An append onto a truncated history is refused rather than silently re-linked.
What is verified
The chain suite runs against a real PostgreSQL instance and covers:
- a record rewritten in place;
- the oldest record removed, a record removed from the middle, and the newest records truncated;
- two records whose positions were swapped;
- a record inserted into the middle;
- a re-linked tail that preserves the recorded sequence and count;
- a missing anchor, and an attempt to move an anchor back to agree with a shortened log;
- one independent chain per organization;
- verification through the audit router, including the no-history case.
Boundary
Anchoring detects edits made through the application or through ordinary SQL; the anchor lives in the same database as the chain, so an actor who controls the database server itself could remove both. Off-database anchoring is a deployment-level option rather than a claim of this documentation.