docs: Jellyfin SSO, auth architecture, fix tree rendering

This commit is contained in:
Hermes
2026-05-15 09:52:38 -04:00
parent 66422a9283
commit 5f128963d3
2 changed files with 160 additions and 2 deletions

View File

@@ -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
<KnownProxies>
<string>172.28.10.0/24</string>
<string>172.28.10.4</string>
</KnownProxies>
#+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
<?xml version="1.0" encoding="utf-8"?>
<NetworkConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<BaseUrl />
<EnableHttps>false</EnableHttps>
<RequireHttps>false</RequireHttps>
<CertificatePath />
<CertificatePassword />
<InternalHttpPort>8096</InternalHttpPort>
<InternalHttpsPort>8920</InternalHttpsPort>
<PublicHttpPort>8096</PublicHttpPort>
<PublicHttpsPort>8920</PublicHttpsPort>
<AutoDiscovery>true</AutoDiscovery>
<EnableUPnP>true</EnableUPnP>
<EnableIPv4>true</EnableIPv4>
<EnableIPv6>false</EnableIPv6>
<EnableRemoteAccess>true</EnableRemoteAccess>
<LocalNetworkSubnets />
<LocalNetworkAddresses />
<KnownProxies>
<string>172.28.10.0/24</string>
<string>172.28.10.4</string>
</KnownProxies>
<IgnoreVirtualInterfaces>true</IgnoreVirtualInterfaces>
<VirtualInterfaceNames>
<string>veth</string>
</VirtualInterfaceNames>
<EnablePublishedServerUriByRequest>false</EnablePublishedServerUriByRequest>
<PublishedServerUriBySubnet />
<RemoteIPFilter />
<IsRemoteIPFilterBlacklist>false</IsRemoteIPFilterBlacklist>
</NetworkConfiguration>
#+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

34
tangle-deploy.sh Normal file
View File

@@ -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 ==='