Skip to main content

Overview

Audit Logs in Bifrost Enterprise record administrative activity so operators can review who changed what, when it happened, and which resource was affected. Audit log entries can be signed with an HMAC key, retained for a configurable number of days, viewed in the dashboard, and exported for downstream review.
Audit logs screen

Key Features


Configuration

Configuration Fields

The three archive_* fields only take effect when object_storage is configured.config.schema.json constrains these fields, so an editor wired to it (via $schema) will flag a malformed duration like "6 hours" or an out-of-range archive_max_object_bytes as you type. The gateway itself does not reject such values at startup: each field is normalized independently at runtime, so a value that gets past the schema falls back to its default or is clamped into range rather than failing startup. A typo like "6 hours" silently yields the 24h default, and archive_interval: "10s" is quietly raised to the 5m floor.Because a bad value fails quietly rather than loudly, confirm what actually took effect from the startup log line, which reports all three:

Viewing Audit Logs

Open Governance > Audit Logs in the Bifrost dashboard. The table shows: You can search by initiator, target, IP, or path, and filter by action, outcome, start date, and end date.

Exporting Audit Logs

Users with AuditLogs:Download permission can export the currently filtered audit log results from the dashboard. Supported export formats:

Archiving to Object Storage

By default, audit events live only in the database, which is the source of truth for everything you see in the dashboard (viewing, filtering, HMAC verification, and export). Databases, however, are not ideal for multi-year compliance retention or off-box durability. When you configure object_storage, a background job additionally copies every audit event to an S3-compatible bucket (S3, GCS, MinIO, R2), one time window at a time. This is a copy, not an offload: the full event ends up in both the database and object storage, so each store holds a complete, independent copy.
This differs from Log Exports, where object storage offloads the heavy request/response payload out of the database. For audit logs, object storage is a complete mirror — the database is never trimmed of data by enabling it.

Database vs. Object Storage

Because the two are decoupled, the archive can outlive the database: once retention_days deletes an old row, its object in the bucket is left untouched. Use S3 Object Lock / WORM and bucket lifecycle rules to govern how long the archive is kept.
Retention and archival are independent loops — the cleaner deletes by age and does not wait for a window to be archived. Keep retention_days comfortably larger than archive_interval + archive_grace_period, or rows may be deleted from the database before they are ever copied to the bucket. With the defaults (24h window, 15m grace) the shortest safe setting is retention_days: 2; retention_days: 1 races the archiver.

How Archival Works

Archival runs as a periodic background job, not as part of the request path. Time is divided into fixed windows of archive_interval, and each closed window is archived as a unit, in order:
  1. A window closes. The window [start, end) becomes eligible only once end is further in the past than archive_grace_period. The grace period absorbs clock skew and rows that land slightly late, so a window is not archived while events may still be arriving for it.
  2. A job is enqueued. One job archives one window. Job IDs derive from the window bounds, so in a multi-node cluster every node can tick while exactly one job row is created and exactly one node runs it — no leader election or lock is involved.
  3. Rows are streamed into part objects. Committed rows in the window are streamed from the database and encoded as JSON Lines. A new part is rolled each time the accumulated uncompressed JSONL crosses archive_max_object_bytes; a part boundary never splits a JSON line.
  4. A manifest commits the window. After every part has been durably stored, a manifest.json listing exactly those parts is written. The manifest is the commit point — only once it lands does the archival watermark advance to end, making the next window eligible.
Objects for a window live under a single window prefix:
For example, with "prefix": "acme-prod", "compress": true, and a 24h interval:
Every object also carries tags (type, window_start, window_end, count, and part/parts) so lifecycle rules and consumers can select objects without listing and pattern-matching keys. Manifests are tagged type=audit-logs-manifest, parts type=audit-logs.

Reading the Archive

Always read a window through its manifest, and ignore any object the manifest does not list. A retried window can leave behind an orphaned part from an earlier attempt; the manifest lists exactly the parts that belong to the window, which is what makes it authoritative.
A window that contained no events still gets a manifest, with an empty parts array — a positive record that the window was examined and held nothing. The absence of a manifest means the window is not yet committed, not that it was empty.

Delivery Guarantees

Unlike the earlier per-flush implementation, archival is no longer best-effort — a failed upload is retried rather than logged and dropped, and the watermark will not advance past a window that did not commit. The database nevertheless remains the source of truth: it is what the dashboard, API, export, and HMAC verification read, and it is written independently of whether the bucket is reachable. Object-storage problems delay the archive; they never block audit logging or request handling.

Tuning

  • archive_interval trades archive freshness against object count. Longer windows mean fewer, larger objects and a longer lag before events reach the bucket; shorter windows mean fresher data and more objects. The job wakes at least hourly regardless, so a window that closes shortly after a tick is not left waiting a full 24h.
  • archive_max_object_bytes bounds peak memory: a part is buffered in memory before upload, so expect roughly 2× this value per archiving node when compress is enabled (compression allocates a second buffer). The 128 MiB default implies a ~256 MiB peak. Raise it for fewer, larger objects on nodes with headroom; lower it on memory-constrained nodes.
  • archive_grace_period rarely needs changing. Raise it if you run with meaningful clock skew across nodes.

Configuration

Object Storage Fields

You can point audit archival at its own dedicated (ideally write-once/locked) bucket, or reuse the same bucket as request logs_store with a distinct prefix. The audit-logs/ path segment keeps the two from overlapping.
If the object store cannot be reached at startup, Bifrost logs the error and continues without archival — audit logging to the database is never blocked by object-storage problems. The watermark is persisted, not held in memory, so archival resumes where it left off after a restart or an outage: windows that closed while the archiver was down are archived on subsequent passes until it catches up. Nothing is skipped — provided the rows are still in the database, which is what the retention_days guidance above protects. Catch-up is paced, though: each pass enqueues one window, and passes run every archive_interval (capped at hourly). After a long outage, expect a backlog of N windows to take roughly N passes to drain — so a multi-day outage with a 24h interval clears at about one window per hour, not all at once. On shutdown the archiver stops before the object store is released, and an in-flight window is allowed to finish rather than being cancelled, so parts are never left behind without a manifest.

API Reference

For the exact request and response contract, see the API Reference.