Skip to content

feat: opt-in structured access-audit logging - #62

Open
alexluft wants to merge 2 commits into
mainfrom
feat/access-audit-log
Open

feat: opt-in structured access-audit logging#62
alexluft wants to merge 2 commits into
mainfrom
feat/access-audit-log

Conversation

@alexluft

@alexluft alexluft commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

What

Adds an opt-in access-audit log: telemetry.audit.enabled (default: false — zero behavior change unless enabled). When on, every HTTP request emits one self-contained JSON line on stdout:

{"audit":"http-access","ts":"2026-08-17T17:16:55Z","user":"alex@example.com",
 "subject":"5939ae08-…","source":"10.249.9.1","method":"GET",
 "path":"/aets/GEPACS/studies/1.2.840…","aet":"GEPACS","study":"1.2.840…",
 "status":200,"duration_ms":4886}

Motivation: in clinical deployments, "who accessed which study, when" is a legal requirement. Log pipelines (Loki, ELK, …) can select these lines by the audit discriminator and ship them to WORM storage.

Design

  • Identity comes from the reverse proxy. DICOM-RST performs no authentication itself (New Feature Request: Enhance Security by Providing Auth to endpoints #15, OpenID Connect Support #42), so the middleware reads X-Auth-Request-Email / X-Auth-Request-User — the headers oauth2-proxy (set_xauthrequest) and friends inject. Documented caveat: trustworthy only when the proxy is the sole ingress. Records are emitted even without identity — an anonymous access is itself audit-relevant.
  • Fail-open, never on the request path. Records go through a bounded mpsc channel (1024) to a stdout writer task. Full buffer ⇒ drop + rate-limited warn! with a running drop counter. A slow collector can never block or fail request handling; deployments with stricter needs alert on the drop warnings.
  • What is recorded: method, full path+query (QIDO match params are part of "which data"), the aet/study/series/instance path parameters via RawPathParams, first X-Forwarded-For hop, status, duration, user agent. Absent fields are omitted from the JSON.
  • The layer sits outside the timeout layer so timed-out requests are audited with their 408.
  • Timestamps via chrono (to_rfc3339_opts(SecondsFormat::Secs, true)), which was already a transitive dependency via dicom-core — now declared directly with default-features = false, features = ["clock"].

Also included

  • movescu: log the Study Instance UID on successful C-MOVE completion, so DIMSE-side retrievals are attributable in the same log stream.
  • with_ansi(is_terminal) on the fmt layer — container logs lose the escape codes, TTY output keeps colors.
  • docs/topics/configuration.md gained a telemetry.audit.enabled section including the trust-boundary note.

Testing

cargo fmt --check, cargo clippy --all-targets (0 warnings), cargo test (19/19, incl. new unit tests for the RFC 3339 formatter and record serialization). Running in our prod/dev clusters behind oauth2-proxy; sample line above is real (redacted).


⚠️ Please do not merge yet — pending internal review (@nickamzol).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q9PMSu4fFsV5dSNojTTQaG

Adds telemetry.audit.enabled (default: false). When enabled, every HTTP
request emits one JSON line on stdout recording who accessed what:
identity from the X-Auth-Request-Email/-User headers an authenticating
reverse proxy injects (DICOM-RST itself has no auth, see #15/#42),
source from X-Forwarded-For, plus method, full path+query, extracted
aet/study/series/instance path parameters, status and duration.

Delivery is fail-open by design: records flow through a bounded channel
to a stdout writer task; a full buffer drops the record and logs a
rate-limited warning instead of ever blocking request handling.

Also: log the Study Instance UID on C-MOVE completion so DIMSE-side
retrievals are attributable too, and disable ANSI colors when stdout is
not a terminal so container logs stay machine-parseable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9PMSu4fFsV5dSNojTTQaG
@feliwir

feliwir commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

I think it would make sense to integrate OIDC authorization first. So we could track the subject from the JWT.

Reading the user & mail from some HTTP header fields is some special case when deployed behind a reverse proxy that is not really the default setup.

@MaxOremek

Copy link
Copy Markdown

We have in Bonn developed a RUST based LDAP Audit Microservices with JWT with Access Logging to a PostGres. Would you be interested in recycling this?

@alexluft

Copy link
Copy Markdown
Collaborator Author

I'd absolutely upvote a native oidc support 👍
Until then, using oauth2 proxy is the only way to get dicom-rst integrated into sso environments.
Running web services behind reverse proxies with- or without an authentication layer is pretty common and best practice. Passing sub and more information from the reverse proxy for audit log (which is required legally) is a low hanging fruit.

Comment thread src/audit.rs Outdated
chrono is already in the dependency graph via dicom-core (with the
clock feature enabled), so the hand-rolled RFC 3339 formatter bought
nothing. Declared directly (default-features = false, clock only)
rather than leaning on the transitive edge. Output format unchanged:
2026-08-17T17:16:55Z.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9PMSu4fFsV5dSNojTTQaG
@alexluft
alexluft requested review from feliwir and nickamzol and removed request for feliwir August 18, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants