Skip to content

Audit Log

Every action in dockmesh — deploy, scale, migrate, user invite, role change, SSO login, backup run — is written to an audit log. Entries chain together with SHA-256 hashes, so altering any row breaks the chain and the UI flags the tampering.

CategoryExamples
AuthenticationLogin success/failure, SSO handshake, 2FA verify, session end
RBACRole create/update/delete, user role assignment, permission check failures
StacksCreate, edit, deploy, stop, scale, migrate, remove
ContainersExec session start (not the content), start/stop/kill, remove
HostsAdd, remove, drain
BackupsJob create/edit/delete, run (success/failure), restore
SettingsOIDC config change, env var change, TLS cert rotation

Read-only actions (list pages, inspect) are not logged — it would drown the chain in noise without changing the security story. There is no toggle for read-logging today.

Audit rows are flat (no nested actor/target sub-objects):

{
"id": 24318,
"ts": "2026-04-17T14:02:31.448Z",
"user_id": "1ca3fb94-269d-4eea-9215-616256dc2c61",
"username": "alice",
"action": "stack.deploy",
"target": "analytics",
"details": "{\"services\":3}",
"prev_hash": "8d21...e3f0",
"row_hash": "a7f3...9c4b"
}
  • details is a free-form JSON string per action — different actions encode different keys here. Inspect-with-care code should parse it as nested JSON.
  • row_hash is SHA-256(prev_hash || canonical_serialisation_of_this_row). The chain is anchored by audit-genesis.sha256 on disk.
  • IP / session / SSO provider are not on the row directly — they’re encoded inside details for the action types where they apply (auth.login, auth.sso_*).

On every page load, the UI runs a verification pass on the visible range. If any entry’s hash doesn’t match the computed hash of its prev_hash + content, the UI shows a red “Chain broken at entry #N” banner.

Common reasons a chain might break:

  • Manual DB edit (not through dockmesh)
  • Restoration from an older backup that overwrites later entries
  • Disk corruption

A broken chain is never “fixed automatically” — investigating is on you. The UI preserves both the computed and stored hash so you can see exactly where divergence happened.

Audit → Search supports two filters today:

  • Action — accepts a prefix, e.g. stack matches every stack.* action, stack.deploy only that one
  • User — filter by user id

Combine the two (AND). Results paginate at 100 entries by default (configurable up to 1000 via the limit query parameter). Target / result / IP / time-range filtering is not yet wired into the API — track that as a follow-up; for now, pull the data and filter client-side.

There is no built-in Export button in the UI yet. The audit.export RBAC permission is defined and reserved for the feature, but the endpoint hasn’t shipped. For SIEM forwarding, use the audit webhook below to stream entries as they’re written, or query GET /api/v1/audit and serialise the JSON yourself.

Default: keep forever. For teams that need less, configure a retention policy under Settings → Audit → Retention:

ModeBehaviour
forever (default)Never prune.
daysKeep last N days; older rows are deleted on the 03:00 daily job.
archive_localWrite older rows as NDJSON to a configurable local directory, then prune from DB.
archive_targetSame, but write to any configured Backup Target (SFTP, SMB, WebDAV, S3, local).

Archive-before-prune means a write failure halts the job — data is never lost to a broken archive destination. Before each prune, dockmesh writes a special audit.chain_bridge audit entry that records the prune count, last-pruned row hash, and cutoff timestamp. The hash chain remains verifiable across prune boundaries: GET /audit/verify still walks from the oldest remaining row forward and never surfaces a break.

You can also trigger a run immediately (after changing policy or for a one-off cleanup) via the Run now button on the same panel, or POST /api/v1/audit/retention/run.

Send every audit entry to a webhook in real time:

Settings → Audit → Webhook:

  • URLhttp(s)://…. Empty disables dispatch entirely.
  • HMAC secret — optional. When set, every request carries X-Audit-Signature: sha256=<hex> computed over the raw body.
  • Filter actions — list of exact action names (stack.deploy) or wildcard prefixes (stack.*, rbac.*). Empty = every entry.

Delivery is decoupled from the request path: a dispatcher goroutine drains a bounded queue (256 entries) so a slow receiver never blocks your stack deploys. On queue overflow the oldest pending event is dropped (logged). On failure the dispatcher retries up to 5 times with 500ms × 2^n backoff; 4xx responses are terminal (the receiver rejected the payload), 5xx and network errors retry.

Use the Send test event button to verify the receiver before flipping on the live feed — it bypasses the filter and posts a synthetic audit.webhook_test entry.

Payload shape:

{
"id": 24318,
"ts": "2026-04-17T14:02:31.448Z",
"user_id": "...",
"action": "stack.deploy",
"target": "analytics",
"details": "{...}",
"row_hash": "a7f3...9c4b"
}
  • RBAC — the audit.view and audit.export permissions
  • Hardening — audit log protection best practices
  • API · Audit — programmatic query interface