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
+88 -18
View File
@@ -81,27 +81,57 @@ requests.ginnoir.com {
# ============================================================= # =============================================================
sonarr.ginnoir.com { sonarr.ginnoir.com {
import internal_only import internal_only
reverse_proxy sonarr:8989 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy sonarr:8989
}
}
} }
radarr.ginnoir.com { radarr.ginnoir.com {
import internal_only import internal_only
reverse_proxy radarr:7878 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy radarr:7878
}
}
} }
bazarr.ginnoir.com { bazarr.ginnoir.com {
import internal_only import internal_only
reverse_proxy bazarr:6767 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy bazarr:6767
}
}
} }
prowlarr.ginnoir.com { prowlarr.ginnoir.com {
import internal_only import internal_only
reverse_proxy prowlarr:9696 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy prowlarr:9696
}
}
} }
tautulli.ginnoir.com { tautulli.ginnoir.com {
import internal_only import internal_only
reverse_proxy tautulli:8181 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy tautulli:8181
}
}
} }
# ============================================================= # =============================================================
@@ -109,27 +139,57 @@ tautulli.ginnoir.com {
# ============================================================= # =============================================================
qbittorrent.ginnoir.com { qbittorrent.ginnoir.com {
import internal_only import internal_only
reverse_proxy qbittorrent:3232 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy qbittorrent:3232
}
}
} }
deluge.ginnoir.com { deluge.ginnoir.com {
import internal_only import internal_only
reverse_proxy deluge:8112 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy deluge:8112
}
}
} }
nzbget.ginnoir.com { nzbget.ginnoir.com {
import internal_only import internal_only
reverse_proxy nzbget:6789 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy nzbget:6789
}
}
} }
whisparr.ginnoir.com { whisparr.ginnoir.com {
import internal_only import internal_only
reverse_proxy whisparr:6969 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy whisparr:6969
}
}
} }
stash.ginnoir.com { stash.ginnoir.com {
import internal_only import internal_only
reverse_proxy stash:6970 route {
import authentik_outpost
handle {
import authentik_forward_auth
reverse_proxy stash:6970
}
}
} }
# ============================================================= # =============================================================
@@ -344,14 +404,24 @@ docs.ginnoir.com {
} }
plane.ginnoir.com { plane.ginnoir.com {
handle /api/* { route {
reverse_proxy plane_api:8000 import authentik_outpost
} handle /api/* {
handle /auth/* { import authentik_forward_auth
reverse_proxy plane_api:8000 reverse_proxy plane_api:8000
} }
handle { handle /auth/* {
reverse_proxy plane_web:3000 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
}
} }
} }
+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 #!/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()) setup-batch1-oidc.py instead. Run this via: ak shell -c exec(open(...).read())
""" """
from authentik.core.models import Application from authentik.core.models import Application
@@ -21,12 +21,25 @@ UPTIME_UNAUTH = """^/status/.*
^/upload/.*""" ^/upload/.*"""
SITES = [ SITES = [
# Batch 1 — admin
("homelab-code", "code", "code-server", "https://code.ginnoir.com", None, None), ("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-registry-ui", "registry-ui", "Registry UI", "https://registry-ui.ginnoir.com", None, None),
("homelab-vault", "vault", "Vault", "https://vault.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-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-backup", "backup", "Kopia", "https://backup.ginnoir.com", None, None),
("homelab-dbx", "dbx", "DBX", "https://dbx.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 = [] providers = []
@@ -1,13 +1,15 @@
"""Create OAuth2 providers for batch-1 apps with native SSO (ak shell: exec(open(...).read())).""" """Create OAuth2 providers for batch-1 apps with native SSO (ak shell: exec(open(...).read()))."""
from authentik.core.models import Application from authentik.core.models import Application
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.outposts.models import Outpost 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 from authentik.providers.proxy.models import ProxyProvider
AUTHZ = Flow.objects.get(slug="default-provider-authorization-implicit-consent") AUTHZ = Flow.objects.get(slug="default-provider-authorization-implicit-consent")
INVALID = Flow.objects.get(slug="default-provider-invalidation-flow") INVALID = Flow.objects.get(slug="default-provider-invalidation-flow")
DEFAULT_MAPPINGS = OAuth2Provider.objects.get(name="famapp").property_mappings.all() 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) # slug, provider name, redirect URI(s)
OIDC_APPS = [ OIDC_APPS = [
@@ -15,7 +17,6 @@ OIDC_APPS = [
("gitea", "gitea", ["https://gitea.ginnoir.com/user/oauth2/Authentik/callback"]), ("gitea", "gitea", ["https://gitea.ginnoir.com/user/oauth2/Authentik/callback"]),
("portainer", "portainer", ["https://portainer.ginnoir.com/"]), ("portainer", "portainer", ["https://portainer.ginnoir.com/"]),
("minio", "minio-console", ["https://minio.ginnoir.com/oauth_callback"]), ("minio", "minio-console", ["https://minio.ginnoir.com/oauth_callback"]),
("plane", "plane", ["https://plane.ginnoir.com/auth/oidc/callback/"]),
] ]
native_slugs = [] native_slugs = []
@@ -25,6 +26,8 @@ for slug, pname, redirects in OIDC_APPS:
defaults={ defaults={
"authorization_flow": AUTHZ, "authorization_flow": AUTHZ,
"invalidation_flow": INVALID, "invalidation_flow": INVALID,
"signing_key": SIGNING_KEY,
"sub_mode": SubModes.USER_UUID if slug == "bookstack" else SubModes.HASHED_USER_ID,
"redirect_uris": [ "redirect_uris": [
RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT, url=url) RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT, url=url)
for url in redirects for url in redirects
@@ -60,7 +63,6 @@ PROXY_NAMES = {
"gitea": "homelab-gitea", "gitea": "homelab-gitea",
"portainer": "homelab-portainer", "portainer": "homelab-portainer",
"minio": "homelab-minio", "minio": "homelab-minio",
"plane": "homelab-plane",
} }
outpost = Outpost.objects.get(name="authentik Embedded Outpost") outpost = Outpost.objects.get(name="authentik Embedded Outpost")
for slug in native_slugs: 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.core.models import Application
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.outposts.models import Outpost, OutpostType 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). # Proxy-only sites (native OIDC apps managed by setup-batch1-oidc.py).
SITES = [ SITES = [
# Batch 1 — admin
("homelab-code", "code", "code-server", "https://code.ginnoir.com", None, None), ("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-registry-ui", "registry-ui", "Registry UI", "https://registry-ui.ginnoir.com", None, None),
("homelab-vault", "vault", "Vault", "https://vault.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-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-backup", "backup", "Kopia", "https://backup.ginnoir.com", None, None),
("homelab-dbx", "dbx", "DBX", "https://dbx.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 = [] providers = []
+19 -3
View File
@@ -147,12 +147,11 @@ services:
- DB_PASS=${BOOKSTACK_DB_PASSWORD} - DB_PASS=${BOOKSTACK_DB_PASSWORD}
- DB_DATABASE=bookstack - DB_DATABASE=bookstack
- AUTH_METHOD=oidc - AUTH_METHOD=oidc
- AUTH_AUTO_INITIATE=true - AUTH_AUTO_INITIATE=false
- OIDC_NAME=Authentik - 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=https://auth.ginnoir.com/application/o/bookstack/
- OIDC_ISSUER_DISCOVER=true - OIDC_ISSUER_DISCOVER=true
- OIDC_END_SESSION_ENDPOINT=true
volumes: volumes:
- /config/bookstack:/config - /config/bookstack:/config
ports: ports:
@@ -197,6 +196,23 @@ services:
- NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com - NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com
- HOSTNAME=0.0.0.0 - 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: plane-api:
container_name: plane_api container_name: plane_api
image: makeplane/plane-backend:latest 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_DB_ROOT_PASSWORD=bookstack_r00t_pass
BOOKSTACK_OIDC_CLIENT_ID=8YV4BI2JVIG1QzS2DbylEgqv56sgfiOVRXOgw2Z5 BOOKSTACK_OIDC_CLIENT_ID=8YV4BI2JVIG1QzS2DbylEgqv56sgfiOVRXOgw2Z5
BOOKSTACK_OIDC_CLIENT_SECRET=wIyiw7LmGBORDyTdc2crSZryXJHzIq4gtajAKSpOU7e8BkBSO6SstM9gGPrnp4tbJhCFSQggYAgtYd5LOjUvQpSWInzm0JLlmLg1BTlPq5EVVieNYuWRCCQrerj8gRZ0 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 # Plane project management
# Rotate with: openssl rand -hex 32 # Rotate with: openssl rand -hex 32