nas: new storage host on A1SAi-2750F — 4 TB off USB onto SATA

Builds the box that ends the usb4t dropouts: the JMicron bridge was the
least reliable device in the setup and it held the intended off-site copy
of ks4. The 4 TB now sits on direct SATA as pool `tank`.

- OS on mdraid RAID1 across two 120 GB SSDs (Intel 330 + Toshiba Q300),
  both ESPs bootable; `incus` ZFS mirror on their tails, ~22% left
  unallocated as over-provisioning
- media at /export/media, exported read-only over NFSv4 to nuc
- transmission-bt moves here (its WireGuard tunnel is in-container, so
  ks4 needed no change) and writes to the dataset locally
- backup pools nucbackup / ks4backup / nasbackup
- monitoring live: msmtp (submission+auth, verified 250), zed with
  NOTIFY_DATA, zpool-health.sh every 15 min, smartd on all three disks

Traps recorded because none of them point at their own cause: booting
with the display active kills the i915 probe and wedges incus; d-i picks
grub-pc vs grub-efi from how the installer booted; `incus storage create`
hangs forever on a mountpoint=none dataset; the BMC is deliberately never
cabled, so there is no out-of-band console.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Julien Lutran
2026-08-30 23:53:30 +02:00
co-authored by Claude Opus 5
parent a2003a11e8
commit 58dcaa5d41
5 changed files with 906 additions and 9 deletions
-194
View File
@@ -1,194 +0,0 @@
# transmission-bt
BitTorrent client in an unprivileged Incus container on `nuc`, with an
**always-on VPN**: all peer traffic exits via ks4's public IP through a
WireGuard tunnel to the `wireguard` container on ks4. Kill switch by
construction — `eth0` has **no default route**, so with the tunnel down
the container simply has no path to the internet.
- Image: `images:ubuntu/24.04`, IP: `192.168.0.7` (macvlan, static via netplan)
- Web UI: `http://192.168.0.7:9091` — no auth
(`rpc-authentication-required: false`); access control is the RPC
whitelist (`192.168.0.*` only)
- Egress: WG peer `10.8.0.21``193.70.35.17:51845`, `AllowedIPs 0.0.0.0/0`
(verified: `curl ifconfig.me` from the container returns ks4's IP)
- Downloads: `/media/downloads` (= `usb4t/media`, same dataset Jellyfin
reads); in-progress files in `/media/.incomplete` so Jellyfin never
scans partials
- Watch folder: `scp` a `.torrent` into `/srv/media/.watchdir` on the host
and it auto-downloads (see "Watch folder" below)
- `transmission-daemon` is `BindsTo=wg-quick@wg0.service` and binds
peer traffic to `10.8.0.21` — three independent layers against leaks
(no default route, unit binding, socket binding)
## Anti-leak design
1. netplan gives `eth0` only: LAN `/24` (web UI + DNS via blocky) and a
`/32` host route to the WG endpoint via the home gateway.
2. `wg-quick` full-tunnel mode adds its fwmark policy routing +
iptables anti-leak rule (`iptables` package required — its absence
makes `wg-quick` fail with `iptables-restore: command not found`).
3. LAN traffic keeps working thanks to wg-quick's
`suppress_prefixlength 0` rule (connected routes win over the
tunnel's default).
## Install script
Run as root on the Incus host. Requires the peer added on ks4 (below).
```bash
#!/usr/bin/env bash
set -euxo pipefail
CNAME="${CNAME:-transmission-bt}"
IMAGE="${IMAGE:-images:ubuntu/24.04}"
incus launch "$IMAGE" "$CNAME"
sleep 8
incus config set "$CNAME" environment.DEBIAN_FRONTEND=noninteractive
incus exec "$CNAME" -- timedatectl set-timezone Europe/Paris
# static LAN config, NO default route (kill switch), /32 to the WG endpoint
incus exec "$CNAME" -- bash -c 'cat > /etc/netplan/10-lxc.yaml <<EOF
network:
version: 2
ethernets:
eth0:
addresses: [192.168.0.7/24]
nameservers:
addresses: [192.168.0.254]
routes:
- to: 193.70.35.17/32
via: 192.168.0.2
EOF
chmod 600 /etc/netplan/10-lxc.yaml
netplan apply'
# packages need a temporary default route (removed right after)
incus exec "$CNAME" -- ip route add default via 192.168.0.2
incus exec "$CNAME" -- apt-get update
incus exec "$CNAME" -- apt-get install -y --no-install-recommends \
transmission-daemon wireguard-tools iptables curl
incus exec "$CNAME" -- ip route del default via 192.168.0.2
# WireGuard full tunnel (generate key, print pubkey for the ks4 side)
incus exec "$CNAME" -- bash -c 'umask 077
wg genkey > /etc/wireguard/wg0.key
wg pubkey < /etc/wireguard/wg0.key
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.8.0.21/24
PrivateKey = $(cat /etc/wireguard/wg0.key)
[Peer]
# wireguard container on ks4 — ALL traffic routes through it
PublicKey = TVs6d7bXTvJ0ZluTLb8wR+zIrsLvkH1944pzM+3dZXM=
Endpoint = 193.70.35.17:51845
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF'
incus exec "$CNAME" -- systemctl enable --now wg-quick@wg0
# media share (same dataset as jellyfin-server)
incus config device add "$CNAME" media disk source=/srv/media path=/media shift=true
incus exec "$CNAME" -- mkdir -p /media/downloads /media/.incomplete
incus exec "$CNAME" -- chown debian-transmission:debian-transmission \
/media/downloads /media/.incomplete
# transmission config (edit only while the daemon is stopped)
incus exec "$CNAME" -- systemctl stop transmission-daemon
incus exec "$CNAME" -- bash -c '
cd /var/lib/transmission-daemon/.config/transmission-daemon
sed -i \
-e "s|\"download-dir\":.*|\"download-dir\": \"/media/downloads\",|" \
-e "s|\"incomplete-dir\":.*|\"incomplete-dir\": \"/media/.incomplete\",|" \
-e "s|\"incomplete-dir-enabled\":.*|\"incomplete-dir-enabled\": true,|" \
-e "s|\"rpc-whitelist\":.*|\"rpc-whitelist\": \"127.0.0.1,::1,192.168.0.*\",|" \
-e "s|\"rpc-authentication-required\":.*|\"rpc-authentication-required\": false,|" \
-e "s|\"bind-address-ipv4\":.*|\"bind-address-ipv4\": \"10.8.0.21\",|" \
settings.json'
# transmission lives and dies with the tunnel
incus exec "$CNAME" -- mkdir -p /etc/systemd/system/transmission-daemon.service.d
incus exec "$CNAME" -- bash -c 'cat > /etc/systemd/system/transmission-daemon.service.d/vpn.conf <<EOF
[Unit]
BindsTo=wg-quick@wg0.service
After=wg-quick@wg0.service
EOF'
incus exec "$CNAME" -- systemctl daemon-reload
incus exec "$CNAME" -- systemctl start transmission-daemon
incus config set "$CNAME" boot.autostart=true
```
On **ks4** (root), authorize the peer with the pubkey printed above:
```sh
incus exec wireguard -- wg set wg0 peer <PUBKEY> allowed-ips 10.8.0.21/32
incus exec wireguard -- wg-quick save wg0
```
## Watch folder (auto-add torrents)
Drop a `.torrent` into `/srv/media/.watchdir` on the host and Transmission
auto-adds it and starts downloading — no web UI needed. The folder lives on
the shared `usb4t/media` dataset (`/media/.watchdir` inside the container).
Edit the **active** config only while the daemon is stopped (it rewrites
`settings.json` on exit). The active file is
`/var/lib/transmission-daemon/info/settings.json` — the daemon runs with
`--config-dir /var/lib/transmission-daemon/info` from
`/etc/default/transmission-daemon`, *not* the `.config` dir.
```bash
incus exec transmission-bt -- install -d -o debian-transmission \
-g debian-transmission -m 0775 /media/.watchdir
incus exec transmission-bt -- systemctl stop transmission-daemon
incus exec transmission-bt -- python3 - <<'PY'
import json
p = "/var/lib/transmission-daemon/info/settings.json"
c = json.load(open(p))
c.update({
"watch-dir": "/media/.watchdir",
"watch-dir-enabled": True,
"watch-dir-force-generic": True, # poll (reliable across the shift mount)
"trash-original-torrent-files": True, # delete the .torrent once added
})
json.dump(c, open(p, "w"), indent=4)
PY
incus exec transmission-bt -- systemctl start transmission-daemon
```
Usage — the `.torrent` is consumed within a few seconds:
```sh
scp some.torrent root@192.168.0.3:/srv/media/.watchdir/
```
- `watch-dir-force-generic: true` makes Transmission **poll** the folder
instead of using inotify, so it reliably sees files written from the host
side across the shift-mounted share.
- `trash-original-torrent-files: true` self-cleans the folder. A *malformed*
`.torrent` is not trashed and gets retried each poll — delete it by hand.
- The watchdir is owned by `debian-transmission` (0775): root's `scp` writes
fine, and Transmission can read the file and remove it after adding.
## Verification
```sh
incus exec transmission-bt -- wg show wg0 latest-handshakes # non-zero timestamp
incus exec transmission-bt -- curl -s https://ifconfig.me # must print 193.70.35.17
incus exec transmission-bt -- bash -c "ping -c1 -W2 8.8.8.8 || echo kill-switch OK" # with wg0 down
# web UI must be tested from a LAN machine — the macvlan quirk means the
# nuc host itself cannot reach 192.168.0.7
```
## Notes
- No inbound peer port is forwarded (would need a proxy device on ks4
+ DNAT through the tunnel); torrents work fine outbound-only, just
connect to fewer peers.
- Jellyfin sees finished downloads under `/media/downloads` — add it as
a library folder or move files into the movie/show trees.
- The image server check can make `incus launch` hang on slow WAN —
launching from the cached image fingerprint (`incus image list`)
bypasses it.