docs: Jellyfin SSO, auth architecture, fix tree rendering
This commit is contained in:
@@ -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
34
tangle-deploy.sh
Normal 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 ==='
|
||||
Reference in New Issue
Block a user