Skip to main content

Audit Trail

vibeD records mutating actions to an append-only audit trail so you can answer "who changed what, when" — a baseline enterprise-governance requirement. The trail is always on; what differs is whether it persists (see Storage) and whether a failed write blocks the action (see Fail-closed mode).

What's recorded

Every recorded event is an AuditEvent. The first block of fields is always present:

FieldMeaning
timeUTC timestamp of the action.
actorAuthenticated user ID ("" when unauthenticated).
actiondeploy, delete, or rollback.
targetApp/artifact name the action operated on.
outcomeok, denied, or error.
detailError message or context, when relevant (omitted otherwise).

Actions and their outcomes:

ActionRecorded whenOutcomes
deploya new app or redeploy (via API or MCP)ok, denied, error
deletean app is deleted (via API or MCP)ok, error
rollbackan artifact is rolled back to a prior versionok, error

outcome=denied is how a quota rejection shows up; error carries the failure reason in detail.

Enrichment fields

Beyond the always-present block, each event carries optional enrichment fields. They are omitted from JSON when empty:

FieldMeaningPopulated by
tenant_idTenant the action belonged to.Core, on deploy, delete, and rollback.
source_hashSHA-256 of the exact deployed source tarball.Core, on deploy (source provenance).
session_idAuth session identifier.An out-of-tree recorder/store.
policy_decisionDeploy-time policy verdict.An out-of-tree recorder/store.
beforePrior state, for a governance diff.An out-of-tree recorder/store.
afterNew state, for a governance diff.An out-of-tree recorder/store.

The core populates tenant_id on every mutating action and source_hash on deploys — a hash of the exact tarball that was injected into the sandbox, so an event ties an action to the precise bytes that ran. The remaining fields (session_id, policy_decision, before, after) are left empty by the core; they exist so an out-of-tree recorder or audit store (for example one that adds tamper-evidence or SIEM export) can enrich events without changing the recorder interface.

Enrichment is carried on the request context: a caller attaches audit.Fields to the context, and every event recorded under that context merges the non-empty values. Empty fields are ignored, so partial enrichment is fine.

Querying it

Admins read the trail over the API (the admin role is required — non-admins get 403):

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://vibed.example.com/v1/audit?actor=alice&action=deploy&app=my-site&limit=100"
{ "events": [
{
"time": "2026-05-24T10:02:11Z",
"actor": "alice",
"action": "deploy",
"target": "my-site",
"outcome": "ok",
"tenant_id": "acme",
"source_hash": "9f2c…e1"
},
{ "time": "2026-05-24T09:58:03Z", "actor": "bob", "action": "delete", "target": "old-demo", "outcome": "ok", "tenant_id": "acme" }
] }

All filters (actor, action, app, limit) are optional; results come back newest-first. Filtering is on the always-present fields only — enrichment fields are returned but not queryable.

Storage

The audit trail is written to a pluggable audit store — the same store backend that holds the rest of vibeD's state:

  • SQLite backend (store.backend: sqlite): events persist in the same database as everything else and survive restarts. Enrichment fields are persisted alongside the core fields.
  • Memory / ConfigMap backends: the trail is kept in memory only and is lost on restart (vibeD logs a warning at startup). The ConfigMap backend does not implement the audit store, so the server falls back to an in-memory audit log there. Use SQLite for a durable audit trail.

Because the store is an interface, an operator or integrator building their own out-of-tree Go module can supply an alternative audit-store implementation (for example a tamper-evident or externally-exported store) without changing the recorder.

Every recorded event also increments vibed_audit_events_total{action,outcome} and is mirrored to structured logs, regardless of the store backend.

Fail-closed mode

By default the audit write is fail-open: if the persistent store rejects an append, vibeD logs a warning and lets the action proceed. This suits dev clusters and the default install, which may not have a persistent audit store wired.

Set audit.failClosed: true to make the trail fail-closed: a mutating action whose audit event cannot be persisted is rejected, so an untraceable mutation never happens. This prefers availability loss over a compliance gap.

audit:
failClosed: false # true → reject the action when the audit write fails

Fail-closed only affects the persistent store write. The Prometheus counter and structured log are updated on every event whether or not the store append succeeds, and a nil (unconfigured) recorder is a no-op that never blocks. Production values.yaml sets failClosed: true.

Egress denials

Blocked outbound connections are logged separately by the egress proxy's authorizer (see Egress Control), not in this trail — they happen in a different process on the request hot path.