Files
felhom.eu/documentation/audits/usenet-2026-10-12
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
..

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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. 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.