Veefore docs

Audit

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.

Tampering with one event breaks every event after itFour chained audit events. The second is altered, and the two after it are shown as broken, with verification pointing at the first divergence.Detectable, and localisedVerification names the first position where the recomputed hash diverges.event n − 2h = HMAC(k, h₋₁ ‖ c)hash verifiesevent n − 1h = HMAC(k, h₋₁ ‖ c)content alteredevent nh = HMAC(k, h₋₁ ‖ c)previous hash wrongevent n + 1h = HMAC(k, h₋₁ ‖ c)previous hash wrongfirst divergence reported hereIntegrity, not completeness: an attacker with database write access and the chain secret could rewrite it consistently.
Altering one event invalidates every event after it, and verification names the first divergence.
Described in text

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.

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 — 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

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.

Identity, authority, policy and enforcement for autonomous AI agents.