Files
felhom.eu/documentation/audits/usenet-2026-10-12/README.md
T
admin 52615d9139
gates / gates (push) Successful in 6m34s
Website + evidence: the Usenet pair (R-988 closed), and the drill route filed as R-989
The catalog published SABnzbd and NZBGet today (app-catalog 8ee687e). Here:

- two cards in the media category on both language sets, each with the project's own logo and
  each saying IN BOLD that a paid Usenet subscription is the household's own
- the published count 60 -> 62 everywhere it is stated; the category badges are derived from
  the cards now (media read 15 with 17 cards, otthon/home read 14 with 16 — both stale)
- documentation/audits/usenet-2026-10-12/: both bench runs, both box steps and verdicts, the
  nine defects with their measurements, what a missing provider left unmeasured, the teardown
- R-988 closed (moved to CLOSED-ITEMS in this commit); R-989 filed: the drill-catalog route of
  09 §6.5 cannot be followed — that repo is private and no box has catalog credentials, so the
  templates were staged in the box's own catalog cache instead
- walk.app_env: a SECRET field in app.yaml is stored ENC:, so that helper returns ciphertext —
  the docstring says so now, measured the hard way
- STATUS.md and REPORT-usenet-2026-10-12.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
2026-10-12 22:58:59 +02:00

156 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The Usenet pair for the catalog — SABnzbd and NZBGet (2026-10-12, R-988)
**Why.** The operator asked for Usenet clients beside the torrent pair published the same day
("sabnzbd, nzbget for example"). Radarr, Sonarr and Jellyseerr can hand a job to a Usenet client
exactly as they hand one to a torrent client, and the catalog had none.
**Where this got to.** Both templates are written, measured in two venues, and published:
`app-catalog-felhom.eu` — SABnzbd 5.1.3 (`lscr.io/linuxserver/sabnzbd`) and NZBGet v26.3
(`nzbgetcom/nzbget`, the project's own image), each with its complete onboarding record and a first
ladder step proven on the bench (LXC 9401) **and** on scratch guest 9202 through the product.
## What is NOT measured, and why it is stated first
**A household must bring a paid Usenet provider**, and this session had none. So the download chain —
connect to a provider, authenticate, fetch articles, par2 repair, unpack, the finished file on the
drive — **is not measured**, and neither is memory under that load. Both app pages say the
subscription is required before anyone installs, and both records carry the gap. Everything else is
measured, including each app's queue taking a job through its own API and keeping it across the
product's Update, which is the store an upgrade must not lose.
The operator was offered three ways to close the gap (a provider's free trial, a free text-only
server to prove the outbound path, or publishing with the gap stated) and the third was taken for
this round. A trial account would add one bench phase.
## What is in here
| | |
|---|---|
| `sabnzbd-bench/`, `nzbget-bench/` | the twelve questions asked of each app on LXC 9401 (image, telemetry, entrypoint, first admin, the Host header, storage, ownership, health, the seed with no provider, the config as the app rewrote it, memory, the log), plus `14-both-after-the-fixes.txt` and SABnzbd's `15-sabnzbd-login.txt`, and `MV-*/` — the harness verdict for each ladder step |
| `box/` | guest 9202 through the product: `boxwalk-results.json` (install, stranger, household, lock-out), each app's `step.txt` + `box-verdict-<app>.json` (the product's guarded Update), the app logs, the settings each app ended up with |
| `tools/` | `stage_in_cache.py` (why the drill route could not be used, and what replaced it), `boxwalk_usenet.py`, `boxstep_usenet.py`, `boxstep_sabnzbd_again.py` |
## Nine defects, each found by measuring and fixed before publication
**NZBGet**
1. **The image has no curl**, so the healthcheck could never pass. It carries busybox wget, python3
and openssl (measured), so the probe is the BusyBox/wget family from `REUSE.md`: rc 0 against the
running app, rc 1 against a port nothing serves.
2. **The app's queue, nzb files, temp dir and log were on the household's DRIVE.** NZBGet ships
`MainDir=/downloads`, and this template's own backup block calls that folder "optional, fetchable
again" — so a restore would have brought back the finished files and lost what was downloading.
The seed moves `MainDir` to `/config/data`, inside the volume classified `important`; only the
finished and half-assembled files stay on the drive.
3. **The shipped config file carries the well-known default password in plain text.** The image
overrides it on the command line (`-o ControlPassword=...`), so the default does not work — but the
file keeps advertising it, and the app's own CLI reads the FILE and so could not talk to its own
server ("No response or invalid response", measured). The seed writes the generated password into
the file: the default is gone (grep count 0) and the CLI works.
4. **The shipped config asks nzbget.com for new versions** (`UpdateCheck=stable`) and ships the
article cache and write buffer off, which the app itself warns about on every start. The seed sets
`UpdateCheck=none`, `ArticleCache=100`, `WriteBuffer=1024`.
**SABnzbd**
5. **The app was open to every visitor.** `inet_exposure = 5` ("web UI with login for external") is
decided from the CLIENT ADDRESS, and SABnzbd's built-in LAN ranges include the 172.16.0.0/12 that
traefik arrives from — so the login applied to nobody. Measured before the fix: a request with no
credentials at all answered 200 and served the Quick-Start Wizard. `local_ranges = 127.0.0.1`
replaces those defaults, so nothing a proxy presents is local; the same list feeds
`xff_trusted_networks`, so it also stops the app trusting an `X-Forwarded-For` header, which the
tunnel's chain can carry client-written (R-753's family). Read in `sabnzbd/security.py` and
`sabnzbd/misc.py`, then measured in both directions on the bench.
6. **The healthcheck dialled `/`, which answers 303**, so the container could never become healthy.
The probe is the API's version call now, accepting 200, 401 or 403 — all three mean the web server
answered.
7. **The controller REJECTED two blocks of `.felhom.yml`**: `backup.userdata["media/usenet"]` and
`data_paths[userdata/media/usenet]` "matches no compose bind", because the compose bound the two
subfolders and the blocks named their parent. Both were inert — the downloads were in no declared
backup class at all. One bind now, with the app's own `complete/` and `incomplete/` inside it.
8. **`after_install` always failed, so the install HELD and the box kept the app closed** ("this app
is waiting for its first setup", measured on 9202 — the gate doing exactly its job). The readback
greps expected `"key": value`; SABnzbd's API answers `"key":value` with no space. With `-E` and an
optional space the marker prints, and the three settings that matter are read back out of the
running app.
**The fixtures themselves**
9. Two defects in the test tooling, both of the same shape — assuming a tool exists inside a
container. The bench NZBGet fixture encoded the NZB with `base64 -w0` **and** called curl inside an
image that has neither; it talks from the harness host now. The box SABnzbd fixture read the API
key from `app.yaml`, where a deploy secret is stored **encrypted** (`SAB_API_KEY: ENC:...`), so the
app answered "API Key Incorrect"; it reads the value the app itself holds now. `walk.app_env`'s
docstring was corrected to say what an `ENC:` value does, since the next session would have made
the same mistake.
## And one finding that was my measurement, not the app
NZBGet looked broken — `Could not create file /downloads/tmp/nzb-1.tmp`, and the queue refused the
job. The cause was my own probe: I ran the app's CLI with `docker exec`, which defaults to **root** in
this image, and that recreated the app's working directories as root. The app, running as uid 1000,
could then write nothing. Re-measured from a clean deploy with no root-run probe, the queue takes the
job. **Second time today** that measuring from the wrong place convicted an innocent app (the
afternoon's apparent household lock-out was the same shape), which is why both are written down.
## The drill route is broken, and what was used instead
`09` §6.5 says a new app's box walk runs from the drill catalog. It cannot today:
`admin/app-catalog-drill` is **private** (Gitea API: `private=true`, while the live catalog is
`private=false`), and **no controller.yaml on 9202 — current or any of its 30 saved copies — has ever
carried `git.username`/`git.token`**. The box's clone fails with
`fatal: could not read Username for 'https://gitea.dooplex.hu'`, exit 128. The route worked for the
campaigns of 2026-10-10 and the torrent pair this afternoon, so the repository's visibility changed
between then and now.
Rather than put a credential on a box or change a repository's visibility unasked, the two templates
were **staged in the box's own catalog cache** (`tools/stage_in_cache.py`) — which is where the sync
copies a catalog change anyway, so the box deploys, scans, badges and updates exactly as it would from
a catalog; the only thing not exercised is the git fetch, which is not what a new app's box walk
measures. The image move for the ladder step was made in the same place, which is what `drill_bump`
does to the repo. Two guards: the sync interval went to 12h for the duration (restored at teardown)
and the walk calls rescan, never sync. **Filed as a register row (R-992)** — the fix (a catalog credential on
the box, or a public drill repo) is an operator decision.
## What the two venues said
| | bench LXC 9401 | box 9202, through the product |
|---|---|---|
| SABnzbd version-5.1.2 → version-5.1.3 | **proven** — seed read before and after, healthy, abort starts-and-serves, peak 78 MB = 7.1 % of 1024M, 0 kills, 8344 requests (all 303: the login redirect, i.e. closed) | **proven** — the guarded Update ran backing-up → pulling → copying → starting → verifying → done in 27.7 s; the queued job AND the category read back |
| NZBGet v26.2 → v26.3 | **proven** — seed read before and after, healthy, abort starts-and-serves with the job still queued, peak 8.8 MB = 1.7 % of 512M, 0 kills, 8404 requests (all 401: closed) | **proven** — the guarded Update ran backing-up → pulling → copying → verifying → done in 15.4 s; the queued job read back |
C3, the harness's negative control, came back `failed` before either edge was run, as the rule
requires.
## First admin, both class 1
| | how | measured |
|---|---|---|
| SABnzbd | the entrypoint seeds `sabnzbd.ini` with the generated login, the generated API key, `inet_exposure = 5` and `local_ranges = 127.0.0.1`; `after_install` reads three of the four back out of the running app | a stranger gets `/login`; `/config/` and `/wizard/` redirect there; the household's session reaches `/config/` with 200; a wrong password gets no session; the API answers the generated key and refuses a wrong one; six wrong sign-ins lock nobody out |
| NZBGet | the image applies `NZBGET_USER`/`NZBGET_PASS` on every start, and this template also writes them into the config file so the shipped default is not left there | the generated password 200, a wrong one 401, **the documented default `nzbget / tegbzn6789` 401**, the default name with our password 401, six wrong tries lock nobody out |
## State left behind — three layers, verified
`box/teardown-usenet.json` holds the measurements.
- **Machine (guest 9202):** both apps removed **through the product** (stop, then remove with the
drive data), `deployed: false` for both, both config volumes gone, no container left, and the two
download folders deleted from the scratch drive. The guest's standing apps (filebrowser, paperless)
are untouched and running.
- **The staging undone:** the two templates are out of the box's catalog cache and out of
`/opt/docker/stacks`, and the sync interval is back from 12h to 15m. The catalog pointer was never
changed away from the live catalog for this campaign (the staging needed it), and the restoration is
proven from a **different channel than the controller's own verdict** (R-286): the sha256 of
`sabnzbd`, `nzbget` and `grocy`'s `.felhom.yml` in the box's cache equals the sha256 of the same
files at the published `origin/main`, with a decoy pair that must differ and does. The two apps are
in that cache again **legitimately** — they are published now.
- **Host (demo-hp):** bench LXC 9401 has no containers and **no volumes** (the three anonymous volumes
this campaign left were removed by name — never a prune) and is **stopped**, as it was found. The
drill catalog is reset to the live `main`, and the DooPlex catalog clone is clean at the published
commit.
- **Hub:** nothing to undo — 9202 reports to no hub by its own configuration.
- **One thing that is NOT a leftover:** `media/torrents` and `media/transmission` exist on the scratch
drive again, empty (4–8 KB), although the afternoon's teardown deleted them. The product's own
storage sync recreates a published app's declared folder so the file browser can show it; these hold
nothing and were not created by this campaign's tests.