feat(audit): add the audit log #9

Merged
ryan.mcgee merged 29 commits from feat/audit-log into main 2026-10-15 18:07:29 +00:00
Owner

Summary

  • An audit log: who changed what in an organization, and who signed in to its protected resources, readable by organization admins.
  • Stacked on #8 (feat/api-keys); retarget to main once #2 to #8 are merged.

Changes

  • audit_events table (reversible, portable, append-only): actor type and snapshot label (user email, API key <name> (tck_...), or a visitor's address), action, subject type/id/label (no foreign key, so events outlive their subject), JSON metadata, microsecond timestamp.
  • Audit.write/4, Audit.update/4 and Audit.record/4 write the event in the same transaction as the change. An update that changes nothing writes nothing and leaves no event. Wired into organizations, memberships, invitations, nodes, resources, targets, headers, access settings, rules, share links and API keys.
  • Sign-ins: successful and failed password, PIN, SSO and share-link sign-ins, with the visitor's address. Throttled attempts and SSO attempts by non-members are deliberately not logged, so nobody can flood an organization's log.
  • Secrets never enter the log: passwords and PINs appear as [changed]; header values (which can be credentials for the backend), CA text, tokens and hashes are not stored.
  • Only admins signed in as users can read it; API keys cannot. Events are pruned by a supervised process after TRAEFIK_CONTROL_AUDIT_RETENTION_DAYS (default 90, 0 keeps them).
  • UI: an Audit log page (category filter, cursor pagination, follows new events, offers a refresh instead of replacing a page the admin scrolled back on), linked from the organization page.
  • README section, configuration table and status update.

Verification

  • 741 tests pass on SQLite and PostgreSQL; format, compile --warnings-as-errors, credo --strict clean; mix dialyzer 0 errors on both backends.
  • Table-driven wiring test: a full lifecycle of administrative actions produces exactly the expected events, once each, attributed to the admin, with the right subjects. No-ops, validation failures and unauthorized writes leave none. A secrets sweep checks the whole log for every credential used. Sign-in, invitation-acceptance, API-key and share-link attribution are tested through the real contexts and controllers. The pruner process is tested, as are pagination (including identical timestamps), category filters and tenant isolation.
  • Migration up, down, up on populated databases on both backends (54 events written by the fixtures): all other tables unchanged, SQLite integrity and foreign-key checks clean; also applied to the existing populated dev database.
  • Live (compose stack, real browser): changes made in the UI, an API key creating and deleting a resource, and a visitor failing then passing the password all appeared on the audit page with the right actors and details, and no secret appeared anywhere on it. Dev data restored.
  • Observed: in the dev stack the visitor address is the container network's. Behind your own proxy, set TRAEFIK_CONTROL_TRUST_PROXY (documented).

Not included

  • An API endpoint for the log (read keys act as members, who may not see it), exporting the log, and showing node names instead of ids in a resource's node assignment events.
## Summary - An audit log: who changed what in an organization, and who signed in to its protected resources, readable by organization admins. - Stacked on #8 (`feat/api-keys`); retarget to `main` once #2 to #8 are merged. ## Changes - `audit_events` table (reversible, portable, append-only): actor type and snapshot label (user email, `API key <name> (tck_...)`, or a visitor's address), action, subject type/id/label (no foreign key, so events outlive their subject), JSON metadata, microsecond timestamp. - `Audit.write/4`, `Audit.update/4` and `Audit.record/4` write the event **in the same transaction** as the change. An update that changes nothing writes nothing and leaves no event. Wired into organizations, memberships, invitations, nodes, resources, targets, headers, access settings, rules, share links and API keys. - Sign-ins: successful and failed password, PIN, SSO and share-link sign-ins, with the visitor's address. Throttled attempts and SSO attempts by non-members are deliberately not logged, so nobody can flood an organization's log. - Secrets never enter the log: passwords and PINs appear as `[changed]`; header values (which can be credentials for the backend), CA text, tokens and hashes are not stored. - Only admins signed in as users can read it; API keys cannot. Events are pruned by a supervised process after `TRAEFIK_CONTROL_AUDIT_RETENTION_DAYS` (default 90, `0` keeps them). - UI: an Audit log page (category filter, cursor pagination, follows new events, offers a refresh instead of replacing a page the admin scrolled back on), linked from the organization page. - README section, configuration table and status update. ## Verification - 741 tests pass on SQLite and PostgreSQL; format, `compile --warnings-as-errors`, `credo --strict` clean; `mix dialyzer` 0 errors on both backends. - Table-driven wiring test: a full lifecycle of administrative actions produces exactly the expected events, once each, attributed to the admin, with the right subjects. No-ops, validation failures and unauthorized writes leave none. A secrets sweep checks the whole log for every credential used. Sign-in, invitation-acceptance, API-key and share-link attribution are tested through the real contexts and controllers. The pruner process is tested, as are pagination (including identical timestamps), category filters and tenant isolation. - Migration up, down, up on populated databases on both backends (54 events written by the fixtures): all other tables unchanged, SQLite integrity and foreign-key checks clean; also applied to the existing populated dev database. - Live (compose stack, real browser): changes made in the UI, an API key creating and deleting a resource, and a visitor failing then passing the password all appeared on the audit page with the right actors and details, and no secret appeared anywhere on it. Dev data restored. - Observed: in the dev stack the visitor address is the container network's. Behind your own proxy, set `TRAEFIK_CONTROL_TRUST_PROXY` (documented). ## Not included - An API endpoint for the log (read keys act as members, who may not see it), exporting the log, and showing node names instead of ids in a resource's node assignment events.
- events record who changed what (members, invitations, nodes,
  resources, targets, headers, access settings, rules, share links,
  API keys) and who signed in to protected resources, with the actor
  (user, API key or visitor address), the subject and what changed
- each event is written in the same transaction as its change, so a
  change that cannot be logged does not happen; updates that change
  nothing leave no event
- secrets never enter the log: passwords and PINs appear as [changed],
  and header values, CA text, tokens and hashes are not stored
- throttled attempts and SSO attempts by non-members are not logged,
  so the log cannot be flooded
- admins read it on a filterable, paginated page that follows new
  events; events are pruned after
  TRAEFIK_CONTROL_AUDIT_RETENTION_DAYS (default 90)
ryan.mcgee changed target branch from feat/api-keys to main 2026-10-15 18:07:19 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
White_Ghost_Cyber_Systems/traefik-control!9
No description provided.