# 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
# quic_port = 8181 # UDP port of HTTP/3; unset means listen_port + 1; "off" disables it
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 for both the TCP listener and the UDP
(HTTP/3) listener. 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`. A number from 1025 to 65535; see
[Listen ports](#listen-ports).
- `quic_port` — UDP port of the HTTP/3 (QUIC) listener. Unset, it is
`listen_port + 1` (`8181` with the bundled config). A number from
1025 to 65535, or `"off"` (any case) or `0` for no HTTP/3 listener.
- `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).
### Listen ports
**A listen port is accepted iff it is a canonical decimal number in
1025..=65535.** The rule covers `listen_port` and `quic_port`. If a value
breaks it, the server stops before it opens a database, prints a message
that names the key and ends with the rule, and exits with **status 64**:
```text
csaf-crud: listen_port: port 80 is in the refused range 0-1024 (privileged ports 0-1023 plus 1024, by policy); accepted: 1025-65535
```
What is refused: 0 to 1024 (the privileged ports, plus 1024 by policy;
port 0 would make the operating system pick any free port), a number
above 65535, and a text that is not a canonical decimal (a sign, a
leading zero, a space, a radix prefix). No other failure uses status 64:
a config file that is not TOML, or a port written as text where a number
belongs, exits with 1. The full table of what is refused and why is in
[REFERENCE.md](../REFERENCE.md#listen-ports); the troubleshooting entry is
[A port is refused](Troubleshooting_Guide.md#a-port-is-refused-exit-status-64).
**Upgrading from a release that accepted a low port.** A config with
`listen_port = 80` or `443` used to start, if the process had the
privilege, and now stops with status 64. Move the port above 1024 and put
a reverse proxy or a firewall redirect in front if clients must reach 80
or 443. Check the config before you upgrade:
```bash
`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:
| `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.
### Releases that fix a security flaw
Find out what is running with `csaf-crud --version`, then compare it
with this list. The full text of each entry is in the
[changelog](../CHANGELOG.md).
- **Upgrade to 1.5.26 or later if untrusted hosts can reach the UDP
(HTTP/3) listener.** Release 1.5.25 bundles `quinn-proto` 0.11.17,
and upstream marks every version up to 0.11.18 as affected
(GHSA-53rm-773f-4q8c, advisory `ndaal-sa-2026-753`, CVSS v3.1 7.5 and
v4.0 8.7, both High; ndaal scored it because upstream published no
vector). After the QUIC handshake, one 13-byte frame makes the
endpoint open streams until memory or CPU runs out. The server asks
for no client certificate, so any peer that completes the handshake
can send it, and an out-of-memory abort ends the whole process, the
TCP listener included. Treat every release before 1.5.26 as affected
unless you have checked its `Cargo.lock`. This was assessed from the
upstream advisory and the source, not reproduced against a running
server.
- A default install binds `127.0.0.1`, which a remote peer cannot
reach. The exposure starts when `listen_addr` is set to `::` or a
routable address.
- Until you can upgrade, do one of: keep `listen_addr` on loopback;
block UDP on `listen_port + 1` at the firewall (the TCP listener is
not affected); or build with `--no-default-features`, which drops
HTTP/3.
- If the service ended under HTTP/3 traffic, see
[Process killed after HTTP/3 traffic](Troubleshooting_Guide.md#process-killed-after-http3-traffic-releases-before-1526).
- **macOS only: upgrade to 1.5.26 or later if untrusted hosts can send
UDP to the HTTP/3 port.** `quinn-udp` before 0.5.16
(GHSA-6pp4-4cxf-xf88, advisory `ndaal-sa-2026-754`, scored 7.5 and
8.7 like the one above) can repeat non-blocking reads after a
truncated datagram on the Unix and Apple fallback receive path. The
Linux build uses the batched `recvmmsg` path and the Windows builds
their own, which upstream states differ from the affected one.
- **Release 1.5.27 is a routine update.** It moves `rustls` to 0.23.46,
which fixes four advisories (RUSTSEC-2026-0346 to -0349, recorded as
the VEX statement `ndaal-sa-2026-887`). The changelog finds that none
of them reaches csaf-crud: no code sets a custom `ClientSessionStore`,
nothing enables ECH, the `brotli` feature is off, and the only
`rustls` client is the updater's `ureq`, because the server never acts
as a TLS client. That reachability was read from the sources and the
feature set, not reproduced against a running server.
### Release 1.5.28: a listen port below 1025 no longer starts
Check `listen_port` and `quic_port` in your config **before** you upgrade.
A value below 1025 (`listen_port = 80` or `443`, for example) used to start
when the process had `CAP_NET_BIND_SERVICE` or ran as root. From 1.5.28 the
server refuses it at start, before it opens a database, with exit status 64
and a message that names the key (see [Listen ports](#listen-ports)). A
service manager that restarts on failure will loop on it.
- Move the port above 1024. If clients must reach 80 or 443, put a reverse
proxy or a firewall redirect in front of the server.
- A script that tests for exit status 1 on a bad port now sees 64.
- `listen_port = 65535` now starts, with HTTP/3 off, where it used to stop
with an integer overflow. `quic_port` sets the HTTP/3 port explicitly.
### 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.
| `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.