---
title: Audit integrity
description: What the hash chain proves, what it does not, and the three things that close the gap.
---

## What the chain is

Each audit event's hash covers its own content **and** the hash of the event
before it, keyed with `VEEFORE_AUDIT_CHAIN_SECRET`.

```text
h(n) = HMAC(secret, h(n-1) ‖ content(n))
```

Change the content of event `n-1` and `h(n-1)` no longer matches, so event `n`'s
recorded previous-hash is wrong, and so is every event after it. Delete an event
and the same thing happens.

:::security What this proves
**Integrity of the stored sequence.** No single event can be altered or removed
in isolation. Any tampering is detectable, and it is **localisable** — the
verification names the first position where the recomputed hash diverges.

Localisation is the useful part. "Something was tampered with" is almost
useless; "the chain diverges at event 418,221, written at 02:14 on 3 October"
narrows an investigation to a window.
:::

## What it does not prove

:::danger An attacker with database write access and the chain secret could rewrite it
If both are compromised, the whole chain can be recomputed consistently and the
verification would pass.

The chain's job is to make **quiet, surgical** tampering impossible — altering
one inconvenient event, deleting one action. That is the realistic threat. A
full consistent rewrite requires two separate compromises and leaves other
traces.
:::

It also does not prove **completeness** against a world outside the database. If
events were never written — because the audit pipeline was down — the chain over
what *was* written is perfectly valid. Coverage gaps and chain breaks are
different failures.

## The three things that close the gap

**1. Keep the chain secret out of the database.** Inject
`VEEFORE_AUDIT_CHAIN_SECRET` from a secret manager. A database compromise should
not hand over the key that makes the database's own integrity check meaningless.

**2. Restrict database write access.** The application role should be the only
thing that writes. A human with ad-hoc write access to the audit tables is the
threat this is defending against.

**3. Ship events to an append-only sink you control separately.** Object storage
with versioning and object lock, or your SIEM. Then "we believe nothing was
altered" becomes "two independent records agree", which is a much stronger
statement.

Subscribe a webhook to the event types you care about, or export on a schedule.
[→ Audit and export](/operate/audit-and-export)

## Verify on a schedule

```bash
veefore audit verify
```

Exits non-zero on a break and names the first divergent position.

```http
POST /v1/audit-events/verify-integrity
```

Requires `audit:verify_integrity`, held by `SECURITY_ADMIN` and `AUDITOR`.

:::warning A check you only run after an incident tells you nothing about when the problem started
Run it daily, from CI, with the result going somewhere a person reads. Then a
failure has a date attached, and the date narrows the investigation.
:::

An `AUDITOR` role is sufficient for a scheduled check — it holds `audit:read`,
`audit:export` and `audit:verify_integrity` and nothing that can change
anything.

## Coverage gaps

`AUDITED_SERVICE_UNAVAILABLE` during an outage means events were not written.
The chain over what *was* written stays valid; the gap is in coverage.

:::danger Audit failure is the most serious dependency failure
The other `503`s mean work is not happening, which is recoverable — the work
waits. Audit failure means **evidence is being lost right now**, and it does not
come back when the service recovers.

Alert on `veefore_audit_pipeline_errors_total` with zero tolerance. Not a
threshold — any increase.
:::

After an outage, record the window on an incident. "Audit coverage is incomplete
between 02:14 and 02:48 UTC" is a usable statement. Discovering it six months
later from a gap in the data is not.

## Export integrity

```bash
veefore audit export --format csv \
  --reason "Quarterly SOC 2 evidence" --out audit-2026-q3.csv
```

Watermarked, and you get a content hash. An auditor recomputes it to prove the
file they received is the file that was produced.

:::security Send the hash through a different channel than the file
The hash proves the file is unaltered only if it did not travel with the file.
Emailing both in one message proves nothing — whoever could alter one could alter
the other.

Send the file however is convenient. Send the hash over a channel that is
independently authenticated.
:::

Taking an export is itself an audit event, requiring a reason. Removing evidence
from the system is worth knowing happened.

## Retention

Changing it requires `audit:configure_retention` — step-up gated, writing a
`CRITICAL` audit event.

Shortening retention destroys evidence, which is precisely why it is treated as
a high-impact operation. A request to "clean up old audit data" leaves a mark
explaining who asked and why.

Export and archive first. The archive is watermarked and hashed, so it remains
verifiable afterwards.

## What is in the record

For a protected action: the actor and its type, the resolved authority and
delegation chain, every policy consulted with versions, every rule that matched
with its per-condition trace, every risk signal with its weight and
explanation, the approval and who decided it, the credential lease **by
reference**, the downstream status, the outcome, and the correlation ids.

Never credential material. Never a private key. Masked parameter paths are
masked in audit too — a `MASK` decision redacts the value in the trail as well
as in the forwarded request, because redaction is applied on ingest.

## Rotating the chain secret

Breaks verification of the existing chain. Before rotating:

1. Export and archive the full trail. The export is independently hashed.
2. Record the rotation point, so a future verification knows where to start.
3. Rotate.
4. Verify forward from the rotation point.

A project, not a config change.

## Verifying this page

```bash
npm run test:integration    # chain construction and break detection
veefore audit verify    # against a live deployment
```

The integration suite includes cases that tamper with a stored event and assert
the verification detects it at the right position.

## Related

:::cards

:::card Audit | /concepts/audit
The model and what gets recorded.
:::

:::card Audit and export | /operate/audit-and-export
Schedules, evidence packs, retention.
:::

:::card Compliance | /secure/compliance
Where the trail becomes evidence.
:::

:::card Audit architecture | /architecture/adr/0012-audit-architecture
Why a keyed chain in PostgreSQL.
:::

:::
