Skip to content
SHC Docs

Audit module

Source: modules/audit/ (ce module); the licence gate overlay lives in this repo’s modules/audit/.

CLIshc events --audit (owned by the events module)
Subcommands0
HTTP endpointssee the route reference
Events emitted
Error codessee the error code reference
Service classesAuditService
Cross-module deps
modules/audit/
├── command.go
├── errors.go
├── events.go
├── router.go
├── service.go
├── models.go
└── commands/
  • shc events --audit — view audit logs (the standalone shc audit command was absorbed into the events module)

Routes are listed in the generated route reference; request/response notes live in the API reference.

The audit module emits no topics of its own — it persists and serves the events other modules publish. The auth failure/permission topics this page previously tabulated are registered by the auth module; see the generated event reference.

Typed errors are catalogued in the error code reference with HTTP statuses and message templates.

The audit module owns persistent, tamper-evident history of every significant action in SHC. It writes to the AuditModel table, one row per event, in a hash chain: each row stores prev_hash + its own computed hash over (code, message, payload, prev_hash, …). Breaking the chain is detectable after the fact.

The single public class is AuditService in service.go:

  • Persist(event, ...) — accepts an event, resolves the code and the format-string msg, captures the current actor (OS user or authenticated principal via getActor), captures the OpenTelemetry trace/span IDs via getTraceInfo, reads the previous row’s hash, computes the new row’s hash, and commits. Tenant / stack / environment from context are recorded for scoped queries.
  • List(...) / Count(...) — scoped reads filtered by tenant, stack, environment, event code, actor, and time range. Used by the CLI (shc events --audit) and the HTTP endpoints under /api/method/shc.audit.*.

Audit rows are also republished on the event bus (see events) so the UI can render them live without polling, and so multi-node clusters can correlate audit events across nodes by trace ID.

What the audit module does not own: the event types themselves — those are defined in each owning module’s events.go and use event code constants. This module only records the fact that an event happened. It also does not own broadcasting to SSE clients (that is events).

AuditModel first-class columns (in addition to event code, message, payload, hash chain):

FieldDescriptionResolution order
actor_idWho performed the actionJWT sub → CLI os.UserLookup → server user
actionThe operation performedMnemonic from event (e.g. STKSCL)
originConnection methodcli / api / socket / system
client_ipOriginating IPleftmost X-Forwarded-ForX-Real-IP → socket IP; UDS = local
client_hostOriginating hostnameresolvable hostname or local for UDS
server_hostServer hosthostname where the SHC server is running
trace_idDistributed trace IDOpenTelemetry trace context
span_idDistributed span IDOpenTelemetry trace context

Three sources, in priority order:

  1. Authenticated Keycloak session — every TCP request carries a Bearer token that the auth middleware resolves to a User; the token’s preferred_username becomes actor_id.
  2. Unix-domain-socket superadmin — UDS requests are tagged by UDSMarkerMiddleware and treated as LocalUser() — they bypass Keycloak entirely and are recorded as a local superadmin actor.
  3. System fallback — for system-triggered tasks (crons, internal reconciliation), actor_id is the OS user running the SHC server.

Vault operations are first-class audit events:

  • A prefixset, update, delete, export, import, and get (read).
  • V prefixlist (listing keys is telemetry, not audit).

Audit rows must never contain secret values, only the keys accessed.

All mutable API endpoints emit an audit event on success. The event is emitted in the service layer after the mutation succeeds, not in the router:

func (s *StackService) Scale(ctx context.Context, ...) (*ScaleResult, error) {
// ... perform scaling ...
log.Event(ctx, events.A200400STKSCL, map[string]any{
"service": service,
"replicas": replicas,
"stack": stackName,
})
return &ScaleResult{...}, nil
}

This guarantees an audit row exists for every observed state change, regardless of whether the trigger was a CLI call, HTTP request, or internal cron.