Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Audit trail (IHE ATNA)

FerroEHR keeps a full security audit trail of API access — who did what to which resource, with what outcome, from where, and when — following the IHE ATNA (Audit Trail and Node Authentication) profile, the standard openEHR itself points at (the platform Service Model names the System Log component “IHE ATNA-compliant system log”). It is on by default: every deployment records a queryable audit trail with zero external dependencies.

The trail is orthogonal to openEHR’s own CONTRIBUTION/AUDIT_DETAILS change-control audit (which the server always writes in the same transaction as every version change): openEHR audit records what a version says about its own authorship; ATNA records security surveillance of access, including reads and rejected attempts.

The record, in both official formats

Every audited operation produces one record, rendered in the two formats the IHE standards define:

  • FHIR R4 AuditEvent following the IHE BALP (Basic Audit Log Patterns) content profiles — the modern RESTful-ATNA form and the canonical stored form. Patient-centric operations carry the resolved EHR subject as the patient entity (PatientRead/PatientCreate/… profile claims); query executions carry the search expression; Bearer-authenticated requests record the token’s jti (and never the token itself) per OAUTHaccessTokenUse.Minimal.
  • DICOM Audit Message (DICOM PS3.15 §A.5 XML) — the classic ATNA form, shipped over syslog per IHE ITI-20 when the syslog sink is enabled. Dedicated DICOM event ids are used throughout: Patient Record (110110), Query (110112), Export/Import (110106/110107, the EHR-Extract directions), User Authentication (110114, with the Login EventTypeCode 110122).

Every server operation is audited (unrecognised extension operations fail closed to a generic audited class — nothing is silently unaudited), and authentication rejections (401/403) are always recorded, attributed to the caller where one authenticated.

Sinks

Records fan out to independently configured sinks ([audit] in ferroehr.toml — see the configuration reference):

SinkDefaultWhat it does
[audit.store]onThe local Audit Record Repository: records persist in the dedicated audit PostgreSQL schema (append-only, strictly outside the EHR content), served back via the ITI-81 search below. retention_days prunes old records hourly (0 = keep forever).
[audit.syslog]offThe classic ATNA feed: DICOM PS3.15 XML over syslog (RFC 5424; UDP or TLS transport) to an external ARR, per IHE ITI-20.
[audit.fhir_feed]offThe RESTful-ATNA feed (ITI-20 ATX: FHIR Feed): each FHIR AuditEvent is POSTed to an external FHIR ARR. With the local store on, delivery is outbox-driven: an ARR outage loses nothing, pending records ship on recovery.

The local store is the durability anchor. Under fail_mode = "closed", a store that stops accepting writes makes every subsequent auditable operation answer 503 Service Unavailable until a write succeeds again — no un-audited PHI access. (open, the default, drops-and-meters instead; every loss path is metered — see the atna_audit_* counters in Operations.)

Retrieving audit records (ITI-81)

The RESTful-ATNA ITI-81 Retrieve ATNA Audit Event transaction is served at the FHIR façade:

GET /ferroehr/rest/openehr/v1/fhir/r4/AuditEvent

It returns a FHIR searchset Bundle of the stored AuditEvent documents, newest first, with the full match total. Supported search parameters: date (ge/le-prefixed instants, repeatable), patient, agent (the principal), entity (the resource id), outcome (0/4/8/12), action (C/R/U/D/E), and _count/_offset paging; other FHIR search parameters are ignored (lenient search). The surface is admin-only under RBAC and answers 404 when the local store is disabled.

# Who accessed patient-42's data this month?
curl -u admin:pw \
  "https://cdr.example.org/ehrbase/rest/openehr/v1/fhir/r4/AuditEvent?patient=patient-42&date=ge2026-07-01T00:00:00Z"

This is also the openEHR “record demerging” instrument: when data lands in the wrong EHR, the patient-filtered audit search shows exactly who read it.

Node authentication (ITI-19, mutual TLS)

ATNA’s second half is node authentication. [server.tls] terminates TLS natively (protocol floor: TLS 1.2+, per IETF BCP 195) and can demand a verified client certificate:

[server.tls]
enabled = true
cert_file = "/etc/ferroehr/server.pem"
key_file = "/etc/ferroehr/server.key"
client_auth = "required"          # off | optional | required
client_ca_file = "/etc/ferroehr/client-ca.pem"

With client_auth = "required", only clients presenting a certificate chaining to your explicit trust anchor complete the handshake — the IHE mutually-authenticated-node posture. Deployments terminating TLS at an ingress keep [server.tls] off and enforce mTLS there instead. The separate-port management listener always stays plain HTTP (an internal surface). Complete the posture with time synchronisation (IHE Consistent Time): run NTP/chrony on every node so audit timestamps align across systems.

Configuration summary

Auditing defaults to on with the local store only; see the configuration reference for every [audit] key and its FERROEHR__AUDIT__* environment form. Deployments upgrading from the previous [atna] section: the server refuses the old keys at boot with did-you-mean guidance — move the settings under [audit.syslog] (host/port/transport/tls_ca_file/ tls_identity_cert_file/tls_identity_key_file) and the shared keys to [audit].