vitrinka.docs

Audit trail

One append-only table per deployment, written inside the transaction of the action it records, and exported off the host as signed, hash-chained batches.

One table

Security-relevant events — sign-ins and denied attempts, connector consents and revocations, share links, break-glass use, forced-SSO refusals, platform-admin grants, key destruction — are appended to one audit_log table in the control database, on the hosted service and on a self-hosted install alike.

ColumnMeaning
tsWhen, UTC.
org_id, workspace_idThe scope the event belongs to.
actor_kind, actor_id, actor_labelWho — a user, a token, a connector, a rule — and how it was named at the time.
on_behalf_ofThe user an agent or token acted for, when different from the actor.
actionWhat happened.
target_kind, target_id, target_labelTo what, and how it was named at the time.
resultAllowed or denied.
ip, user_agentWhere from — recorded for actions by a person; token and agent rows name the credential instead. The address is the X-Real-Ip your reverse proxy forwards, taken only from loopback or a proxy listed in VITRINKA_TRUSTED_PROXIES; from any other peer the header is ignored and the connection's own address is recorded, so a caller cannot write an arbitrary address into the log. Have the proxy overwrite that header, and list it when it reaches the server over the network — otherwise the column holds the proxy's address.
detailA JSON object with the event's specifics, redacted of secrets.

Append-only, in the same transaction

Two SQLite triggers, audit_log_no_update and audit_log_no_delete, abort any UPDATE or DELETE against the table, so the database itself rejects an edit through SQL. Rows are written inside the caller's transaction: the action and its audit row commit together or not at all, and a denied attempt leaves its row even though nothing else changed.

The trail is kept for the life of the organization and is not subject to the retention windows.

The triggers bind the database, not whoever holds its file: the export copies the trail off the host as hash-chained, signed batches that vitrinka audit-verify checks offline.

Export off the host

VITRINKA_AUDIT_EXPORT sends the trail where the host cannot rewrite it: every VITRINKA_AUDIT_EXPORT_EVERY the exporter takes the rows since its last batch, writes them as one JSONL object in which every line carries the SHA-256 of the line before it and of its own content, signs the batch with the deployment's Ed25519 key, and puts it on the target. The hosted service's target is an object-storage bucket with object lock; an air-gapped install uses a local write-once directory. An export that fails is logged and counted — vitrinka_audit_export_failures_total, and from the second failure in a row vitrinka_audit_export_repeat_failures_total, the one to page on, both on the VITRINKA_METRICS_ADDR listener; the rows stay in the table and go with the next batch.

VariableDefaultNotes
VITRINKA_AUDIT_EXPORToffoff, dir or s3.
VITRINKA_AUDIT_EXPORT_DIRDATA_DIR/audit-wormThe write-once directory for dir.
VITRINKA_AUDIT_EXPORT_S3_ENDPOINT VITRINKA_AUDIT_EXPORT_S3_REGION VITRINKA_AUDIT_EXPORT_S3_BUCKET VITRINKA_AUDIT_EXPORT_S3_PREFIX—The bucket for s3. Turn on object lock and versioning so a batch cannot be replaced.
VITRINKA_AUDIT_EXPORT_S3_ACCESS_KEY_ID VITRINKA_AUDIT_EXPORT_S3_SECRET_ACCESS_KEY—A credential that may put but not delete. Secret.
VITRINKA_AUDIT_EXPORT_EVERY5mHow often a batch is written.
VITRINKA_AUDIT_EXPORT_RETAIN_DAYS—s3 only: stamps each batch with compliance-mode Object Lock retention for that many days and reads it back after the put — a bucket that ignores the lock fails the export. Unset stamps nothing and leaves retention to the bucket's own default. The dir target never expires anything.

vitrinka audit-verify reads a stored export end to end, checks every hash link and every signature, and compares the chain against the live table where one is reachable; it exits non-zero on the first broken link and names it. Verifying an offline copy needs the deployment's public signing key, which vitrinka audit-export-key prints — keep a copy outside the host, so a verifier never has to trust the machine it is checking.

vitrinka audit-export-key -data /data                                    # prints the public signing key
vitrinka audit-verify -data /data                                        # the live table against the export target in the environment
vitrinka audit-verify -dir /mnt/audit-worm -key /root/audit.pub          # an offline copy on its own; -key takes base64 or a file
vitrinka audit-verify -dir /mnt/audit-worm -key /root/audit.pub -from-hash <hex>   # resume a partial check from a known-good link

The recovery procedure — a gap after an outage, a rotated signing key, a bucket that lost its lock — is the runbook at docs/runbooks/audit-export.md in the source repository.

Reading it

The trail is in the product under Settings → Organization → Audit log, and at GET /api/v1/audit, within the caller's scope in the workspace it addresses: a workspace admin sees that workspace plus their own account-level actions such as sign-ins, an organization admin also sees the organization's own events, a platform admin sees everything, and everyone else sees only their own actions. Filters: actor, action, target, from and to (RFC 3339), q for free text over actor labels, actions, targets and detail with typo tolerance, limit and cursor for paging.

On this page