nas-runbooks/common/vps-qbittorrent-docker.md

4.4 KiB

VPS — qBittorrent Docker + VPN WireGuard intégré (kill-switch)

Date : 2026-07-12 Contexte : remplacement de qbittorrent-nox natif (systemd + policy routing UID vers wg0 KeepSolid) par un conteneur hotio/qbittorrent avec VPN interne. L'ancienne archi rendait le WebUI inexposable (routing asymétrique : requêtes entrantes par eth0/tunnel CF, réponses sortantes par wg0).

Architecture finale

  • VPS : vmi3418972 (Contabo), Tailscale 100.94.90.119, hostname contabo-vps-qbt.
  • Conteneur : ghcr.io/hotio/qbittorrent:latest (qBittorrent 5.2.3), compose dans /opt/qbittorrent-docker/.
  • VPN : WireGuard KeepSolid dans le conteneur (VPN_ENABLED=true, VPN_CONF=wg0, provider generic). Conf : /opt/qbittorrent-docker/config/wireguard/wg0.conf (chmod 600, survit aux recreate via le volume ./config:/config). Clé = peer régénéré 12/07 (l'ancienne était grillée), stockée dans Vaultwarden org claude.
  • Kill-switch : nftables généré par hotio, policy drop sur input/output. Seuls autorisés : endpoint WireGuard (UDP), WebUI 8080 depuis VPN_LAN_NETWORK, wg0, lo. Testé : ip link del wg0 dans le conteneur → curl et DNS totalement bloqués, zéro fuite.
  • WebUI : port publié 8080:8080 → cloudflared (cloudflared-mcp-vps, réseau mcp-vps) le joint via vps.internal:8080 (host-gateway). Ingress qb.yesminedor.tn → http://vps.internal:8080 déjà en place côté dashboard Zero Trust (tunnel mcp-vps réutilisé).
  • Sécurité WebUI : user dédié (pas admin), mot de passe fort (Vaultwarden org claude), LocalHostAuth=true (PAS de bypass localhost — critique : le trafic CF arrive vu comme IP docker locale), CSRF actif, HostHeaderValidation avec ServerDomains=qb.yesminedor.tn;localhost.
  • Downloads : /opt/qbittorrent-docker/data (chown 1000:1000 — hotio ne prend possession que de /config, pas de /data → sinon torrent en error mkdir permission denied).

Pièges rencontrés

  1. VPN_LAN_NETWORK=172.16.0.0/12 refusé : le bloc englobe le sous-réseau du conteneur lui-même (172.19.0.0/16) → hotio exit 1 « Route conflicts with address on interface eth0 ». Énumérer les sous-réseaux précis : 172.17.0.0/16,172.18.0.0/16 (docker0 + réseau mcp-vps de cloudflared).
  2. qBittorrent 5.x API : login OK = HTTP 204 (pas 200) + cookie SID. Extraction cookie par sed fragile → utiliser curl -c cookiejar. Trop d'échecs login = ban IP temporaire (401 sur tout).
  3. HostHeaderValidation : une fois ServerDomains défini, Host: localhost:8080 est rejeté même avec localhost dans la liste (le test local doit envoyer -H "Host: qb.yesminedor.tn").
  4. Éditer qBittorrent.conf UNIQUEMENT conteneur arrêté (qbt réécrit le fichier à l'arrêt). Hash mot de passe : PBKDF2-HMAC-SHA512, 100 000 itérations, dklen 64, format WebUI\Password_PBKDF2="@ByteArray(b64salt:b64hash)".
  5. Ne JAMAIS refaire le policy routing UID système (ip rule uidrange → table dédiée) : c'était la cause du WebUI inexposable. Le VPN dans le conteneur isole tout.
  6. Règles Tailscale à ne pas toucher lors des nettoyages ip rule/mangle : fwmark 0x80000/0xff0000 (prio 5210/5230/5250), lookup 52 (5270), et les 2 CONNMARK 0xff0000 dans mangle sont à Tailscale, pas au VPN torrent.

Démolition de l'ancienne instance (faite le 12/07)

  • qbittorrent-nox.service : stop, disable, fichier supprimé, daemon-reload.
  • wg-quick@wg0 système : stop, disable, /etc/wireguard/wg0.conf shreddé (clé grillée). Vérifié avant : seul uid 998 routait dessus, memory-node passe par Tailscale.
  • ip rules 5208 (to 172.16.0.0/12) et 5209 (uidrange 998) supprimées.
  • ufw : règles 8080 docker0 + 8080 from 172.16.0.0/12 supprimées. Conservé : 22+8080 sur tailscale0, 22+8080 fallback IP Nabil, 22 from 172.16.0.0/12 (SSH vps.internal depuis conteneurs MCP).
  • deluser qbtuser + rm -rf /home/qbtuser49 Go libérés (disque 28% → 3%).

Vérifs de routine

# IP de sortie = IP KeepSolid, jamais l'IP Contabo (194.163.159.151)
docker exec qbittorrent curl -s ifconfig.me
# handshake WG
docker exec qbittorrent wg show
# test kill-switch (puis docker restart qbittorrent pour rétablir)
docker exec qbittorrent ip link del wg0 && docker exec qbittorrent curl -s --max-time 10 ifconfig.me || echo "BLOQUÉ = OK"

Persistance : restart: unless-stopped + docker.service enabled ; la conf WG et le mot de passe WebUI vivent dans ./config (volume disque).