Skip to main content
Zunder records an authoritative audit trail of security-relevant activity performed through the FHIR API, modeled as FHIR AuditEvent resources.
Audit logs contain highly sensitive metadata (who accessed what, when, and why). Treat the audit_log table and the admin endpoints as privileged operational interfaces.

Key properties

PropertyBehavior
StorageSeparate audit_log table (not in resources)
MutabilityAppend-only and immutable (DB triggers prevent UPDATE/DELETE)
ReliabilityBest-effort: audit failures must not fail the main request
ThroughputAsync write path (queue + background insert)
Output formatFull serialized FHIR AuditEvent (R4/R5 depending on fhir.version)
AccessOnly via internal admin endpoints (not queryable via normal FHIR APIs)

What gets audited

Zunder audits FHIR REST interactions under /fhir including (when enabled):
  • read, vread, history
  • search
  • create, update, patch, delete
  • capabilities (GET /fhir/metadata)
  • $operation and $export
  • batch / transaction
Audit logging currently targets the FHIR API surface. Internal routes like /admin and /health are not audited by the same middleware.

Captured security context (SMART / OAuth2)

When requests are authenticated, Zunder binds the audit record to the SMART security context:
  • client_id (OAuth2 client)
  • user_id (end-user subject when present)
  • scopes (granted scopes)
  • token_type inferred as user / system / anonymous / unknown
This context is stored both as indexed columns (for fast filtering) and inside the JSON details.

Captured data access semantics

For each interaction, Zunder captures (best-effort):
  • Target resource: resource_type + resource_id when known
  • Patient subject: patient_id when resolvable
  • Request correlation: request_id (server-generated), client IP, user agent
  • Outcome:
    • success for HTTP < 400
    • authz_failure for 401/403
    • processing_failure for other >= 400 responses

Search events and entity.query

FHIR recommends that servers capture search activity as an Execute event and include the raw HTTP request as base64 in AuditEvent.entity.query. Zunder can do this for search requests:
  • Stores the full HTTP start line + headers + body as base64binary
  • Also records the “harmonized” request URL string (path + query) in details

OperationOutcome capture

On failures, Zunder can capture the returned OperationOutcome and include it in the serialized AuditEvent (contained + linked from entity.what).

Patient resolution

When possible, audit records include the patient subject:
  • From SMART context (patient claim on the token, if present)
  • From a compartment path (/fhir/Patient/{id}/...)
  • From direct resource targets (Patient/{id})
  • For successful searches: by inspecting a searchset Bundle and extracting referenced patients
For searches that touch multiple patients, Zunder can emit one audit record per patient to improve privacy segmentation. This is configurable.

Storage model (audit_log)

Audit records are stored in PostgreSQL in the audit_log table.
  • Structured columns for common filters (action, outcome, patient_id, request_id, …)
  • audit_event column stores the complete FHIR AuditEvent as JSONB
  • details stores additional structured metadata (SMART + HTTP context)
The table is created in the main DB migration (server/migrations/001_init.sql) and is protected with triggers to prevent modification.

Admin API

Audit logs are intentionally not accessible via the normal FHIR resource API. Instead, Zunder exposes an internal admin interface (behind admin auth middleware).

List audit events

GET /admin/audit/events Query parameters:
  • action: interaction (e.g. read, search, create, transaction)
  • outcome: success | authz_failure | processing_failure
  • resourceType, resourceId, patientId, clientId, userId, requestId
  • limit (default 100, max 1000), offset
Example:
curl "http://localhost:8080/admin/audit/events?outcome=authz_failure&limit=25"

Get a single audit event

GET /admin/audit/events/{id} Returns the full stored row including the serialized audit_event JSON.
curl "http://localhost:8080/admin/audit/events/123"

Configuration

Audit logging is configured under logging.audit:
logging:
  audit:
    enabled: true

    # Outcome filters
    include_success: true
    include_authz_failure: true
    include_processing_failure: true

    # Interaction filters
    interactions:
      read: true
      vread: true
      history: true
      search: true
      create: true
      update: true
      patch: true
      delete: true
      capabilities: true
      operation: true
      batch: true
      transaction: true
      export: true

    # Payload capture controls
    capture_search_query: true
    capture_operation_outcome: true
    per_patient_events_for_search: true
Common hardening examples:
# Only keep failures (common in high-volume deployments)
logging:
  audit:
    include_success: false

# Avoid storing raw HTTP bodies for searches
logging:
  audit:
    capture_search_query: false

Operational notes

  • Audit writes are queued and inserted asynchronously; on DB errors, the server logs warnings and continues serving requests.
  • The audit log table can grow quickly. Use standard Postgres operational strategies (partitioning, retention policies, backups) depending on your compliance needs.