Audit Logging

Mezite records a structured audit event for every security-relevant action in the cluster: logins, certificate issuance, SSH session lifecycle, access requests, and more. Events are written to the configured backing database (PostgreSQL or SQLite) via the audit Emitter. This guide covers event types, event structure, querying, the Emitter architecture, and external sinks.


Event Types

Every action in Mezite generates a typed audit event. The following table lists the core event types:

Event Type Description
user.loginUser authenticated (local password, SSO, or certificate)
user.login.failedFailed authentication attempt
authz.deniedAuthenticated user refused a webapi action by an RBAC gate (e.g. non-admin POSTing to /v1/webapi/users)
user.cert.issuedUser certificate issued by the CA
session.startSSH session started
session.endSSH session ended (records duration; recording metadata when the session was recorded)
node.joinedAgent registered with the cluster
node.leftAgent disconnected or was removed
user.createdUser account created
access_request.createdAccess request submitted
access_request.approvedAccess request approved
access_request.deniedAccess request denied
access_request.expiredAccess request or granted access expired

Event Structure

Each audit event is a structured JSON document with a consistent schema. All events share common fields, with type-specific fields nested under the relevant key.

Example: session.start event json
{
  "id": "f3a1...uuid",
  "event_type": "session.start",
  "event_code": "T2000I",
  "user_name": "alice",
  "resource_type": "node",
  "resource_name": "web-server-01",
  "server_hostname": "web-server-01",
  "client_ip": "203.0.113.10",
  "timestamp": "2026-03-24T10:16:01.234Z",
  "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "success": true
}

The full AuditEvent schema (defined in server/audit/events.go) also includes cluster_name, user_roles, resource_labels, server_id, impersonator, error_message, and a free-form details map. Most fields use omitempty, so events only carry the columns the emitter explicitly populates. The exact set varies by event type — inspect server/proxy, server/auth, and server/web emit sites to see which fields are set for each event.

Example: user.login event json
{
  "id": "9b2c...uuid",
  "event_type": "user.login",
  "event_code": "T1000I",
  "user_name": "alice",
  "client_ip": "203.0.113.10",
  "timestamp": "2026-03-24T10:15:32.567Z",
  "success": true
}

Today the user.login event does not record which authentication method (password, SSO connector, certificate) was used — clients distinguish that by the surrounding flow, not by a field on the event itself.


Querying Audit Events

Use mezctl audit ls to query the audit log with flexible filtering.

Audit log queries bash
# List events from the last hour (default --since=1h)
mezctl audit ls

# Show events from the last 24 hours
mezctl audit ls --since=24h

# Filter by event type
mezctl audit ls --type=user.login --since=24h

# Filter by user
mezctl audit ls --user=alice --since=7d

# Combine filters
mezctl audit ls --type=session.start --user=alice --since=1h

Example output:

Audit log output text
TIME                   TYPE              USER    RESOURCE            CLIENT_IP        STATUS
2026-03-24 10:15:32    user.login        alice                       203.0.113.10     ok
2026-03-24 10:15:45    user.cert.issued  alice                       203.0.113.10     ok
2026-03-24 10:16:01    session.start     alice   node/web-server-01  203.0.113.10     ok
2026-03-24 10:28:35    session.end       alice   node/web-server-01  203.0.113.10     ok

The Emitter Architecture

Mezite's audit system uses a producer/consumer split between event emit sites and the database writer, so audit writes do not block the critical path of SSH session establishment.

Buffered Async Writes

Most audit events are written asynchronously via a buffered channel. The Emitter accepts events from any goroutine, queues them in a channel buffer, and a background writer flushes them to the configured database in batches.

Emitter architecture text
goroutine A (session.start)    ──┐
goroutine B (session.command)  ──┼──> [buffered channel] ──> background writer ──> database
goroutine C (node.joined)      ──┘
                                       capacity:       4096 events
                                       batch size:     up to 100 events
                                       flush ticker:   500ms

This design means that SSH session establishment is not blocked by database write latency. When the buffer is full, additional async events are dropped with a warning log line — so this path is reserved for high-volume informational events. Security-relevant events use the sync path below.

Sync Writes for Critical Events

Certain high-priority events bypass the buffer and are written synchronously to guarantee they are persisted before the operation completes:

  • Authentication outcomes: user.login, user.login.failed, user.cert.issued
  • Identity mutations: user.created, user.totp_reset, user.webauthn_reset, lock and connector CRUD
  • Every denial event (any *.denied / *.failed)
  • CA lifecycle: ca.cert.issued, ca.rotate.*

Sync writes ensure that if a sensitive change is acknowledged to the admin, it is already recorded in the audit log. See server/audit/CLAUDE.md for the full classification.


External Sinks

In addition to the database, audit events can be mirrored to external sinks for SIEM integration, alerting, or long-term archival. Two sink types are supported today: webhook (POSTs each event as JSON to a URL) and file (appends to a JSONL file on disk). Sinks receive every event after it has been written to the database — there is no per-sink event-type filter yet, so filter downstream if you only want a subset.

Configuring sinks (server/audit/config.go) go
type SinkConfig struct {
    Type           string        // "webhook" or "file"
    WebhookURL     string        // for type="webhook"
    WebhookTimeout time.Duration // defaults to 5s
    FilePath       string        // for type="file"
}

Storage and Retention

Audit events are stored in the configured database (PostgreSQL or SQLite). There is no automatic retention or purge mechanism — events accumulate until you prune them out of band (for example, with a scheduled SQL delete against audit_events).

An S3-compatible archiver exists in server/audit/s3_archiver.go and a mezctl audit archive CLI is wired up, but the server-side ArchiveAuditEvents RPC currently returns Unimplemented. Plan around external retention until the server handler ships.


Troubleshooting

Audit events missing

  • Verify the auth service is connected to its database and migrations are up to date.
  • Check mezhub logs for audit buffer full, dropping event warnings — high-volume informational events use a 4096-event channel and are dropped when the buffer is saturated. Sync events (logins, denials, identity mutations) are never dropped.

Slow audit queries

  • Use filters (type, user, time range) to narrow the query scope.
  • For large deployments, ensure the database has appropriate indexes (applied by migrations under server/migrate/migrations/).

Next Steps