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
| Place | Who | What it shows |
|---|---|---|
| Administration → Audit Log | global admin | Every event in the installation, filterable. |
| Project → Settings → Audit Log | project maintainer | The project's own slice — the same view, scoped server-side. |
| The instance's timeline in its status panel | project viewer | One 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
SYSTEMactors 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.