Files
Jungfraujoch/docs/SECURITY.md
T
leonarski_fandFilip Leonarski 19b135186d
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
v1.0.0-rc.175 (#85)
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`).
* Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection.
* Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS).
* Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead.
* Rugnux: On macOS, Bragg integration runs on the Apple GPU.
* Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand.
* Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`.
* Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit.
* Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed.
* Rugnux: The beam-stop shadow is found correctly on tilted detectors.
* Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement.
* jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps.
* jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection.
* jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images.
* Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges.

---------

Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home>
Reviewed-on: #85
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-16 15:34:43 +02:00

9.2 KiB

Security

Jungfraujoch is a data-acquisition and analysis system for X-ray detectors, designed to run inside a controlled facility network. This document describes what the software does and does not protect against, the current known limitations, and the authentication work in progress.

Threat model and scope

The security model targets a semi-trusted internal facility network. The concern is a peer on that network reaching a Jungfraujoch service with little or no effort — a mistyped host/port, a curious colleague, a mis-pointed script, a stray browser tab — not a determined attacker and not passive wire capture (which is the responsibility of the network layer: 802.1x, VLANs, facility infrastructure).

The asset that matters most is the confidentiality of live analysis data: the diffraction images and derived metadata (unit cell, resolution, spot counts, sample name) that reveal which sample is being measured. This matters for industrial and proprietary experiments. By contrast, acquisition control (start / stop / configure) is treated as low risk — scientists operate their own experiments and there is little to gain from restricting it.

Security is best-effort: measures that materially impede normal operation get turned off, so the design favours a few high-value, low-friction controls over comprehensive lockdown.

Out of scope. Jungfraujoch is not designed to be exposed to an untrusted network or the public internet. Do not do this.

1. Good practice — what is and is not protected

What you can secure (and should)

These controls work and a deployment should apply them (see also DEPLOYMENT.md):

  • Network isolation. Keep the broker and its data streams on a controlled segment. The broker ↔ writer ↔ receiver traffic should run on a dedicated back-end network, with the data-socket addresses pinned to that interface and the ports firewalled.
  • Reverse proxy for TLS. The broker speaks plain HTTP. To get HTTPS, put a reverse proxy (Apache / nginx) in front that terminates TLS and pin the broker to localhost behind it. The desktop viewer supports https:// endpoints — choose the scheme in the Open HTTP Connection dialog.
  • Filesystem confinement of written data. The writer creates NXmx HDF5 files on shared storage. Confidentiality of that data at rest is enforced by the filesystem: run the writer under a dedicated identity and use directory ownership / ACLs (and setgid) so that only the owning experiment can read its files.
  • Firewall the ZeroMQ ports. The image / preview / metadata / republish streams have no access control of their own (see below), so restrict who can reach those ports at the network layer.

What the software does NOT provide

The gaps are listed explicitly so a deployment does not assume protection that is not there:

  • No authentication or authorization in the broker. The HTTP/REST API currently has no login, token, or access control. Anyone who can reach the broker's host and port has full read access (live images, unit cell, resolution, sample metadata) and full write access (start, cancel, reconfigure). Confidentiality currently depends entirely on network/firewall isolation. This is being addressed — see §3.
  • No transport encryption in the broker. The broker serves plain HTTP; there is no built-in TLS. Encryption must be provided by a reverse proxy.
  • No access control or encryption on the ZeroMQ streams. The preview, metadata, image, and republish streams are unauthenticated sockets. Any peer that can connect can subscribe to live data. For the image PUSH stream specifically, an accidental extra consumer does not merely eavesdrop — a PULL peer is load-balanced into the stream and will divert images away from the real writer.
  • No per-user isolation. The broker has no concept of users; it cannot separate one operator's access from another's.
  • No application-level audit trail of who accessed or changed what.
  • Broker and back-end streams on an isolated network; never exposed to a general/untrusted network.
  • ZeroMQ data-socket addresses pinned to the back-end interface; ports firewalled to known peers.
  • TLS terminated by a reverse proxy; broker bound to localhost behind it.
  • Writer run under a dedicated identity; data directories owned / ACL'd per experiment (setgid) so users read only their own data.
  • ZeroMQ compatibility streams (preview / metadata / republish) enabled only if actually consumed, and only on the trusted back-end.

2. Known issues

# Issue Impact Mitigation today
1 Broker HTTP API has no authentication for control, and read access is protected only per dataset Anyone who can reach it can start / stop / configure; a dataset started without tokens is readable by anyone Per-dataset bearer tokens (§3) for the read endpoints; network / firewall isolation for the rest
2 Broker binds all interfaces, plain HTTP Reachable from anywhere routable; no encryption Expose only on the trusted segment; TLS via reverse proxy
3 ZeroMQ preview / metadata / image / republish streams are unauthenticated and unencrypted Live-data exfiltration; a rogue PULL on the image stream diverts/steals images Firewall the ports; run only on the back-end network
4 Web frontend has no login The bundled UI takes a dataset token (key button) but nothing identifies its user Serve and reach it only on the trusted network

Input robustness. Services parse framed data from peers on the (trusted) data path. Hardening of untrusted-frame handling (size caps, overflow guards) is ongoing; these paths are not intended to face an untrusted network.

3. Authenticated read access — per-dataset bearer tokens

The confidential read endpoints are protected with a best-effort, low-friction scheme; acquisition control stays open.

How it works. /start accepts an optional list of tokens - plain strings, any number, all equivalent. A typical pair is one constant beamline secret and one secret minted for the experiment, so both the beamline staff and the experiment's own users can open the data. While the current dataset has tokens, the endpoints below answer 401 unless the request carries Authorization: Bearer <one of them>; the 401 says nothing about the dataset. Every accepted /start replaces the previous tokens, so a run started without them is open, and the next run's users cannot read this one. No endpoint returns the tokens; the broker only compares strings (constant-time) and keeps them in memory. Whoever runs /start hands the token to the viewers. An accepted /start also clears what the previous run left readable - its statistics, plots and buffered images - in the same step that installs the new tokens, so the previous run is never served under the new tokens, and the new run's name never under the old ones. A refused /start (wrong state, invalid settings) changes nothing.

Endpoint With tokens set
/statistics/data_collection (dataset name, unit cell, ...) 401 without a token
/result/scan (dataset name, cell of a grid scan / rotation) 401 without a token
/image_buffer/start.cbor, /image_buffer/image.cbor, /image_buffer/image.jpeg, /image_buffer/image.tiff (the images and the start message) 401 without a token
/preview/plot, /preview/plot.bin (per-image plots, unit cell) 401 without a token
/live/events (the start message and the data files of the collection being written) 401 without a token
/statistics (the aggregate the web UI polls) 200, but the measurement block is omitted
everything else (/status, /config/*, /start, /cancel, masks, pedestal, ...) open

Clients.

  • jfjoch_viewer - the token field of File ▸ Open HTTP (password echo), the JUNGFRAUJOCH_HTTP_TOKEN environment variable, or D-Bus (LoadFile(url, image, sum, token) / SetHttpToken(token)); the dialog overrides both. A 401 clears the display and puts a note on the status bar - no dialog, since a changed dataset is the normal reason.
  • Web frontend - the key button in the top bar; the token lives in the tab's sessionStorage and is sent with the protected calls only. The start form has a field for the tokens of a new run.
  • Python client - Configuration(host=..., access_token="<token>").
  • rugnux following a collection from the broker - the JUNGFRAUJOCH_HTTP_TOKEN environment variable, sent only in the Authorization header and never written to its logs or reports.
  • Anything else - curl -H "Authorization: Bearer <token>" ....

What it does not do. The broker still speaks plain HTTP, so the token crosses the network in clear unless a TLS reverse proxy fronts the broker (§1); on a facility network this raises the bar from "type the IP" to "capture packets", which is the aim. It does not authenticate users or control, does not touch the ZeroMQ streams (issue #3), and /status's free-text message may still quote a path. Datasets of different users within one session are separated by their tokens alone; there is no long-lived login.