---
title: Audit
description: Hash-chained events, watermarked exports, and an integrity check that tells you exactly where a chain breaks.
---

Every state change writes an audit event. Events are hash-chained, so an event
cannot be altered or removed without breaking the chain — and the break is
detectable, at a specific position.

This is the difference between a log and an audit trail. A log is something you
read. An audit trail is something you can prove.

## The chain

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

```text
event n-1  →  hash(h(n-2) ‖ content(n-1))  →  h(n-1)
event n    →  hash(h(n-1) ‖ content(n))    →  h(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. There is no way to tamper with one event in
isolation.

:::diagram audit-chain | Altering one event invalidates every event after it, and verification names the first divergence.
Each event hashes its own content together with the previous hash, keyed with
`VEEFORE_AUDIT_CHAIN_SECRET`. Altering event `n−1` changes its hash, so event
`n` carries the wrong previous hash — and so does everything after it.
Verification reports the first position where the recomputed hash diverges,
which narrows an investigation to a point in time.
:::

```bash
veefore audit verify
```

Exits non-zero if the chain is broken, and names the first position where the
recomputed hash diverges. Over the API:

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

Requires `audit:verify_integrity`, which `SECURITY_ADMIN` and `AUDITOR` hold.
Run it on a schedule, not only when you suspect something — a check you only run
after an incident tells you nothing about when the problem started.

:::security What the chain does and does not prove
It proves **integrity**: that the stored sequence has not been altered since it
was written.

It does not prove **completeness** against a world outside the database. An
attacker with write access to PostgreSQL and the chain secret could in principle
rewrite the whole chain consistently. Mitigate it the normal way: keep the chain
secret out of the database, restrict database write access, and ship events to
an append-only sink you control separately. The chain's job is to make quiet,
surgical tampering impossible — which is the realistic threat.
:::

## Reading events

```bash
veefore audit list --severity CRITICAL --from 2026-10-01T00:00:00Z
veefore audit show evt_01ABC
```

or:

```http
GET /v1/audit-events?severity=CRITICAL&cursor=…&limit=50
GET /v1/audit-events/{eventId}
```

The detail view includes the event's position in the hash chain, so you can tie
a specific event to a specific verified range.

Severities: `INFO`, `NOTICE`, `WARNING`, `CRITICAL`.

`CRITICAL` is reserved for things a security team must see: agent revocation,
emergency controls, break-glass access, reducing enforcement, changing audit
retention. If `CRITICAL` is noisy, something is miscategorised.

The full event type registry is in [Event types](/reference/events) — generated,
so it lists what the system actually emits.

## Exporting

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

Three things happen that are worth knowing about:

1. **A reason is required.** It is recorded.
2. **The export is itself an audit event.** Taking evidence out of the system is a thing worth knowing happened.
3. **The export is watermarked and hashed.** You get a content hash back; an auditor can recompute it to prove the file they received is the file that was produced.

The file is written at mode `0600`. `audit:export` is held by `SECURITY_ADMIN`
and `AUDITOR`; `DEVELOPER` has `audit:read` but not `audit:export`, because
reading the trail and removing a copy of it are different privileges.

## What gets recorded

For a protected action, the audit event and the action record together carry:

- The actor — agent, user, API key or system — and which one
- The resolved authority and the delegation chain
- Every policy consulted, with versions, and every rule that matched
- Every risk signal that fired, with its weight and explanation
- The approval, if any, and who decided it
- The credential lease **by reference**, never by value
- The downstream response status
- The final outcome
- `request_id` and `trace_id` for correlation

:::security Never the secret
Audit events carry credential ids, not credential material. Masked parameter
paths are masked in audit too, so a `MASK` decision redacts the value in the
trail as well as in the forwarded request. The redaction layer is applied on the
way in, not on the way out, so there is no code path where a secret is written
and later filtered.
:::

## Retention

Configurable, and changing it requires `audit:configure_retention`, which is
step-up gated and writes a `CRITICAL` event.

Shortening retention destroys evidence. Treating it as a high-impact operation
means a request to "clean up old audit data" leaves a mark explaining who asked
and why.

## Correlation

Every API response carries `x-request-id` and `x-trace-id`. Both appear on the
action record, on the audit event, and in the structured logs.

Given a `request_id` from a user complaint you can find the action, its full
lineage, the audit event, and every log line the request produced. That is the
intended debugging path and it is why the ids are on the response rather than
only in the logs.

## Related

:::cards

:::card Audit integrity | /secure/audit-integrity
The threat model for the chain, in detail.
:::

:::card Audit and export | /operate/audit-and-export
Operating it: schedules, evidence packs, retention.
:::

:::card Event types | /reference/events
Every event type. Generated.
:::

:::card Audit API | /reference/api/audit
Search, export, verify integrity.
:::

:::
