--- type: reference title: qBittorrent description: "The download client for the *arr stack: pinned image, shared WireGuard netns, WebUI credential recipe." tags: [qbittorrent, arr-stack, frankbook-pro, gluetun] timestamp: 2026-09-03T00:00:00Z --- # qBittorrent qBittorrent is the torrent download client for the *arr stack. The stack runs on **FrankBook-Pro** (the Mac mini) as quadlet units inside its podman machine — not on Dankserver. It receives download requests from Sonarr, Radarr, Lidarr, and Readarr via category tagging. **Host:** FrankBook-Pro (podman machine) — **Access:** WebUI on port `8080` published by the [[Gluetun]] unit because qBittorrent shares that container's network namespace (a netns-sharer publishes no ports of its own). ## Facts | Item | Value | |------|-------| | Image | `lscr.io/linuxserver/qbittorrent:4.6.7` — **pinned**, see the pin rationale below | | Network | `Network=gluetun.container` (torrent egress exits via the WireGuard/ProtonVPN tunnel) | | Ports | 8080 (WebUI) and 6881 TCP+UDP published on the gluetun unit | | Env | `PUID=0`, `PGID=0`, `TZ=America/Los_Angeles`, `UMASK_SET=022` | | Volumes | `/Volumes/COMPLETE/qbittorrent-data:/config`, plus `/Volumes/COMPLETE` and `/Volumes/INCOMPLETE` mounted at identical paths | | Auth | Web UI username/password (`admin` + generated password); LAN bypass not used | ## Why the 4.6.7 pin exists (2026-09-01) Readarr's abandoned qBittorrent client rejects qB ≥ 5's **204 empty-body login success** as `Authentication failure` — a runtime break, not just an API-create issue. The fleet pins `lscr.io/linuxserver/qbittorrent:4.6.7` while Readarr is in the stack. **Never unpin to `latest` while Readarr is in the fleet.** Second trap on any version flip: qB 5.x's shutdown conf rewrite leaves a `WebUI\Password_PBKDF2=@ByteArray(...)` line that 4.6.7 rejects — every login then returns "invalid credentials" (the arrs included). Fix = rewrite the same conf line with the same password while qB is stopped. ## Persistent WebUI credentials (verified 2026-08-30) qBittorrent 5 mints a **new temporary password at every start** unless a password is stored in `qBittorrent.conf`. That rotation silently breaks every arr download client after each reboot. The Web-API has no password endpoint any more (`/api/v2/app/setPassword` was removed), and the linuxserver image has no `WEBUI_AUTH` handling — so persistence must be written to the conf directly, in qb's own `salt:hash` format (`src/base/utils/password.cpp`: PBKDF2-HMAC-SHA512, 100 000 iterations, 16-byte salt = 4×uint32, then `base64(salt) + ":" + base64(hash)` as the raw value): ```python import base64, hashlib, os salt = os.urandom(16) h = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000, 64) val = (base64.b64encode(salt) + b":" + base64.b64encode(h)).decode() # with the container STOPPED, in [Preferences]: # WebUI\Username=admin # WebUI\Password_PBKDF2= ``` Then `podman start qbittorrent` and confirm login survives a `podman restart` — the temp-password rotation stops the moment stored credentials verify. The chosen value lives host-local in `/home/dank/.qbpass` (mode 600, owner `dank`) — never in git; every arr download client and [[Prowlarr]] reference it. (The classic `@ByteArray(salt+hash)` form is WRONG for qb5: login 401s on it; verify() base64-decodes the two halves separately.) ## Deployment (current) Quadlet unit `qbittorrent.container` from this repo (`frankbook-pro/qbittorrent/quadlet/`), installed inside the podman machine at `/home/core/.config/containers/systemd/qbittorrent.service` by the frankbook-pro handler in `scripts/deploy-remote.sh`: ```ini [Container] ContainerName=qbittorrent Image=lscr.io/linuxserver/qbittorrent:4.6.7 Network=gluetun.container Environment=PUID=0 Environment=PGID=0 Environment=TZ=America/Los_Angeles Environment=UMASK_SET=022 Volume=/Volumes/COMPLETE/qbittorrent-data:/config Volume=/Volumes/COMPLETE:/Volumes/COMPLETE Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE [Service] TimeoutStartSec=900 Restart=always ``` ## Gotchas (verified live) - Temporary-password rotation on every boot is the default state of an untouched qb5 — see the persistence recipe above; without it, all arr download clients silently fail queue adds ("Couldn't add release … to download queue" in the arr logs) after any restart. - API login from `curl` needs a `Referer: http://:/` header (CSRF protection) or POSTs get rejected even with a valid SID. - Cookie-name quirk by version: qB 5.x names its WebUI session cookie `QBT_SID_`; 4.6.7 uses plain `SID`. Any automation must parse the cookie generically (e.g. `grep -oE '[A-Za-z_]*SID[A-Za-z0-9_]*=[^;[:space:]]*'`) instead of hard-coding a port-suffixed name, or it fails with 403 cascades that never reach qB as failed logins. - Egress verification for this stack must run **inside the gluetun container** (`podman exec gluetun wget -qO- https://ipinfo.io/json`); running the same probe one ssh-context out returns the residential IP — a false leak alarm. - Ensure umask is set to `022` so downloaded files are readable by arr apps. ## E2E verification after any pipeline change Run after ANY change to the torrent pipeline — WireGuard credential rotation, qB version change, gluetun reconfig, category re-seeding. Proves the full grab path (tunnel egress → qB download → category save path → clean removal). All commands from a dev box via `ssh frankbook-pro`; inside the Mac shell export `PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"` for `podman machine ssh`. **Access note (2026-09-21):** the Mac's login user is **dank**@192.168.0.112 (NOT core — `core` is the podman-machine VM user, reached only `podman machine ssh`). blankpad has key auth for `dank@` (bootstrapped 2026-09-21) and a `frankbook-pro` alias in `~/.ssh/config`. macOS sshd has a low MaxAuthTries: when password-bootstrapping use `-o PreferredAuthentications=password -o PubkeyAuthentication=no -o IdentitiesOnly=yes` or the key offers exhaust the attempts before the password is tried. **1. Egress gate — MUST run inside gluetun:** ```bash podman machine ssh 'podman exec gluetun wget -qO- https://ipinfo.io/json' ``` Require `org` = AS9009 (M247 — ProtonVPN Panama PA-12 config); judge by org, never country alone. NEVER run this one ssh-context out (bare `wget` on the Mac host): it silently returns the residential Comcast IP and reads as a hard-gate leak that does not exist. A residential reading ⇒ re-run inside the container before diagnosing. **2. Login — generic cookie parse.** The QPW fetch is flaky (~1 in N returns empty); assert length before use — an empty-password run once produced a Forbidden loop that cost hours: ```bash QPW=$(podman machine ssh 'cat /home/core/Containers/qbittorrent/env' | grep QBIT_WEBUI_PASSWORD | cut -d= -f2) [ ${#QPW} -eq 20 ] || exit 1 SID=$(curl -si --data-urlencode "username=admin" --data-urlencode "password=$QPW" \ http://127.0.0.1:8080/api/v2/auth/login \ | grep -oE '[A-Za-z_]*SID[A-Za-z0-9_]*=[^;[:space:]]*' | head -1) curl -b "$SID" … # pass the WHOLE match ``` WebUI from the LAN: `http://192.168.0.112:8080` (published on the gluetun unit). Username `admin`; password = the machine-local `QBIT_WEBUI_PASSWORD` (20 chars, generated once at provisioning by the deploy handler — never in the repo). Parse-bug 403s never reach qB as failed logins — no ban accrues; server-side truth is `/Volumes/COMPLETE/qbittorrent-data/qBittorrent/config/qBittorrent/logs/qbittorrent.log` (`WebAPI login success/failure` + source IP). Repeated REAL failures accrue toward qB's 5-failures/3600 s IP ban. **3. Add a legal test torrent.** Blender open-movie torrents (~300 MB, e.g. Big Buck Bunny) via `POST /torrents/add` with the raw `.torrent` file (`-F "file=@…"`) and `category=Finished Torrents`. Never leave a real-looking grab behind. **4. Poll and verify.** Poll `GET /torrents/info` (~20 s): state `downloading` → `moving` (INCOMPLETE→COMPLETE move) → `stalledUP`/`uploading` at progress=1. `save_path` must be `/Volumes/COMPLETE/Finished Torrents` (qB 4.x auto-creates unknown categories at DefaultSavePath; 5.x did NOT inherit category paths — one more reason for the 4.6.7 pin). Confirm on disk: `ls "/Volumes/COMPLETE/Finished Torrents/"`. Throughput ~17 MB/s is normal for the tunnel. **5. Cleanup — delete only your artifact:** ```bash curl -s -o /dev/null -w "%{http_code}" -b "$SID" \ --data-urlencode "hashes=" \ --data-urlencode "deleteFiles=true" \ http://127.0.0.1:8080/api/v2/torrents/delete # expect 200 ``` Target the known hash literally — the user's real grabs share the directory and MUST be untouched. Verify: hash absent from `/torrents/info`, your dir gone, all other entries intact. **Version-coupling constraints:** qB is pinned to 4.6.7 (Readarr's EOL client rejects qB ≥ 5's `204` login at runtime — see the pin rationale above). Do not unpin while Readarr exists. After ANY qB version flip, regenerate the `WebUI\Password_PBKDF2` line fresh (same password, 100k iterations) — a 5.x-written hash line makes 4.6.7 reject every login. Rewrite while qB is stopped. ## See Also - [[ARR-Stack]] — full *arr stack documentation - [[Gluetun]] — the WireGuard tunnel this container shares