Audit Events
Catalog of every event the exd-server audit log emits. The codes here are normative API surface — SIEM rules, compliance queries, and incident timelines grep against the audit_event field.
Source:
crates/exd-server/src/audit.rs(event constants) and the call sites incrates/exd-server/src/routes/,crates/exd-server/src/bin/admin.rs. New codes go through this page first; the reservation isn't real until a row lands here.
Wire format
Every audit line is a regular tracing::info! event with target exd_server::audit. In EXD_LOG_FORMAT=json mode (production default) it lands as one NDJSON line carrying:
{
"timestamp": "2026-05-15T14:23:11.482Z",
"level": "INFO",
"target": "exd_server::audit",
"message": "audit",
"audit_event": "tenant.created",
"tenant": "acme",
"span": {
"request_id": "01HXY3Z6Q9NQF6P2K2T4ZR3T8V",
"principal_kind": "superadmin",
"principal_id": "7c4a8d09",
"method": "POST",
"route": "/api/v1/tenants",
"name": "http_request"
}
}
The §3 field set (request_id, principal_kind, principal_id, tenant, namespace, method, route) is inherited from the surrounding request span when one is active. Audit lines that originate outside a request — bootstrap.run from the admin CLI, for example — carry only the event-specific fields.
To route audit lines to a separate sink (write-only S3, SIEM, tamper-evident store), filter on target == "exd_server::audit" in the log shipper. Today the audit lines are interleaved with the request log on the same stdout/stderr stream; a follow-up will support a dedicated file descriptor (fd:3) for sidecar fan-out.
Catalog
| Code | When it fires | Source | Status | Event-specific fields |
|---|---|---|---|---|
tenant.created | POST /api/v1/tenants returns 201 | routes/tenants.rs::create | Shipped | tenant (the new slug) |
namespace.created | POST /api/v1/tenants/{tenant}/namespaces returns 201 | routes/namespaces.rs::create | Shipped | tenant, namespace (the new slug) |
token.minted | POST /api/v1/tokens returns 201, or exd-server-admin token mint (CLI), or bootstrap.run minting the first superadmin | routes/tokens.rs::create, bin/admin.rs::bootstrap | Shipped | token_id, token_kind, tenant?, namespace?, source (api / bootstrap — only when CLI-minted) |
token.revoked | DELETE /api/v1/tokens/{id} returns 200 with non-idempotent path | routes/tokens.rs::revoke | Shipped | token_id, token_kind, revoked_by, self_revoke (bool) |
manifest.version_accepted | A new manifest version commits — PUT /manifest or git smart-HTTP push | routes/manifest.rs::upload, routes/git.rs::receive_pack | Shipped | source (upload / git-push), tenant, namespace, version, token_id? |
bootstrap.run | exd-server-admin bootstrap succeeds (initial superadmin minted, closure-signing key generated). Fires at most once per database. | bin/admin.rs::bootstrap | Shipped | token_id (the new superadmin's id) |
git.force_push_accepted | The pre-receive hook accepts a non-fast-forward push to refs/heads/main (only tenant-admin and superadmin tokens can authorize one) | bin/git_hook.rs | Deferred | (planned) tenant, namespace, token_id, old_sha, new_sha |
git.force_push_accepted — deferred status
The pre-receive hook is a separate subprocess invoked by git http-backend. Today it writes plaintext rejection messages to stderr; emitting structured audit lines from there requires either:
- Initializing tracing inside
bin/git_hook.rswith a stderr-only writer (so structured audit lines don't interleave with git's own protocol output on stdout), or - Routing the audit event through the database (the hook inserts a row, the server polls and emits via tracing).
Both paths are tractable but didn't make the v0 cut. The constant audit::events::GIT_FORCE_PUSH_ACCEPTED is reserved against future use.
What is deliberately not audited
- Authenticated reads.
GET /manifest,POST /evaluate,GET /readyz, the closure download, the SSE event stream. These are high-volume and low-value for a compliance trail; the request log already records them with the §3 field set if you need read-side context. - 4xx responses. A failed-auth 401, a 404 on a missing namespace, a 422 on a malformed upload — none of these emit audit events. The compliance trail tracks completed operations.
- Internal state changes. Eval cache invalidations, push-lock acquisitions, SSE connection bookkeeping. Operational, not compliance-relevant.
See also
docs/user-guide/exd-server-observability.md§9 — the design context for why the audit log is a separate stream.crates/exd-server/src/audit.rs— the constants, single source of truth.crates/exd-server/tests/audit_log.rs— one test per shipped code.