Audit and export
Verify the chain on a schedule, produce a verifiable evidence pack, and treat retention changes as the destructive operation they are.
#Verify on a schedule
bashveefore audit verify
Exits non-zero if the chain is broken, and names the first position where
the recomputed hash diverges. Over the API:
POST /v1/audit-events/verify-integrity, requiring
audit:verify_integrity.
Verify on a schedule, not after an incident
A check you only run when you suspect something tells you nothing about when the problem started. Run it daily, from CI or a cron job, with the result going somewhere a person reads.
Then a failure has a date attached, and the date narrows the investigation to a window instead of to all of history.
yamlname: Veefore audit integrity
on:
schedule: [{ cron: '0 6 * * *' }]
jobs:
verify:
runs-on: ubuntu-latest
env:
VEEFORE_CONTROL_PLANE_URL: ${{ vars.VEEFORE_CONTROL_PLANE_URL }} # self-hosted only
VEEFORE_EMAIL: ${{ secrets.VEEFORE_AUDIT_EMAIL }}
VEEFORE_PASSWORD: ${{ secrets.VEEFORE_AUDIT_PASSWORD }}
steps:
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm install -g veefore
- run: veefore audit verify
An AUDITOR role is enough — it holds audit:read, audit:export and
audit:verify_integrity and nothing that can change anything.
#Produce an evidence pack
bashveefore audit export \
--format csv \
--from 2026-07-01T00:00:00Z \
--to 2026-10-01T00:00:00Z \
--reason "Quarterly SOC 2 evidence" \
--out audit-2026-q3.csv
Three things happen:
- The reason is required and recorded.
- The export is itself an audit event. Removing evidence from the system is worth knowing happened.
- The file is watermarked and you get a content hash. An auditor recomputes the hash to prove the file they received is the file that was produced.
Written at mode 0600.
Give the auditor the hash separately
The hash proves the file is unaltered only if it reaches them through a different path than 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.
Over the API: POST /v1/audit-events/export, requiring audit:export.
#Search
bashveefore audit list --severity CRITICAL --from 2026-10-01T00:00:00Z
veefore audit list --type agent.revoked
veefore audit show evt_01ABC
Cursor-paginated over the API:
httpGET /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.
#Severity, and what belongs in CRITICAL
INFO |
Routine state changes |
NOTICE |
Worth seeing in a review |
WARNING |
Something is off |
CRITICAL |
A security team must see this |
CRITICAL is reserved for: agent revocation, emergency controls, break-glass
access, reducing enforcement, and changing audit retention.
If CRITICAL is noisy, something is miscategorised
The point of the severity is that a CRITICAL event is always worth a human
look. A CRITICAL stream that gets skimmed is a CRITICAL stream that will be
skimmed on the day it matters.
#Ship it somewhere else
Audit events exist to survive. The chain detects tampering within the stored sequence; it does not protect against an attacker with database write access and the chain secret, who could rewrite the whole chain consistently.
The mitigation is boring and effective:
- Keep
VEEFORE_AUDIT_CHAIN_SECRETout of the database and out of the application's own config store. - Restrict database write access to the application role.
- Ship events to an append-only sink you control separately — object storage with versioning and object lock, or your SIEM.
Subscribe a webhook to the event types you care about, or export on a schedule. Either way the second copy is what turns "we think nothing was altered" into "two independent records agree".
See Audit integrity.
#Retention
Configurable, requiring audit:configure_retention — step-up gated, writing a
CRITICAL audit event.
Shortening retention destroys evidence
That is why it is a high-impact, step-up-gated, CRITICAL-audited operation. A
request to "clean up old audit data" leaves a mark explaining who asked and why.
Export and archive before you shorten:
bashveefore audit export --format json \
--reason "Pre-retention-change archive" --out archive.json
The archive is watermarked and hashed, so it remains verifiable afterwards.
#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:
bashveefore logs --limit 100 # the decision
curl -s $CP/v1/actions/act_01ABC … # the full lineage
curl -s $CP/v1/audit-events/evt_01ABC … # the audit event
# plus every log line, if you index on requestId
That is the intended path, and it is why the ids are on the response rather than only in the logs.
#What an export contains
For a protected action: the actor and its type, the resolved authority and delegation chain, every policy consulted with versions, every rule that matched, 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 the credential material. Never a private key. Masked parameter paths are
masked in audit too, because a MASK decision redacts the value in the trail as
well as in the forwarded request.