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.
What gets logged
Section titled “What gets logged”| Category | Examples |
|---|---|
| Authentication | Login success/failure, SSO handshake, 2FA verify, session end |
| RBAC | Role create/update/delete, user role assignment, permission check failures |
| Stacks | Create, edit, deploy, stop, scale, migrate, remove |
| Containers | Exec session start (not the content), start/stop/kill, remove |
| Hosts | Add, remove, drain |
| Backups | Job create/edit/delete, run (success/failure), restore |
| Settings | OIDC 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.
Entry shape
Section titled “Entry shape”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"}detailsis a free-form JSON string per action — different actions encode different keys here. Inspect-with-care code should parse it as nested JSON.row_hashisSHA-256(prev_hash || canonical_serialisation_of_this_row). The chain is anchored byaudit-genesis.sha256on disk.- IP / session / SSO provider are not on the row directly — they’re encoded inside
detailsfor the action types where they apply (auth.login,auth.sso_*).
Hash chain integrity
Section titled “Hash chain integrity”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.
Search and filter
Section titled “Search and filter”Audit → Search supports two filters today:
- Action — accepts a prefix, e.g.
stackmatches everystack.*action,stack.deployonly 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.
Export
Section titled “Export”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.
Retention
Section titled “Retention”Default: keep forever. For teams that need less, configure a retention policy under Settings → Audit → Retention:
| Mode | Behaviour |
|---|---|
forever (default) | Never prune. |
days | Keep last N days; older rows are deleted on the 03:00 daily job. |
archive_local | Write older rows as NDJSON to a configurable local directory, then prune from DB. |
archive_target | Same, 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.
Webhook
Section titled “Webhook”Send every audit entry to a webhook in real time:
Settings → Audit → Webhook:
- URL —
http(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"}See also
Section titled “See also”- RBAC — the
audit.viewandaudit.exportpermissions - Hardening — audit log protection best practices
- API · Audit — programmatic query interface