Skip to main content
Version: 1.3

Audit log

Enterprise

Who did what, when — state-changing and security-relevant actions recorded as structured, append-only events: actor, action, target, outcome, timestamp, and the context that makes a row useful on its own. Recording is an Enterprise feature (Licensing); reading is never gated, so history recorded while entitled stays visible on every tier.

Where to read it​

PlaceWhoWhat it shows
Administration → Audit Logglobal adminEvery event in the installation, filterable.
Project → Settings → Audit Logproject maintainerThe project's own slice — the same view, scoped server-side.
The instance's timeline in its status panelproject viewerOne instance's events, newest first: who created, deployed and stopped it, and how each attempt ended.

Filters narrow by actor, action, outcome, target and a time window, and free-text search matches actor names, target names and projects. The same queries are available over the API (GET /api/admin/audit, GET /api/projects/{project}/audit) for scripting.

Times are yours, storage is UTC​

Every timestamp is stored in UTC and shown in your browser's own time zone and date format, so the same event reads 4:04 PM for a colleague in Prague and 7:04 AM for one in California. Picking a day in the time-range filter means that day where you are, matching the column beside it — so a window never hides a row that the listing would have shown.

The clock follows Settings → Appearance → Time format: Automatic takes its cue from your browser's locale, or you can pin 24-hour or 12-hour. That setting governs every timestamp Fadebox renders, not just this page.

Over the API a timestamp is an ISO-8601 instant with its offset spelled out — 2026-08-16T09:30:00Z — so a script can read it without knowing anything about the server. A query window's from and to take the same form; the window is half-open — from is inclusive, to exclusive — so back-to-back day windows tile without dropping or double-counting an event on the boundary. A value can be copied straight out of a response and sent back as a bound. A bare wall-clock time (2026-08-16T09:30:00) is refused with a 400, because without an offset it names no single moment — which is exactly the ambiguity the rest of this section exists to remove. Convert a hand-written local time to an instant first, the way the UI does when you pick a day.

What is recorded​

Sign-ins — including failed ones, each with its reason, and the moment an account locks after too many of them (user.login_locked, with the count and the deadline; a request refused by the per-address throttle writes nothing, so a flood cannot inflate the log) — and everything that changes state: users, groups and membership; API keys; identity providers; the license; projects, environments and templates; the whole instance lifecycle (create, deploy, stop, delete, extend, move, expiry sweeps, and whether it is shared or personal — instance.sharing_changed) including log exports and the log captured when a stop or reset removes its containers (instance.logs_captured, with the attempt, the byte count and which services could not be read), and every change in an instance's health (instance.health_changed, attributed to SYSTEM); what a person or a pipeline says about an instance — its purpose and links (instance.purpose_updated), the checks a pipeline reports (instance.check_reported, instance.check_removed) and the verdict somebody reaches (instance.verdict_set, instance.verdict_withdrawn), each naming the deploy attempt it landed on, so a replaced check or a second verdict leaves the first one readable; every access token minted for a gated instance (instance.access_token_issued, recording which hostnames it covers and never the value); runtimes and their certificates; ingress installations and the system-image registry (system.image_registry_changed, carrying the new prefix or that it was reset); the Prometheus exporter's switch (system.metrics_export_changed) and the one that shows instance creators' email addresses (system.creator_email_changed); every image reclaim on a runtime (runtime.images_reclaimed, with the count and the bytes freed, whether an admin, a project's deployer or the retention timer did it); and fadebox's own cleanup jobs.

Connecting an agent is recorded too. Approving a consent request on the MCP endpoint records oauth.grant_approved against the user who approved it — the moment a machine credential comes into existence, and the answer to "who connected this agent, and when". A token that fails to verify records oauth.auth_failed, the way a failed sign-in does, naming the credential's public id rather than a user: a token that does not verify names nobody.

Each event carries the actor (a user, a service account — with the API key that authenticated the request — or SYSTEM for fadebox's own jobs), the action, the target and its project, SUCCESS or FAILURE, when it happened, a source IP where the request carries one, and action-specific detail: the deploy's error, the export's byte count, the role a membership granted.

On a rejected credential the address is the record. A failed sign-in, a bad API key or an access token that does not verify names no user, so where it came from — and how often — is what the row is for. Behind a reverse proxy that address is the caller's only if the proxy forwards it: with no X-Forwarded-For every event carries the proxy's own address, the same value for every user. See Reverse proxy.

Three attribution rules worth knowing:

  • Async outcomes belong to whoever asked. A deploy finishes on a worker thread, but its outcome event names the user who requested it — "who deployed this" returns a person on the outcome row too, never a pool thread.
  • Fadebox's own jobs are SYSTEM actors named after the job (instance-expiry, startup-sweep, retention), never an invented user.
  • An event exists only if the change actually happened. Rolled-back work records nothing, and failures — a failed login, a failed deploy — are recorded where the failure became final.

Recording follows the license​

Below Enterprise the recorder simply drops events — the feature does not exist there — but the pages and the API stay open, showing whatever was recorded while entitled. A downgrade stops new events without hiding or deleting history: there are no carve-outs to non-destructive downgrades.

Shipping to a SIEM​

Setting FADEBOX_AUDIT_LOG_MIRROR=true additionally emits every recorded event as one JSON line on the fadebox.audit logger, so an existing log pipeline can forward events without touching the database. The mirror is a feed, not the record — the database stays authoritative.

Retention​

Events are kept forever by default. Setting FADEBOX_AUDIT_RETENTION (ISO-8601, e.g. P365D) enables a periodic purge of older events — and the purge records its own audit.purged event on every tier, so the log never shrinks silently. Retention is deliberately configuration-only, with no UI: pruning policy belongs in reviewable deployment configuration, not behind an admin session. The variables are in the configuration reference.