# Audit trail (https://docs.vitrinka.ai/security/audit)



## One table [#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.

| Column                                     | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ts`                                       | When, UTC.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `org_id`, `workspace_id`                   | The scope the event belongs to.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `actor_kind`, `actor_id`, `actor_label`    | Who — a user, a token, a connector, a rule — and how it was named at the time.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `on_behalf_of`                             | The user an agent or token acted for, when different from the actor.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `action`                                   | What happened.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `target_kind`, `target_id`, `target_label` | To what, and how it was named at the time.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `result`                                   | Allowed or denied.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `ip`, `user_agent`                         | Where 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. |
| `detail`                                   | A JSON object with the event's specifics, redacted of secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

## Append-only, in the same transaction [#append]

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](/security/retention#control).

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

## Export off the host [#export]

`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.

| Variable                                                                                                                                  | Default               | Notes                                                                                                                                                                                                                                                                                         |
| ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VITRINKA_AUDIT_EXPORT`                                                                                                                   | `off`                 | `off`, `dir` or `s3`.                                                                                                                                                                                                                                                                         |
| `VITRINKA_AUDIT_EXPORT_DIR`                                                                                                               | `DATA_DIR/audit-worm` | The 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_EVERY`                                                                                                             | `5m`                  | How 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.

```bash
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 [#read]

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.
