#!/usr/bin/env bash set -euo pipefail # TB-006 batch 2: disable local auth; Servarr -> External; skip tautulli. # Run on valhalla as ginnoir. set_servarr_external() { local app="$1" local cfg="/config/${app}/config.xml" if [[ ! -f "$cfg" ]]; then echo "SKIP ${app}: no ${cfg}" return 0 fi echo "=== ${app}: set External ===" docker stop "$app" >/dev/null python3 - "$cfg" <<'PY' import sys import xml.etree.ElementTree as ET path = sys.argv[1] tree = ET.parse(path) root = tree.getroot() auth_required = None for el in list(root): if el.tag == "AuthenticationRequired" and el.text: auth_required = el.text.strip() break for el in list(root): if el.tag in ("AuthenticationMethod", "AuthenticationRequired"): root.remove(el) ext = ET.SubElement(root, "AuthenticationMethod") ext.text = "External" req = ET.SubElement(root, "AuthenticationRequired") req.text = auth_required or "Enabled" tree.write(path, encoding="UTF-8", xml_declaration=True) print(f" wrote External (AuthenticationRequired={req.text})") PY docker start "$app" >/dev/null grep Authentication "$cfg" } for app in sonarr radarr prowlarr whisparr; do set_servarr_external "$app" done echo "=== bazarr: verify auth disabled ===" python3 - <<'PY' import yaml path = "/config/bazarr/config/config.yaml" with open(path) as f: d = yaml.safe_load(f) auth = d.setdefault("auth", {}) changed = False if auth.get("type") is not None: auth["type"] = None changed = True if auth.get("username"): auth["username"] = "" changed = True if auth.get("password"): auth["password"] = "" changed = True if changed: with open(path, "w") as f: yaml.safe_dump(d, f, default_flow_style=False, sort_keys=False) print(" updated bazarr auth -> type null") else: print(" already type null / no credentials") PY docker restart bazarr >/dev/null echo "=== qbittorrent: disable WebUI auth (keep API key for *arr) ===" QCONF="/config/qbittorrent/qBittorrent/qBittorrent.conf" docker stop qbittorrent >/dev/null python3 - "$QCONF" <<'PY' from pathlib import Path import sys path = Path(sys.argv[1]) lines = path.read_text().splitlines() out = [] added = False for line in lines: if line.startswith("WebUI\\AuthenticationEnabled="): out.append("WebUI\\AuthenticationEnabled=false") added = True continue out.append(line) if not added: final = [] for line in out: final.append(line) if line.strip() == "[Preferences]": final.append("WebUI\\AuthenticationEnabled=false") added = True out = final path.write_text("\n".join(out) + "\n") print(" WebUI\\AuthenticationEnabled=false") PY grep 'WebUI\\AuthenticationEnabled' "$QCONF" || true docker start qbittorrent >/dev/null echo "=== nzbget: disable form auth (API creds unchanged) ===" NZB="/config/nzbget/nzbget.conf" sed -i 's/^FormAuth=.*/FormAuth=no/' "$NZB" sed -i 's/^SecureControl=.*/SecureControl=no/' "$NZB" grep -E '^(FormAuth|SecureControl)=' "$NZB" docker restart nzbget >/dev/null echo "=== stash: external authwall (Authentik + internal_only) ===" python3 - <<'PY' from pathlib import Path import re path = Path("/config/stash/config.yml") text = path.read_text() text = re.sub(r'^dangerous_allow_public_without_auth:.*$', 'dangerous_allow_public_without_auth: "true"', text, flags=re.M) text = re.sub(r'^username:.*$', 'username: ""', text, flags=re.M) text = re.sub(r'^password:.*$', 'password: ""', text, flags=re.M) text = re.sub(r'^security_tripwire_accessed_from_public_internet:.*\n', '', text, flags=re.M) path.write_text(text) print(" dangerous_allow_public_without_auth=true, cleared username/password") PY docker restart stash >/dev/null echo "=== *arr download clients: internal Docker URLs (bypass Caddy forward_auth) ===" python3 - <<'PY' import json import sqlite3 # Servarr apps talk to download clients over the media network, not via *.ginnoir.com. INTERNAL = { "Nzbget": ("nzbget", 6789), "QBittorrent": ("qbittorrent", 3232), "Deluge": ("deluge", 8112), } PUBLIC_SUFFIX = ".ginnoir.com" LOCALHOSTS = {"localhost", "127.0.0.1"} for app in ("sonarr", "radarr", "prowlarr", "whisparr"): db = f"/config/{app}/{app}.db" conn = sqlite3.connect(db) cur = conn.cursor() cur.execute("SELECT Id, Name, Implementation, Settings FROM DownloadClients") for cid, name, impl, settings_json in cur.fetchall(): if impl not in INTERNAL: continue settings = json.loads(settings_json) host = (settings.get("host") or "").strip() internal_host, port = INTERNAL[impl] if host == internal_host and settings.get("port") == port and not settings.get("useSsl"): continue if PUBLIC_SUFFIX in host or host in LOCALHOSTS: old = f"{host}:{settings.get('port')}" if settings.get("useSsl"): old += " ssl" settings["host"] = internal_host settings["port"] = port settings["useSsl"] = False cur.execute( "UPDATE DownloadClients SET Settings=? WHERE Id=?", (json.dumps(settings), cid), ) print(f" {app}/{name}: {old} -> {internal_host}:{port} http") conn.commit() conn.close() PY echo "=== deluge: bypass web login (Authentik at edge; patch re-applies on recreate) ===" if docker exec deluge grep -q 'homelab external auth' /lsiopy/lib/python3.12/site-packages/deluge/ui/web/auth.py 2>/dev/null; then echo " already patched" else docker cp /tmp/patch-deluge-auth.py deluge:/tmp/patch-deluge-auth.py docker exec deluge python3 /tmp/patch-deluge-auth.py docker restart deluge >/dev/null echo " patched and restarted" fi echo "=== done (tautulli untouched) ==="