csaf-crud 1.5.26

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
<!-- SPDX-License-Identifier: Apache-2.0 -->
<!-- SPDX-FileCopyrightText: 2026 ndaal GmbH & Co KG, Cologne -->

# Administrator Guide

Deployment, configuration, operations, and recovery for ndaal
CSAF-CRUD. For exhaustive key/flag descriptions see
[REFERENCE.md](../REFERENCE.md); for end-user workflows see the
[User Guide](User_Guide.md); for failure diagnosis see the
[Troubleshooting Guide](Troubleshooting_Guide.md).

## Overview

ndaal CSAF-CRUD is a single self-contained binary. It embeds two
databases (redb for CSAF documents, bundled SQLite for the audit log
and the user schema), generates its own TLS 1.3 certificate, and
needs no external services.

Two binaries ship from the workspace:

- `csaf-crud` — the web UI and JSON API server.
- `ndaal-csaf-cli` — a headless CLI for import, export, validate,
  and stats.

## Platform support

- Minimum Rust toolchain: `1.95.0` (`rust-version` in `Cargo.toml`),
  edition `2024`.
- Pinned Rust toolchain: `rust-toolchain.toml` pins Rust `1.99.0`, the
  exact version used to develop and release. It also lists the six
  release targets, so `rustup` installs the pin and the targets
  automatically in a checkout. The pin is newer than the minimum on
  purpose; the two are not meant to match. See the
  [Developer Guide]Developer_Guide.md#toolchain-pin-and-minimum-version.
- Packaged targets: `x86_64-unknown-linux-gnu`,
  `aarch64-unknown-linux-gnu`, `x86_64-pc-windows-gnu`,
  `x86_64-apple-darwin`, `aarch64-apple-darwin`,
  `aarch64-pc-windows-msvc`.
- Cross-compilation: the Linux targets and `x86_64-pc-windows-gnu` use
  cargo-zigbuild. Both Apple targets build with the native Apple
  toolchain, and `aarch64-pc-windows-msvc` builds with cargo-xwin. See
  [Release pipeline]Developer_Guide.md#release-pipeline.

### Platform notes

- **Windows binaries have no Control Flow Guard.** Neither Windows
  binary (`x86_64-pc-windows-gnu`, `aarch64-pc-windows-msvc`) sets the
  `GUARD_CF` flag in its PE header. ASLR, high-entropy ASLR and DEP
  (NX) are set.
- **`aarch64-pc-windows-msvc` uses the system allocator.** It is built
  without mimalloc because the cargo-xwin sysroot has no MSVC C++
  standard library. Every other target, including
  `x86_64-pc-windows-gnu`, includes mimalloc in secure mode.
- **HTTP/3 on Windows ARM64 needs 1.5.24 or later.** Up to and
  including 1.5.23, the `aarch64-pc-windows-msvc` `csaf-crud` was built
  without the `quic` feature. That build serves HTTP/1.1 and HTTP/2
  only and opens no UDP listener on `listen_port + 1`. From 1.5.24 it
  includes HTTP/3, but the 1.5.24 changelog records that this build
  has not been run on Windows ARM64 hardware.

## Installation

Build from source in the workspace root:

```bash
cargo build --release
# binaries: target/release/csaf-crud and target/release/ndaal-csaf-cli
```

To build without the optional OASIS validator and QUIC features
(smaller binary, fewer dependencies):

```bash
cargo build --release --no-default-features
```

## Configuration

The server reads a TOML file at startup. The path is the first
command-line argument and defaults to `config/default.toml`.

```bash
csaf-crud /etc/csaf-crud/config.toml
```

The shipped `config/default.toml`:

```toml
listen_addr = "127.0.0.1"
listen_port = 8180
data_dir    = "./data"
# tls_cert = "config/cert.pem"
# tls_key  = "config/key.pem"
```

Keys (full descriptions and the compiled-in fallbacks are in
[REFERENCE.md](../REFERENCE.md#configuration)):

- `listen_addr` — bind address. Default `127.0.0.1`. Set to `::` or
  a routable address to expose the server beyond loopback (read the
  security note below first).
- `listen_port` — TCP port for HTTP/1.1 and HTTP/2. The bundled
  config uses `8180`; the QUIC/HTTP3 listener binds `listen_port + 1`
  (UDP, `8181`).
- `data_dir` — directory holding `csaf.redb` and `csaf.sqlite`.
  Created on first run if absent.
- `tls_cert` / `tls_key` — optional PEM paths. When unset, the
  server generates a self-signed certificate at startup.

Operational settings (export directories, sidecar toggles,
publisher defaults, classification policy, CSAF mode) are **not** in
the TOML file. They live in redb and are edited at runtime through
the `/settings` page or `PUT /api/v1/settings`, and reset with
`POST /settings/reset`. See [REFERENCE.md](../REFERENCE.md#runtime-settings).

## TLS

- Transport is **TLS 1.3 only** (no TLS 1.2 fallback), via rustls
  with the ring crypto provider — no OpenSSL or aws-lc.
- When no certificate is configured, the server issues a self-signed
  certificate valid for **45 days** with SANs for `localhost`,
  `127.0.0.1`, and `::1`. It is regenerated on every restart.
- For a stable certificate, set `tls_cert` and `tls_key` to PEM
  files. Rotate them before expiry.
- Verify the deployed endpoints with the bundled harness
  `test/testssl/run.sh`.

## Security headers

Every response — HTML pages, static files, the JSON API, redirects, and
`404` and `405` errors — carries the same six security headers. They are
added in one place (`SharedRouter::handle_parts` in
`crates/csaf-crud/src/router.rs`), which both the TCP/TLS listener
(HTTP/1.1 and HTTP/2) and the QUIC listener (HTTP/3) go through, so no
response skips them. A header that a handler sets itself is kept.

| Header | Value |
| --- | --- |
| `Content-Security-Policy` | `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'` |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` |
| `X-Content-Type-Options` | `nosniff` |
| `X-Frame-Options` | `DENY` |
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
| `Permissions-Policy` | `camera=(), microphone=(), geolocation=(), payment=()` |

What the policy means for you:

- **Nothing is inline.** The CSP has no `'unsafe-inline'`, so the pages
  load one stylesheet set and one script from `/static/` and use no
  `<style>`, `<script>`, `style=` attribute, or `on…=` handler. Fonts and
  icons are bundled; a page makes no request to another host.
- **A reverse proxy must not replace these headers** with weaker ones. If
  it adds its own `Content-Security-Policy`, the browser enforces the
  stricter result of both, which can block the interface.
- **No framing.** `frame-ancestors 'none'` and `X-Frame-Options: DENY`
  stop the interface being embedded in another site (clickjacking).
- **The `Referer` still works for the language and theme redirects.**
  `strict-origin-when-cross-origin` sends the full address to the same
  origin, which `/lang/{code}` and `/preferences/theme` use to return you
  to the page you were on.

### HSTS and the default certificate

`Strict-Transport-Security` applies to the whole host name for a year,
on every port, and to its subdomains. Browsers ignore the header on a
connection with a certificate error, so the self-signed certificate the
server issues by default never activates it. It takes effect once you
configure a certificate your browsers trust (see "TLS"). Do that only
when every service on that host name, and under it, serves HTTPS, because
the browser will refuse plain HTTP for a year. `preload` is a signal for
browser preload lists and has no effect until you submit the domain.

### Verify the headers

```bash
# All six headers on a page, a static file, a 404 and a 405.
bash tests/scripts/test_security_headers.sh \
  --base-url https://127.0.0.1:8180 --insecure-loopback

# The same through the Bruno collection.
test/bruno/run.sh development
```

The gate fails if a header is missing or differs, if the CSP contains
`unsafe-inline` or `unsafe-eval`, or if the server cannot be reached.
`--insecure-loopback` skips certificate verification for `127.0.0.1`,
`localhost` and `[::1]` only; against any other host pass `--cacert`.
For an independent check, run `testssl.sh --headers` and the OWASP ZAP
baseline scan described in "Verification".

## Authentication and access control

This release enforces **no** authentication or authorization. Every
web page and API route is reachable by anyone who can connect. The
`users` and `sessions` tables exist in SQLite (Argon2id hashing,
per-user API keys) but are not yet enforced by the router.

Until authentication lands, operate the service accordingly:

- Keep `listen_addr = "127.0.0.1"` and reach it through an
  authenticating reverse proxy or an SSH tunnel.
- Never expose it directly to an untrusted network.
- Restrict filesystem access to `data_dir` and the export/dump
  directories — they hold every advisory and the audit log.

## Data directories

Created on first run relative to the working directory unless
reconfigured in settings:

| Directory | Default | Purpose |
| --- | --- | --- |
| `data/` | `data_dir` | `csaf.redb` + `csaf.sqlite` |
| `data_import/` | `import_directory` | Inbox scanned by import |
| `data_export/` | `export_directory` | Exported CSAF + sidecars |
| `data_dump/` | `dump_directory` | Database snapshots |
| `data_log/` | `log_directory` | Rolling tracing logs |

## Running as a service

Example systemd unit (Linux):

```ini
[Unit]
Description=ndaal CSAF-CRUD
After=network.target

[Service]
ExecStart=/usr/local/bin/csaf-crud /etc/csaf-crud/config.toml
WorkingDirectory=/var/lib/csaf-crud
User=csaf
Environment=RUST_LOG=info
Restart=on-failure

[Install]
WantedBy=multi-user.target
```

Place the data directories under `WorkingDirectory` (or set absolute
paths in settings) so the service user owns them.

## Logging

- Logs are written to stderr and to a daily-rotated file
  `<log_directory>/csaf-crud.log.<YYYY-MM-DD>`.
- The level is controlled by `RUST_LOG` (default `info`). Use
  `RUST_LOG=debug` for verbose diagnosis; library noise from
  `hyper`, `quinn`, and `h2` is filtered at `info`.
- If the log directory cannot be created, the server logs a warning
  to stderr and continues with stderr-only logging.

## Audit log

Every create, update, delete, import, export, dump, and
settings-reset is recorded in the SQLite `audit_log` table. Export it
from `/admin/export` → Export Audit Log, or
`POST /api/v1/.../export-audit`, in Markdown, CSV, JSON, or
SARIF 2.1.0. Read it via `GET /api/v1/audit-log`.

## Backup and recovery

### Backup

Trigger a consistent snapshot from `/admin` → Dump Database (or
`POST /admin/dump`). It writes timestamped copies of both databases
plus the enabled hash sidecars to `dump_directory`:

```text
csaf.redb.<timestamp>      + .sha-256 / .sha-512 / .sha3-512 / …
csaf.sqlite.<timestamp>    + .sha-256 / .sha-512 / .sha3-512 / …
```

The redb snapshot is taken under a read transaction; the SQLite
snapshot uses the online backup API — both are consistent against
live traffic. Copy the `dump_directory` contents to off-host
storage on a schedule.

### Recovery

1. Stop the server.
2. Verify the snapshot integrity against its sidecar, for example
   `shasum -a 512 -c csaf.sqlite.<timestamp>.sha-512`.
3. Copy `csaf.redb.<timestamp>` to `<data_dir>/csaf.redb` and
   `csaf.sqlite.<timestamp>` to `<data_dir>/csaf.sqlite`. Remove any
   stale `csaf.sqlite-wal` / `csaf.sqlite-shm` files.
4. Start the server; it opens the restored databases normally.

## Upgrades

1. Back up (dump) the databases first.
2. Replace the binary and restart.
3. SQLite migrations run automatically on open. redb and SQLite
   schemas are forward-compatible within a minor series.

### In-place self-update

Both binaries can replace their own executable from the project's GitLab
releases. The capability is compiled into **every** build — it is never
behind a cargo feature — so there is exactly one binary shape to deploy,
and it is disabled by policy at runtime rather than by shipping a
different build.

```bash
# Is a newer release published?  Never touches the running binary.
csaf-crud --check-update
ndaal-csaf-cli --check-update

# Download, verify against SHA256SUMS, and replace the running binary.
csaf-crud --self-update
```

Disable it on a package-managed or locked-down host:

```bash
csaf-crud --self-update --no-self-update       # exit 3, nothing changed
export CSAF_NO_SELF_UPDATE=1                   # same, for the whole host
```

`CSAF_NO_SELF_UPDATE` accepts `1`, `true`, `yes` or `on`
(case-insensitive); anything else, including an empty value, is treated
as "not set". An explicit `--no-self-update` on the command line always
wins over the environment. The policy gate is evaluated **before** any
network request is made, so a refused update produces no outbound
traffic.

| Exit | Meaning |
| --- | --- |
| `0` | Up to date, update installed, host unreachable, `--help`, `--version` |
| `1` | `--self-update` failed: no asset for this triple, checksum mismatch, or install error |
| `2` | Argument parse error |
| `3` | `--self-update` refused by policy |
| `10` | `--check-update` found a newer release |

`--check-update` is safe to put in a boot path, a health check or a cron
job: it **never** fails on a network problem. An unreachable host, a
malformed API response, an empty release list and a rate-limit HTTP 403
all report `Unreachable` and exit `0`. Only exit `10` means "a newer
release exists", which is what lets a wrapper branch on it without
parsing text:

```bash
if csaf-crud --check-update; then
    logger "csaf-crud is current"
else
    [ $? -eq 10 ] && logger "csaf-crud update available"
fi
```

When the server binary updates itself, the new executable takes effect on
the **next restart** — an already-running process keeps executing the
image it started with. Under systemd, follow `--self-update` with
`systemctl restart`.

#### What the verification does and does not prove

The downloaded archive is checked against the `SHA256SUMS` manifest
published in the same release. That proves the artifact matches the
manifest and was neither corrupted in transit nor swapped for another
asset within the release.

It is **not a signature**. Anyone able to rewrite the release archive at
the source can rewrite `SHA256SUMS` alongside it. This is **integrity,
not authenticity**, and the artifacts are **not signed** — detached
signing is deliberately not enabled rather than half-implemented, because
a partial signing story invites false confidence.

Note also that GitLab's releases API publishes no per-asset digest, so
the updater's automatic release-digest check is a no-op on this backend;
the `SHA256SUMS` manifest is the only integrity check that actually runs.

Operators who need authenticity should disable self-update
(`CSAF_NO_SELF_UPDATE=1`), install from the release bundle by hand, and
verify the file against the five aggregate manifests (`SHA-256SUMS.txt`,
`SHA-512SUMS.txt`, `SHA3-512SUMS.txt`, `BLAKE3-512SUMS.txt`,
`SHAKE256-512SUMS.txt`) out of band. See
[Verify a download](#verify-a-download).

### What a release contains

A GitLab release for version 1.5.21 or later carries 66 assets:

- 12 archives `<binary>-v<version>-<triple>.tar.gz`: two binaries
  (`csaf-crud`, `ndaal-csaf-cli`) for six triples.
- 8 Linux packages: `.deb` (amd64, arm64) and `.rpm` (x86_64, aarch64)
  for each binary.
- 40 checksum sidecars: five per package (`.sha-256`, `.sha-512`,
  `.sha3-512`, `.blake3-512`, `.shake256-512`).
- 5 aggregate manifests, `SHA-256SUMS.txt`, `SHA-512SUMS.txt`,
  `SHA3-512SUMS.txt`, `BLAKE3-512SUMS.txt` and `SHAKE256-512SUMS.txt`.
  Each lists every archive and package.
- 1 manifest, `SHA256SUMS`, which `--self-update` reads.

The archives have **no** per-file checksum sidecars on the release page.
The release script still writes them next to the archives on the build
host, but they are not uploaded. Releases up to 1.5.20 attached them, and
that broke `--self-update`: the updater picks the first asset whose name
contains the target triple and the binary name, and an archive's name is
a prefix of the names of its sidecars. It therefore downloaded a sidecar
instead of the archive and failed with a checksum mismatch. The failure
dated from 1.4.18.

If a script fetches a per-archive sidecar such as
`<archive>.tar.gz.sha-256` from the release page, switch it to the
aggregate manifests. The sidecars of the `.deb` and `.rpm` packages are
still published.

### Verify a download

Check an archive or package that you downloaded against the checksum
manifest published in the same release.

Prerequisites:

- The downloaded file and the manifest in the same directory.
- `shasum` (macOS) or `sha256sum` (Linux).

Procedure:

1. Download the file and `SHA256SUMS` from the release into one
   directory.

2. Check the file against its line in the manifest. Replace the file
   name with the name of the file you downloaded:

   ```bash
   grep ' csaf-crud-v1.5.23-aarch64-apple-darwin.tar.gz$' SHA256SUMS \
     | shasum -a 256 -c -
   ```

   `grep` keeps only the line for that file, so the check does not
   complain about the other files in the manifest. On Linux, use
   `sha256sum -c -` in place of `shasum -a 256 -c -`.

3. For a second digest, check the SHA-512 manifest the same way. On
   Linux, use `sha512sum -c -`:

   ```bash
   grep ' csaf-crud-v1.5.23-aarch64-apple-darwin.tar.gz$' SHA-512SUMS.txt \
     | shasum -a 512 -c -
   ```

Each check prints one line and exits with status `0` when the file
matches:

```text
csaf-crud-v1.5.23-aarch64-apple-darwin.tar.gz: OK
```

If the line ends in `FAILED`, the file differs from the manifest.
Discard it and download it again. If `shasum` reports `no properly
formatted SHA checksum lines found`, `grep` matched nothing, so compare
the file name in your command with the name in the manifest.

The other three manifests list the same files, but `shasum` cannot check
them. Recompute the digest and compare it with the manifest line. For
`BLAKE3-512SUMS.txt` use `b3sum --length 64`; for
`SHAKE256-512SUMS.txt` use `hashlib.shake_256(data).hexdigest(64)` in
Python. `SHA3-512SUMS.txt` keeps the native layout of `sha3sum`: the
digest, a non-breaking space, `*`, then the file name. The
`grep ' <name>$'` pattern above finds no line in it, so select the line
with `grep -F` and compare the digest with the output of
`sha3sum -a 512`.

Like the self-update check, this proves integrity, not authenticity: the
manifests come from the same release as the files.

## Verification

After deployment, confirm the service is healthy:

```bash
curl -k https://127.0.0.1:8180/api/v1/system/health
# {"data":{"status":"healthy", ...}, "_links": {...}}
```

`-k` is required while the self-signed certificate is in use.