Skip to main content

Audit logging

The ToolHive Registry Server provides structured audit logging for all admin and consumer API operations. Audit logs give security teams the visibility they need to investigate incidents, satisfy compliance requirements, and track who did what in the registry.

Enable audit logging

Audit logging is disabled by default. Add an audit section to your configuration file to enable it:

config.yaml
audit:
enabled: true

With no other options set, audit events are written to stdout alongside application logs, in JSON format.

Helm

In a Helm deployment, set the same values under config.audit in your values.yaml:

values.yaml
config:
audit:
enabled: true

Configuration options

OptionTypeDefaultDescription
enabledboolfalseEnable audit logging
logFilestring""File path for audit logs. Empty means stdout.
eventTypes[]string[]Allowlist of event types to log. Empty means all events.
excludeEventTypes[]string[]Blocklist of event types to suppress. Takes precedence over eventTypes.
includeRequestDataboolfalseCapture request bodies in audit events (mutating requests only).
maxDataSizeint1024Maximum request body size in bytes. Hard limit: 1 MB.

Audit event types

The registry server emits events across three API surfaces.

Admin API (/v1/)

Resource lifecycle events:

Event typeTrigger
source.createSource created (PUT, HTTP 201)
source.updateSource updated (PUT, HTTP 200)
source.deleteSource deleted
source.listSources listed
source.readSource retrieved
source.entries.listEntries listed for a source
registry.createRegistry created (PUT, HTTP 201)
registry.updateRegistry updated (PUT, HTTP 200)
registry.deleteRegistry deleted
registry.listRegistries listed
registry.readRegistry retrieved
registry.entries.listEntries listed for a registry
entry.publishEntry published to a managed source
entry.deleteEntry deleted
entry.claims.updateEntry claims updated
user.infoCaller identity info retrieved (/v1/me)

Security events:

Event typeTrigger
auth.unauthenticatedRequest rejected with HTTP 401

MCP Registry discovery API (/registry/{name}/v0.1/)

Event typeTrigger
server.listServer list requested
server.versions.listServer version list requested
server.version.readSpecific server version retrieved

Skills extension API (/x/dev.toolhive/skills)

Event typeTrigger
skill.listSkill list requested
skill.readSkill retrieved
skill.versions.listSkill version list requested
skill.version.readSpecific skill version retrieved

Filter events

By default, every event type is logged. Use eventTypes and excludeEventTypes to control what gets captured.

Log only write operations and auth failures:

audit:
enabled: true
eventTypes:
- source.create
- source.update
- source.delete
- registry.create
- registry.update
- registry.delete
- entry.publish
- entry.delete
- entry.claims.update
- auth.unauthenticated

Log everything except high-frequency read events:

audit:
enabled: true
excludeEventTypes:
- source.list
- registry.list
- server.list
- skill.list
- user.info
info

When both eventTypes and excludeEventTypes are set, excludeEventTypes takes precedence. Events in the exclusion list are never logged, even if they appear in eventTypes.

Request body capture

By default, audit events record metadata about API operations but not the request payload. To capture request bodies for forensic analysis:

audit:
enabled: true
includeRequestData: true
maxDataSize: 4096 # bytes; default 1024, hard limit 1 MB

Request body capture applies only to mutating operations (POST, PUT, DELETE). Bodies larger than maxDataSize are truncated before logging.

warning

Request bodies may contain sensitive data. Review your organization's data handling policies before enabling request body capture in production.

Audit log format

Each audit event is a single-line JSON object:

{
"time": "2025-06-23T10:15:30.123456789Z",
"level": "AUDIT",
"msg": "audit_event",
"audit_id": "a3f2b8d1-4c5e-6789-abcd-ef0123456789",
"type": "source.delete",
"logged_at": "2025-06-23T10:15:30.123456Z",
"outcome": "success",
"component": "toolhive-registry-api",
"source": {
"type": "network",
"value": "10.0.1.50:54321",
"extra": {
"user_agent": "kubectl/1.30",
"x_forwarded_for": "203.0.113.42"
}
},
"subjects": {
"sub": "alice|12345",
"user": "alice@example.com",
"role": "super_admin"
},
"target": {
"method": "DELETE",
"path": "/v1/sources/my-source",
"resource_type": "source",
"resource_name": "my-source"
},
"metadata": {
"extra": {
"request_id": "req-abc-123",
"duration_ms": 12,
"response_bytes": 0
}
}
}

Field descriptions

FieldDescription
timeTimestamp when the log line was written
levelLog level (AUDIT for audit events)
msgAlways audit_event for audit log entries
audit_idUnique identifier for this event
typeEvent type (see Audit event types)
logged_atUTC timestamp when the event was recorded
outcomesuccess, failure, denied, or error
componentAlways toolhive-registry-api
source.valueDirect peer address (the connecting IP, not the X-Forwarded-For header value)
source.extrauser_agent and x_forwarded_for when present
subjectsCaller identity from the JWT (sub, user, optionally role)
targetAlways includes method and path; resource_type, resource_name, registry_name, entry_type, version, and namespace are present when applicable.
metadata.extrarequest_id, duration_ms, response_bytes
dataRequest body, present only when includeRequestData: true

resource_name values are percent-encoded when the name contains slashes (for example, "com.example%2Fmy-server").

Unauthenticated requests

When a request fails authentication, subjects contains {"identity": "unknown"} and target contains only the HTTP method and path. The metadata.extra block contains only request_idduration_ms and response_bytes are absent because the event fires before the response is written:

{
"subjects": { "identity": "unknown" },
"target": { "method": "GET", "path": "/registry/default/v0.1/servers" },
"metadata": { "extra": { "request_id": "pod-name/abc-000001" } }
}
Audit log level

Audit events use a custom AUDIT log level that sits between INFO and WARN. Because AUDIT is not a standard level name, some log aggregators may display it as unknown. To filter for audit events, match on level = "AUDIT" or msg = "audit_event" directly rather than relying on level detection.

Known issue: level field may show as INFO+2

In some versions of the Registry Server, the level field is serialized as "INFO+2" rather than "AUDIT" (tracked in #826). If your logs show "INFO+2", filter on "INFO+2" or "audit_event" instead.

Configure output destination

Stdout (default)

Without logFile set, audit events go to stdout alongside application logs. Use the level and msg fields to distinguish them from regular application logs. This works well with any Kubernetes log collection stack (Fluent Bit, Grafana Alloy, the OTel Collector):

audit:
enabled: true
# logFile not set = stdout

Log to a file

To write audit logs to a dedicated file, set logFile and ensure the parent directory exists and is writable. In Kubernetes, mount a persistent volume for the target path:

values.yaml
config:
audit:
enabled: true
logFile: /var/log/toolhive/audit.log

extraVolumes:
- name: audit-logs
persistentVolumeClaim:
claimName: registry-audit-pvc

extraVolumeMounts:
- name: audit-logs
mountPath: /var/log/toolhive

Audit log files are created with mode 0600.

info

For most deployments, stdout is simpler and integrates better with Kubernetes log collection. Use file-based logging only if your compliance requirements mandate a separate audit trail or your log collector cannot distinguish audit from application traffic at the log-level field.

Querying audit logs

With stdout logging, filter audit events from application logs using jq:

# All audit events
kubectl logs -n <NAMESPACE> deployment/toolhive-registry-api \
| jq 'select(.msg == "audit_event")'

# Write operations only
kubectl logs -n <NAMESPACE> deployment/toolhive-registry-api \
| jq 'select(.msg == "audit_event" and (.type | test("create|update|delete|publish")))'

# Auth failures
kubectl logs -n <NAMESPACE> deployment/toolhive-registry-api \
| jq 'select(.type == "auth.unauthenticated")'

# Activity from a specific user
kubectl logs -n <NAMESPACE> deployment/toolhive-registry-api \
| jq 'select(.msg == "audit_event" and .subjects.user == "alice@example.com")'

Next steps