diff --git a/infrastructure.org b/infrastructure.org index 1dfba6a..e02b32c 100644 --- a/infrastructure.org +++ b/infrastructure.org @@ -16,6 +16,7 @@ ** External Access Architecture +#+BEGIN_EXAMPLE Cloudflare (edge, orange cloud) └─ Cloudflare Tunnel "home" (cloudflared on production-1) └─ Traefik (entrypoint=tunnel, port 8081) @@ -24,9 +25,11 @@ Cloudflare (edge, orange cloud) ├─ jellyfin (SSO via plugin + OIDC) ├─ gitea (native OIDC) └─ *.gharbeia.net services +#+END_EXAMPLE ** Internal Access Architecture +#+BEGIN_EXAMPLE LAN client (browser) └─ Traefik (entrypoint=secureweb, port 443) ├─ Authentik Forward Auth (internal.yaml routers) @@ -36,7 +39,8 @@ LAN client (browser) Service-to-service / automation / cross-VLAN └─ Traefik (entrypoint=internal, port 8083 — NO auth) - └─ Same routing as secureweb, from =traefik-internal-noauth.yaml= + └─ Same routing as secureweb, from traefik-internal-noauth.yaml +#+END_EXAMPLE Key distinction: =:443= = browsers/humans with Authentik auth. =:8083= = runners, automated tooling, services on other VLANs. @@ -736,7 +740,57 @@ Generated by stripping the auth middleware from =traefik-internal.yaml=. # See: docker/appdata/traefik/internal-noauth.yaml for the production copy. #+END_SRC -* Docker Services +** Authentication Architecture + +Three authentication mechanisms depending on the service type: + +*** Forward Auth (default for web-only services) + +Traefik middleware intercepts every request and redirects unauthenticated +users to the Authentik login page. After login, Authentik sets a session +cookie that passes subsequent checks transparently. + +Used by: all =*arr=, dashboards, monitoring, Portainer, Guacamole, etc. +Limitation: only works in browsers — native/TV apps can't use Forward Auth. + +*** Native OIDC / SSO (for services with apps) + +Services that have native mobile or TV apps need real OIDC integration so +the app can authenticate directly via a browser-based login flow. + +- Gitea: configured with Authentik OIDC provider in Gitea's admin panel. + Users log in via "Sign in with Authentik" button on the Gitea login page. + Existing user accounts are linked by username match. + +- Jellyfin: uses the SSO-Auth plugin (v4.0.0.4) with an Authentik OIDC + provider (client_id = =jellyfin-sso=). The plugin does a two-step flow: + 1. OIDC callback returns an HTML page with JavaScript + authorization state + 2. JavaScript POSTs to =/sso/OID/Auth/Authentik= to complete the login + + Critical detail: Jellyfin must trust Traefik's =X-Forwarded-Proto= header + or the JavaScript will construct URLs with =http://= instead of =https://=. + This is configured via =KnownProxies= in =/config/network.xml=: + + #+BEGIN_SRC xml + + 172.28.10.0/24 + 172.28.10.4 + + #+END_SRC + + Without this, =GetRequestBase()= returns =http://jellyfin.gharbeia.net= and + the iframe, auth POST, and final redirect all use the wrong scheme. + +*** Local users (TV apps, fallback) + +TV apps (Android TV, webOS, Tizen) often can't complete the OIDC JavaScript +two-step flow inside their embedded browser. For these, create a dedicated +Jellyfin local user (e.g. =tv=) with a simple password. The app logs in +directly with password — no SSO involved. The user keeps library access +without losing admin history/settings. + +This pattern applies to any service where native app SSO doesn't work: +create a local service account, use it for app access, keep SSO for browsers. The entire stack runs on =production-1= using Docker Compose. Services are split into individual YAML fragment files under =/docker/compose/services/=, @@ -814,6 +868,65 @@ All 43 services are organized alphabetically by category in the include list. The order matters for startup dependencies: infrastructure services (gluetun, postgresql, valkey, authentik, traefik) come first. +** Jellyfin — Media Server + +Jellyfin serves media libraries through the browser and native apps. It runs +in Gluetun's network namespace (VPN-routed), uses Authentik SSO for browser +login, and supports local user accounts for TV apps. + +*** KnownProxies (Critical for SSO) + +Jellyfin sits behind Traefik reverse proxy. Without =KnownProxies=, Jellyfin +doesn't trust =X-Forwarded-Proto: https=, so the SSO plugin's JavaScript +flow constructs URLs with HTTP instead of HTTPS. This file must match the +runtime config at =/docker/appdata/jellyfin/network.xml=. + +#+BEGIN_SRC xml :tangle /docker/appdata/jellyfin/network.xml + + + + false + false + + + 8096 + 8920 + 8096 + 8920 + true + true + true + false + true + + + + 172.28.10.0/24 + 172.28.10.4 + + true + + veth + + false + + + false + +#+END_SRC + +*** SSO-Auth Plugin Configuration + +The SSO plugin config lives at =/docker/appdata/jellyfin/plugins/configurations/SSO-Auth.xml=. +Key settings: +- OIDC provider pointing to =https://auth.gharbeia.net/application/o/jellyfin-sso= +- =EnableAuthorization=false= (bypasses group-based role checking) +- =EnableAllFolders=true= (all libraries accessible to SSO users) +- Scopes: =openid profile email groups= + +With =EnableAuthorization=false=, any Authentik user can log in to Jellyfin +via SSO. Admin rights are managed within Jellyfin itself. + ** Gluetun — VPN Client Gluetun is the VPN gateway for all media-related traffic. Services that need @@ -1111,6 +1224,17 @@ Key variables: * LOGBOOK +** [2026-05-15 Thu 09:30] Jellyfin SSO fixed — KnownProxies and Two-Step Flow +- Root cause: Jellyfin's empty KnownProxies caused SSO plugin to use HTTP + base URL, breaking the JavaScript two-step auth flow (iframe/POST/redirect) +- Fix: Added 172.28.10.0/24 and 172.28.10.4 to Jellyfin's KnownProxies +- Created jellyfin_admin Authentik group + linked user amr to it +- Set EnableAuthorization=false in SSO-Auth plugin config +- Documented three authentication mechanisms: Forward Auth, OIDC/SSO, local users +- Fixed ASCII tree diagrams by wrapping in #+BEGIN_EXAMPLE blocks +- Fixed trees rendering issue: Unicode box-drawing chars must be inside + example blocks in org-mode, otherwise font rendering mangles them + ** [2026-05-15 Thu 06:40] Pipeline fixed — Emacs path and auth - Fixed Emacs org-loaddefs.el path in tangle-deploy - Created Gitea access token for git operations diff --git a/tangle-deploy.sh b/tangle-deploy.sh new file mode 100644 index 0000000..d46ec96 --- /dev/null +++ b/tangle-deploy.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# tangle-deploy — Tangle infrastructure.org and restart affected services +GITEA_URL='http://amr:tangle-deploy-2026@10.10.10.201:3001/amr/infrastructure.git' +REPO_DIR="${1:-/docker/compose/infrastructure}" +ORG_FILE="${REPO_DIR}/infrastructure.org" +if [ -z "${1:-}" ]; then + if [ ! -d "$REPO_DIR" ]; then + git clone "$GITEA_URL" "$REPO_DIR" + else + cd "$REPO_DIR" && git pull + fi +fi +if [ ! -f "$ORG_FILE" ]; then + echo "ERROR: $ORG_FILE not found in $REPO_DIR" + exit 1 +fi +echo "=== Tangling $ORG_FILE ===" +emacs --batch -Q --load /usr/share/emacs/28.2/lisp/org/org-loaddefs.el \ + --eval "(require 'org)" \ + --eval "(org-babel-tangle-file \"$ORG_FILE\")" 2>&1 +echo "=== Restarting services ===" +cd /docker/compose +if [ -f /docker/compose/traefik-static.yaml ] || \ + [ -f /docker/compose/traefik-internal.yaml ] || \ + [ -f /docker/compose/traefik-internal-noauth.yaml ] || \ + [ -f /docker/compose/traefik-dynamic.yaml ]; then + echo 'Traefik config changed -- restarting...' + docker compose up -d traefik +fi +if [ -f /docker/compose/docker-compose.yaml ]; then + echo 'Docker compose changed -- restarting all services' + docker compose up -d 2>&1 | tail -5 +fi +echo '=== Deploy complete ==='