Persist TB-006 batch 2 media SSO and batch 1 auth fixes to the repo.

Media sites get Caddy forward_auth; Authentik scripts cover all proxy providers. BookStack OIDC and Plane admin routing fixes included, plus ops scripts for external auth on valhalla.
This commit is contained in:
ginnoir
2026-06-11 01:19:06 -05:00
parent 63889fc150
commit 1ce63153ab
8 changed files with 308 additions and 27 deletions
+70
View File
@@ -81,56 +81,116 @@ requests.ginnoir.com {
# =============================================================
sonarr.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy sonarr:8989
}
}
}
radarr.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy radarr:7878
}
}
}
bazarr.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy bazarr:6767
}
}
}
prowlarr.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy prowlarr:9696
}
}
}
tautulli.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy tautulli:8181
}
}
}
# =============================================================
# DOWNLOAD CLIENTS — internal only
# =============================================================
qbittorrent.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy qbittorrent:3232
}
}
}
deluge.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy deluge:8112
}
}
}
nzbget.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy nzbget:6789
}
}
}
whisparr.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy whisparr:6969
}
}
}
stash.ginnoir.com {
import internal_only
route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy stash:6970
}
}
}
# =============================================================
# GAMES / ROMS — internal only
@@ -344,16 +404,26 @@ docs.ginnoir.com {
}
plane.ginnoir.com {
route {
import authentik_outpost
handle /api/* {
import authentik_forward_auth
reverse_proxy plane_api:8000
}
handle /auth/* {
import authentik_forward_auth
reverse_proxy plane_api:8000
}
handle /god-mode/* {
import authentik_forward_auth
reverse_proxy plane_admin:3000
}
handle {
import authentik_forward_auth
reverse_proxy plane_web:3000
}
}
}
# =============================================================
# NOTES / PKM — internal only (LiveSync over LAN or tailnet)
+138
View File
@@ -0,0 +1,138 @@
#!/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 "=== 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) ==="
+26
View File
@@ -0,0 +1,26 @@
from pathlib import Path
path = Path("/lsiopy/lib/python3.12/site-packages/deluge/ui/web/auth.py")
text = path.read_text()
if "homelab external auth" in text:
print("already patched")
raise SystemExit(0)
text = text.replace(
" return s.hexdigest() == config['pwd_sha1']",
" return True # homelab external auth",
1,
)
old_login = """ if self.check_password(password):
log.info('Login success (ClientIP %s)', __request__.getClientIP())
return self._create_session(__request__)
else:
log.error('Login failed (ClientIP %s)', __request__.getClientIP())
return False"""
new_login = """ log.info('Login success (ClientIP %s) [homelab external auth]', __request__.getClientIP())
return self._create_session(__request__)"""
if old_login not in text:
raise SystemExit("login() pattern not found")
text = text.replace(old_login, new_login, 1)
path.write_text(text)
print("patched auth.py")
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Apply TB-006 batch 1 forward_auth proxy providers (forward_auth-only sites).
"""Apply TB-006 forward_auth proxy providers (admin batch 1 + media batch 2).
Native OAuth/OIDC apps (bookstack, gitea, portainer, minio, plane, homarr) use
Native OAuth/OIDC apps (bookstack, gitea, portainer, minio, homarr) use
setup-batch1-oidc.py instead. Run this via: ak shell -c exec(open(...).read())
"""
from authentik.core.models import Application
@@ -21,12 +21,25 @@ UPTIME_UNAUTH = """^/status/.*
^/upload/.*"""
SITES = [
# Batch 1 — admin
("homelab-code", "code", "code-server", "https://code.ginnoir.com", None, None),
("homelab-registry-ui", "registry-ui", "Registry UI", "https://registry-ui.ginnoir.com", None, None),
("homelab-vault", "vault", "Vault", "https://vault.ginnoir.com", None, None),
("homelab-uptime", "uptime", "Uptime Kuma", "https://uptime.ginnoir.com", "http://uptime-kuma:3001", UPTIME_UNAUTH),
("homelab-backup", "backup", "Kopia", "https://backup.ginnoir.com", None, None),
("homelab-dbx", "dbx", "DBX", "https://dbx.ginnoir.com", None, None),
("homelab-plane", "plane", "Plane", "https://plane.ginnoir.com", None, None),
# Batch 2 — media *arr stack (internal_only + forward_auth at Caddy)
("homelab-sonarr", "sonarr", "Sonarr", "https://sonarr.ginnoir.com", None, None),
("homelab-radarr", "radarr", "Radarr", "https://radarr.ginnoir.com", None, None),
("homelab-bazarr", "bazarr", "Bazarr", "https://bazarr.ginnoir.com", None, None),
("homelab-prowlarr", "prowlarr", "Prowlarr", "https://prowlarr.ginnoir.com", None, None),
("homelab-tautulli", "tautulli", "Tautulli", "https://tautulli.ginnoir.com", None, None),
("homelab-qbit", "qbit", "qBittorrent", "https://qbittorrent.ginnoir.com", None, None),
("homelab-deluge", "deluge", "Deluge", "https://deluge.ginnoir.com", None, None),
("homelab-nzbget", "nzbget", "NZBGet", "https://nzbget.ginnoir.com", None, None),
("homelab-whisparr", "whisparr", "Whisparr", "https://whisparr.ginnoir.com", None, None),
("homelab-stash", "stash", "Stash", "https://stash.ginnoir.com", None, None),
]
providers = []
@@ -1,13 +1,15 @@
"""Create OAuth2 providers for batch-1 apps with native SSO (ak shell: exec(open(...).read()))."""
from authentik.core.models import Application
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.outposts.models import Outpost
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode, SubModes
from authentik.providers.proxy.models import ProxyProvider
AUTHZ = Flow.objects.get(slug="default-provider-authorization-implicit-consent")
INVALID = Flow.objects.get(slug="default-provider-invalidation-flow")
DEFAULT_MAPPINGS = OAuth2Provider.objects.get(name="famapp").property_mappings.all()
SIGNING_KEY = CertificateKeyPair.objects.get(name="authentik Self-signed Certificate")
# slug, provider name, redirect URI(s)
OIDC_APPS = [
@@ -15,7 +17,6 @@ OIDC_APPS = [
("gitea", "gitea", ["https://gitea.ginnoir.com/user/oauth2/Authentik/callback"]),
("portainer", "portainer", ["https://portainer.ginnoir.com/"]),
("minio", "minio-console", ["https://minio.ginnoir.com/oauth_callback"]),
("plane", "plane", ["https://plane.ginnoir.com/auth/oidc/callback/"]),
]
native_slugs = []
@@ -25,6 +26,8 @@ for slug, pname, redirects in OIDC_APPS:
defaults={
"authorization_flow": AUTHZ,
"invalidation_flow": INVALID,
"signing_key": SIGNING_KEY,
"sub_mode": SubModes.USER_UUID if slug == "bookstack" else SubModes.HASHED_USER_ID,
"redirect_uris": [
RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT, url=url)
for url in redirects
@@ -60,7 +63,6 @@ PROXY_NAMES = {
"gitea": "homelab-gitea",
"portainer": "homelab-portainer",
"minio": "homelab-minio",
"plane": "homelab-plane",
}
outpost = Outpost.objects.get(name="authentik Embedded Outpost")
for slug in native_slugs:
@@ -1,4 +1,4 @@
"""Update proxy providers for forward_auth-only batch-1 apps (ak shell: exec(open(...).read()))."""
"""Update proxy providers for forward_auth apps (admin batch 1 + media batch 2)."""
from authentik.core.models import Application
from authentik.flows.models import Flow
from authentik.outposts.models import Outpost, OutpostType
@@ -17,12 +17,25 @@ UPTIME_UNAUTH = """^/status/.*
# Proxy-only sites (native OIDC apps managed by setup-batch1-oidc.py).
SITES = [
# Batch 1 — admin
("homelab-code", "code", "code-server", "https://code.ginnoir.com", None, None),
("homelab-registry-ui", "registry-ui", "Registry UI", "https://registry-ui.ginnoir.com", None, None),
("homelab-vault", "vault", "Vault", "https://vault.ginnoir.com", None, None),
("homelab-uptime", "uptime", "Uptime Kuma", "https://uptime.ginnoir.com", "http://uptime-kuma:3001", UPTIME_UNAUTH),
("homelab-backup", "backup", "Kopia", "https://backup.ginnoir.com", None, None),
("homelab-dbx", "dbx", "DBX", "https://dbx.ginnoir.com", None, None),
("homelab-plane", "plane", "Plane", "https://plane.ginnoir.com", None, None),
# Batch 2 — media
("homelab-sonarr", "sonarr", "Sonarr", "https://sonarr.ginnoir.com", None, None),
("homelab-radarr", "radarr", "Radarr", "https://radarr.ginnoir.com", None, None),
("homelab-bazarr", "bazarr", "Bazarr", "https://bazarr.ginnoir.com", None, None),
("homelab-prowlarr", "prowlarr", "Prowlarr", "https://prowlarr.ginnoir.com", None, None),
("homelab-tautulli", "tautulli", "Tautulli", "https://tautulli.ginnoir.com", None, None),
("homelab-qbit", "qbit", "qBittorrent", "https://qbittorrent.ginnoir.com", None, None),
("homelab-deluge", "deluge", "Deluge", "https://deluge.ginnoir.com", None, None),
("homelab-nzbget", "nzbget", "NZBGet", "https://nzbget.ginnoir.com", None, None),
("homelab-whisparr", "whisparr", "Whisparr", "https://whisparr.ginnoir.com", None, None),
("homelab-stash", "stash", "Stash", "https://stash.ginnoir.com", None, None),
]
providers = []
+19 -3
View File
@@ -147,12 +147,11 @@ services:
- DB_PASS=${BOOKSTACK_DB_PASSWORD}
- DB_DATABASE=bookstack
- AUTH_METHOD=oidc
- AUTH_AUTO_INITIATE=true
- AUTH_AUTO_INITIATE=false
- OIDC_NAME=Authentik
- OIDC_CLIENT_ID=${BOOKSTACK_OIDC_CLIENT_ID}
- OIDC_CLIENT_SECRET=${BOOKSTACK_OIDC_CLIENT_SECRET}
- OIDC_ISSUER=https://auth.ginnoir.com/application/o/bookstack/
- OIDC_ISSUER_DISCOVER=true
- OIDC_END_SESSION_ENDPOINT=true
volumes:
- /config/bookstack:/config
ports:
@@ -197,6 +196,23 @@ services:
- NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com
- HOSTNAME=0.0.0.0
plane-admin:
container_name: plane_admin
image: makeplane/plane-admin:v0.23.1
restart: unless-stopped
networks: [dev, edge]
command: node admin/server.js
env_file:
- stack.env
environment:
- NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com
- HOSTNAME=0.0.0.0
depends_on:
plane-api:
condition: service_started
plane-web:
condition: service_started
plane-api:
container_name: plane_api
image: makeplane/plane-backend:latest
+3
View File
@@ -14,6 +14,9 @@ BOOKSTACK_DB_PASSWORD=bookstack_d3v_pass
BOOKSTACK_DB_ROOT_PASSWORD=bookstack_r00t_pass
BOOKSTACK_OIDC_CLIENT_ID=8YV4BI2JVIG1QzS2DbylEgqv56sgfiOVRXOgw2Z5
BOOKSTACK_OIDC_CLIENT_SECRET=wIyiw7LmGBORDyTdc2crSZryXJHzIq4gtajAKSpOU7e8BkBSO6SstM9gGPrnp4tbJhCFSQggYAgtYd5LOjUvQpSWInzm0JLlmLg1BTlPq5EVVieNYuWRCCQrerj8gRZ0
# BookStack reads OIDC_* directly (env_file); BOOKSTACK_* kept for Portainer UI mirror if using ${} in compose.
OIDC_CLIENT_ID=8YV4BI2JVIG1QzS2DbylEgqv56sgfiOVRXOgw2Z5
OIDC_CLIENT_SECRET=wIyiw7LmGBORDyTdc2crSZryXJHzIq4gtajAKSpOU7e8BkBSO6SstM9gGPrnp4tbJhCFSQggYAgtYd5LOjUvQpSWInzm0JLlmLg1BTlPq5EVVieNYuWRCCQrerj8gRZ0
# Plane project management
# Rotate with: openssl rand -hex 32