Files
Jungfraujoch/docs/SECURITY.md
T
leonarski_f afbe844b20
Build Packages / Create release (push) Successful in 17s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m41s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m49s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 13m6s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 23m44s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m28s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m50s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 14m9s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 16m25s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 15m42s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 20m50s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 20m36s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 22m18s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m29s
Build Packages / Generate python client (push) Successful in 30s
Build Packages / Build documentation (push) Successful in 1m10s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 21m38s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 17m8s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 23m10s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 19m16s
Build Packages / Unit tests (push) Successful in 2h8m4s
Build Packages / Validation Battery (push) Successful in 40m53s
v1.0.0-rc.178 (#88)
* Rugnux: Processes serial crystallography data from XFELs - SACLA (MPCCD), PAL-XFEL and European XFEL (CXI), LCLS (XTC) and SwissFEL Alvra (JUNGFRAU with a CrystFEL module geometry) - with several files or Jungfraujoch master files merged in one run (`rugnux *_master.h5`).
* Rugnux: Stills processing takes a known unit cell and space group as a starting point, finds the beam centre (and, with a cell, the detector distance) from the images, detects the saturation level in other facilities' files and refines the placement of individual detector panels.
* Rugnux: The stills report gives R_split and CC1/2 from frame half-sets, comparable with CrystFEL.
* jfjoch_viewer: Opens the same XFEL file formats.
* Validation: A stills battery checks Rugnux against deposited structures from SACLA, PAL-XFEL, LCLS, European XFEL and SwissFEL.
* Rugnux: More reliable convergence of rotation scaling, and live molecular replacement starts as soon as the model is uploaded.
* jfjoch_broker: Initialize clears the previous dataset's bearer tokens, image buffer and live Rugnux status, and a slow CUDA JIT cache is reported at start.

Reviewed-on: #88
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-21 11:20:01 +02:00

138 lines
9.5 KiB
Markdown

# 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.
### Recommended deployment checklist
- [ ] 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. `/initialize` drops the tokens together with
everything they protected - the dataset's settings, results, images, plots, live rugnux status and
the record of its collection - so the broker is left as open and as empty as a freshly started one.
| 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 |
| `/rugnux/status` (live rugnux run of the collection: cell, space group, results) | 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.