Skip to main content
Version: Next

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 a person an OIDC provider authenticated but its required claims refused (user.login_refused, with the provider and the claim of the first rule that failed — the claim's name, never its value, which is the person's email, tenant or groups; email_verified when an email rule's address matched but was not verified) — 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 — and who may open its URLs, the access level it ends on — instance.gate_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), and every time a global admin is let through an instance's gate that their roles in its project would not have opened (ingress.admin_admitted, with the host and the project — a sign-in the gate admits by a role is not recorded); 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.

On an elastic runtime the machines are recorded as well, and between them they are the record of what a machine was charged for: machine.created when the provider accepted it, machine.started and machine.stopped bracketing each interval it was billed by the hour for, machine.failed with the provider's reason, machine.expired when stopped-machine retention gave one up — naming the retention, how long it had been off and whose instance's disk it was — and machine.destroyed, the last thing ever said about a machine and the only place its bill is recorded as ended for good. Their actor is fadebox's own machine sweep, with whoever asked for the machine in the detail, so the two trails join. Two more come from reconciling a runtime with its cloud account: machine.reattached when a machine whose creation fadebox never heard the answer to gets its handle back — the one write a reconcile pass makes, its actor fadebox's own reconcile job — and machine.reconciled when an admin asks for a pass on demand, recorded against the runtime with the counts it found and that admin as the actor. The pass that runs every ten minutes on its own writes nothing, so a fleet nobody is touching does not produce an event every ten minutes. The sizes an admin offers are recorded too (runtime.machine_profile_set, runtime.machine_profile_deleted).

Two are an admin reaching into a machine's lifecycle by hand. machine.reclaimed is a machine destroyed at the provider that fadebox had no row for — and since the row that would have said it existed is exactly what was missing, this event is the only record that it ever did: it carries the runtime, the name and the handle the provider knew it by, its three ownership labels, and when the platform said it was made. One event per machine actually destroyed, and none for one that was refused, so the trail is what happened rather than what was attempted. machine.retry_requested is an admin asking for a stop or a destroy the provider keeps refusing to be tried at once rather than on the slow cadence; nothing is asked of the provider by it, and it is recorded because the destroy that follows — or does not — would otherwise be attributed to the sweep alone.

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, machine-sweep, 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.