-
Notifications
You must be signed in to change notification settings - Fork 1
feat(sso): SSO forward-auth gateway — auth once, access all (Phase 1: service + edge) #2221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
281a7da
1706949
82c41ca
f5300da
a68a0d3
6d65c1b
2bc407f
9c070b1
8c0e725
f1f10f2
e81baa3
c621396
eb3fbe2
aa96666
9824a54
0d91d80
8e82e95
c4cdba7
811d87a
f6a5539
a4b71bb
2a20ca6
9ef3061
819db40
d8b4ef5
35b5ec8
f8f20d3
def4bb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| # PMOVES SSO Gateway — Design Spec | ||
|
|
||
| **Date:** 2026-07-24 | ||
| **Goal:** A user who authenticates once against Supabase GoTrue reaches wger (Health), Firefly III (Wealth), Open-Notebook, and Jellyfin **already logged in** — "auth once, access all." Supabase stays the single identity provider; the Supabase `JWT_SECRET` and BoTZ's shipped `validate_jwt` pattern are reused. | ||
|
|
||
| **Approved decisions (brainstorm 2026-07-24):** | ||
| - Login: **GitHub OAuth (primary) + email/password (fallback)**, both via GoTrue. | ||
| - Validator: **custom FastAPI forward-auth service** (`pmoves-sso-auth`), reusing BoTZ's `validate_jwt`. | ||
| - Reverse proxy: **Traefik** with the ForwardAuth middleware. | ||
| - Routing: **subdomains** (`health/wealth/notebook/media.pmoves.ai`); session cookie scoped to `.pmoves.ai`. | ||
| - Scope: **all 4 apps in Phase 1** (Jellyfin included via `jellyfin-plugin-sso` against minimal OIDC endpoints on the auth service). | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| Browser ──HTTPS──> Traefik (edge, pmoves_external, TLS) | ||
| │ Host: {health,wealth,notebook,media}.pmoves.ai | ||
| ▼ | ||
| ForwardAuth middleware ──subrequest──> pmoves-sso-auth (FastAPI) GET /auth/verify | ||
| • no/invalid pmoves_session cookie → 302 https://auth.pmoves.ai/login?rd=<orig> | ||
| • valid cookie → 200 + headers: Remote-User, X-Auth-Email, X-Auth-Subject | ||
| ▼ (on 200, Traefik forwards to the app with those headers injected) | ||
| App (wger | firefly | open-notebook | jellyfin) trusts the injected identity | ||
| ``` | ||
|
|
||
| Login/session issuance (also on `auth.pmoves.ai`, same service): | ||
| ``` | ||
| GET /login → page: "Sign in with GitHub" + email/password form | ||
| GitHub → 302 GoTrue /authorize?provider=github&redirect_to=/callback | ||
| email/pw→ POST /login → GoTrue /auth/v1/token?grant_type=password | ||
| GET /callback → exchange GoTrue code/token → Set-Cookie pmoves_session=<supabase access_jwt> | ||
| (HttpOnly, Secure, SameSite=Lax, Domain=.pmoves.ai) → 302 back to rd | ||
| GET /logout → clear cookie → 302 GoTrue logout | ||
| ``` | ||
|
|
||
| ## Components | ||
|
|
||
| ### 1. `pmoves-sso-auth` (new service — `pmoves/services/sso-auth/`) | ||
| FastAPI app. Responsibilities and endpoints: | ||
| - **`GET /auth/verify`** — Traefik ForwardAuth target. Reads `pmoves_session` cookie, runs `validate_jwt(cookie, SUPABASE_JWT_SECRET, alg=HS256)` (lifted from `PMOVES-BoTZ/features/mcp_bridge/auth.py`). On success → `200` with response headers `Remote-User` (GoTrue `email` or `sub`), `X-Auth-Email` (email), `X-Auth-Subject` (`sub`). On failure/absent → `401` (Traefik converts to the configured redirect). Near-zero latency; no network calls on the hot path (HS256 local verify). | ||
| - **`GET /login`** — renders login page (GitHub button + email/pw form), carries `rd` (return destination). | ||
| - **`POST /login`** — email/pw → GoTrue `/auth/v1/token?grant_type=password` → set cookie → redirect `rd`. | ||
| - **`GET /callback`** — GoTrue OAuth callback (GitHub) → obtain session → set cookie → redirect `rd`. | ||
| - **`GET /logout`** — clear cookie, GoTrue signout. | ||
| - **`GET /healthz`** — liveness. | ||
| - **OIDC-for-Jellyfin subset** (only consumed by `jellyfin-plugin-sso`): `GET /.well-known/openid-configuration`, `GET /oidc/authorize` (reuses the same GoTrue login → issues an OIDC `code`), `POST /oidc/token` (returns an `id_token` minted from the validated Supabase identity), `GET /oidc/userinfo`. This is a *thin adapter over the already-validated session*, not a general OIDC provider — scoped to Jellyfin's client_id only. | ||
|
|
||
| Config (env, from the pipeline): `SUPABASE_JWT_SECRET` (alias of `JWT_SECRET`), `GOTRUE_URL` (internal `http://supabase-gotrue:9999` / external `https://supabase.pmoves.ai`), `SSO_COOKIE_DOMAIN=.pmoves.ai`, `SSO_SESSION_TTL`, `JELLYFIN_OIDC_CLIENT_ID`/`_SECRET`. No new secrets minted — reuses `JWT_SECRET`. | ||
|
|
||
| ### 2. Traefik (`pmoves-traefik` — `pmoves/docker-compose.traefik.yml`) | ||
| - Edge on `pmoves_external`; entrypoints `:80`→`:443` redirect. TLS via **Cloudflare DNS-01** for `*.pmoves.ai` (Cloudflare is the existing DNS per the fleet setup); mesh-internal access (Tailscale/Headscale hostnames) is the reachability path, DNS-01 is the cert path. | ||
| - One router per app (Host rule → app service), all attached to the shared `forward-auth@docker` middleware pointing at `pmoves-sso-auth:8080/auth/verify`, `authResponseHeaders: Remote-User,X-Auth-Email,X-Auth-Subject`. | ||
| - `auth.pmoves.ai` router → `pmoves-sso-auth` **without** the ForwardAuth middleware (login must be reachable unauthenticated). | ||
| - Apps drop their host `ports:` — reachable only through Traefik. | ||
|
|
||
| ### 3. Per-app integration | ||
| | App | Mechanism | Change | | ||
| |---|---|---| | ||
| | **firefly** (Laravel) | `AUTHENTICATION_GUARD=remote_user_guard`, `AUTHENTICATION_GUARD_HEADER=Remote-User`, `AUTHENTICATION_GUARD_EMAIL=X-Auth-Email` | compose env only; auto-provisions the user on first header-auth. **Firefly has NO native OIDC** (open FR #10662) — remote-user IS the documented SSO path. **Disable Firefly MFA** when remote-user is on. | | ||
| | **wger** (Django) | `RemoteUserMiddleware` in `MIDDLEWARE`, `REMOTE_USER` trust; `WGER_ALLOW_REMOTE_USER=True` in the PMOVES wger fork | fork config + compose env | | ||
| | **open-notebook** (Streamlit fork) | replace `PasswordAuthMiddleware` with `RemoteUserMiddleware` in `PMOVES-Open-Notebook/api/auth.py` — trust `Remote-User` (fallback to the existing token for non-proxied access) | fork code. **Confirmed password-only, no OIDC/SSO upstream** — header-trust is the only option. | | ||
| | **jellyfin** | install an OIDC SSO plugin, configured against the auth service's OIDC subset; auto-provision Jellyfin users from the Supabase `sub`/email | plugin baked into the PMOVES jellyfin fork image at a PINNED version. **Primary: `Ezeqielle/jellyfin-plugin-oidc`** (actively maintained — v1.0.8 2026-07-15, pushed 2026-07-23; generic-OIDC). **Fallback: `9p4/jellyfin-plugin-sso`** (archived 2026-05-12 but battle-tested, 1461★, generic-OIDC, ≥10.8) if Ezeqielle proves too thin. Jellyfin has NO native OIDC (open FR #16470) and no trusted-header auth, so an OIDC plugin is the only path. **Caveat:** OIDC plugins cover the **web UI only** — Jellyfin *mobile apps* still use native login (out of scope for browser-seamless Phase 1). | | ||
|
|
||
| **Current-docs validation (2026-07-24):** the header-based forward-auth is the *correct common denominator*, not a shortcut — Firefly III and Open-Notebook have **no OIDC at all**, so trusted-header is their only SSO; wger uses Django's `RemoteUserMiddleware`; only Jellyfin requires OIDC (hence the auth-service OIDC subset). No simpler unified solution exists given this heterogeneity. Sources: Firefly III authentication docs + FRs #10662/#6514; open-notebook README (password-only); jellyfin-plugin-sso (archived, generic-OIDC). | ||
|
|
||
| Only the reverse proxy is trusted to set `Remote-User`; apps must reject the header on any path not fronted by Traefik (defence-in-depth — bind app ports to the Traefik network only). | ||
|
|
||
| ## Data flow — session lifecycle | ||
| 1. Unauthenticated request to `health.pmoves.ai` → Traefik ForwardAuth → `/auth/verify` 401 → 302 to `auth.pmoves.ai/login?rd=health.pmoves.ai`. | ||
| 2. User signs in (GitHub or email/pw) → GoTrue issues Supabase JWT → cookie `pmoves_session` set on `.pmoves.ai`. | ||
| 3. Redirect back → ForwardAuth now `200` + `Remote-User` → app auto-logs-in. | ||
| 4. Same cookie satisfies `wealth/notebook/media.pmoves.ai` — no re-login (that IS the SSO). | ||
| 5. JWT ~1h; **Phase 1:** on expiry `/auth/verify` returns 401 → transparent re-login (GoTrue session is still valid, so the redirect round-trip is invisible when the GoTrue cookie persists). **Enhancement (out of Phase 1):** silent refresh via a second HttpOnly `pmoves_refresh` cookie holding the GoTrue refresh token, exchanged in `/auth/verify` before the access JWT expires. | ||
|
|
||
| ## Error handling | ||
| - Expired/invalid JWT → 401 → redirect to login (never a 500). | ||
| - GoTrue unreachable → login page shows a clear error; `/auth/verify` fails-closed (401) so apps stay protected. | ||
| - App up but header missing (misconfig) → app denies; Traefik logs the missing `Remote-User`. | ||
| - Jellyfin OIDC failure → falls back to Jellyfin's native login (not locked out). | ||
|
|
||
| ## Security | ||
| - Cookie: HttpOnly, Secure, SameSite=Lax, `Domain=.pmoves.ai`. | ||
| - Apps trust `Remote-User` **only** from Traefik — app containers do not publish host ports; only Traefik is on their network edge. | ||
| - Reuse the single `SUPABASE_JWT_SECRET`; no new long-lived secret. HS256 today; the asymmetric-JWT (JWKS) migration (separate design) later swaps the verify path only. | ||
| - Forward-auth is fail-closed. | ||
|
|
||
| ## Testing | ||
| - Unit: `/auth/verify` with valid / expired / tampered / absent cookie → 200+headers / 401. | ||
| - Integration: login (both methods) sets a valid cookie; a second app request with the cookie returns 200 without re-login. | ||
| - Per-app: header-auth auto-provisions + logs in the user (firefly, wger, open-notebook); Jellyfin OIDC round-trip creates + logs in the user. | ||
| - Negative: direct app access bypassing Traefik with a forged `Remote-User` is rejected. | ||
|
|
||
| ## Scope / phasing | ||
| **Phase 1 (this spec):** Traefik + `pmoves-sso-auth` + all 4 apps (wger, firefly, open-notebook header-auth; jellyfin via OIDC subset). Delivers true one-login-all-apps. | ||
| **Out of scope (separate specs):** asymmetric JWKS migration; 2FA; adding further apps; the firefly tmpfs-hardening 500 fix (tracked separately, unblocks firefly serving at all). | ||
|
|
||
| ## Global constraints | ||
| - Supabase GoTrue is the ONLY IdP; reuse `JWT_SECRET`/`SUPABASE_JWT_SECRET` (no new IdP, no separate user store). | ||
| - Reuse BoTZ's `validate_jwt` (HS256) — do not reimplement JWT verification. | ||
| - Apps are PMOVES forks — prefer config/env over code; code changes only where no header-auth config exists (open-notebook middleware). | ||
| - Follow the env pipeline (example → manifest → funnel); never inline secrets. | ||
| - Compose/Dockerfile/migration edits go through their Known-Road domains. | ||
| - Prerequisite: firefly must actually serve (its tmpfs 500 fix) before its SSO can be validated end-to-end. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| http: | ||
| middlewares: | ||
| pmoves-forward-auth: | ||
| forwardAuth: | ||
| address: "http://pmoves-sso-auth:8080/auth/verify" | ||
| authResponseHeaders: ["Remote-User", "X-Auth-Email", "X-Auth-Subject"] | ||
| # On 401 Traefik returns the auth service's response; a 401 body/redirect | ||
| # sends the browser to auth.pmoves.ai/login (see errors middleware below). | ||
| pmoves-auth-redirect: | ||
| errors: | ||
| status: ["401"] | ||
| service: sso-auth@docker | ||
| query: "/login?rd=https://{host}{uri}" | ||
|
Comment on lines
+9
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When forward-auth returns 401 for Health, Wealth, or Notebook, Traefik's errors middleware fetches this query internally and substitutes the response; it does not navigate the browser to Useful? React with 👍 / 👎. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,7 +191,12 @@ services: | |
| restart: unless-stopped | ||
| environment: | ||
| # SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY from env.tier-agent | ||
| # NATS_URL from env.tier-agent has credentials: nats://nats:***@nats:4222 | ||
| # NATS_URL pinned to the in-network broker (documented default creds — | ||
| # feedback_nats_creds_convention). Two prior sources delivered a | ||
| # HOST-shaped value (localhost) into the container: env.tier-agent after | ||
| # a prod-bundle materialization, and a duplicate ${NATS_URL} entry below | ||
| # (last list entry wins). Broke archon's broker connect 2026-07-24. | ||
| - NATS_URL=${ARCHON_BUS_NATS_URL:-nats://nats:pmoves@nats:4222} | ||
|
Comment on lines
+194
to
+199
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Remove the committed NATS credential fallback. Line 199 hardcodes As per path instructions, committed Compose configuration must keep secrets out of configuration files. Based on learnings, agent NATS settings should use the established environment/secret wiring rather than a credentialed default. 🤖 Prompt for AI AgentsSources: Path instructions, Learnings |
||
| - PORT=8091 | ||
| - ENVIRONMENT=${ENVIRONMENT:-production} | ||
| - ARCHON_ENV=${ARCHON_ENV:-production} | ||
|
|
@@ -209,7 +214,10 @@ services: | |
| - SUPABASE_REST_URL=${ARCHON_SUPABASE_REST_URL:-${SUPABASE_REST_URL:-http://supabase-kong:8000/rest/v1}} | ||
| - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY:-${SERVICE_ROLE_KEY:-${SUPABASE_SECRET_KEY:-}}} | ||
| - SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY:-${SUPABASE_SERVICE_ROLE_KEY:-${SERVICE_ROLE_KEY:-${SUPABASE_SECRET_KEY:-}}}} | ||
| - NATS_URL=${NATS_URL} | ||
| # (duplicate NATS_URL removed 2026-07-24: ${NATS_URL} interpolates the | ||
| # HOST-shaped project value (localhost) into the container — last list | ||
| # entry wins, silently overriding the in-network pin above. Same class | ||
| # as the nats-init localhost bug.) | ||
| - SUPA_REST_URL=${ARCHON_SUPABASE_REST_URL:-${SUPABASE_REST_URL:-http://supabase-kong:8000/rest/v1}} | ||
| # TensorZero routing — _sync_openai_compat_env() in services/archon/main.py | ||
| # reads TENSORZERO_BASE_URL and populates OPENAI_COMPATIBLE_BASE_URL* vars | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,10 @@ services: | |
| DJANGO_CLEAR_STATIC_FIRST: "${WGER_CLEAR_STATIC_FIRST:-True}" | ||
| TIME_ZONE: "${WGER_TIME_ZONE:-America/New_York}" | ||
| SITE_URL: "${WGER_SITE_URL:-http://localhost:8010}" | ||
| # SSO gateway: trust the Remote-User header injected by Traefik forward-auth. | ||
| # wger's nginx front (wger-nginx) must proxy_set_header Remote-User upstream. | ||
| WGER_ALLOW_REMOTE_USER: "True" | ||
| WGER_REMOTE_USER_HEADER: "Remote-User" | ||
| volumes: | ||
| - wger-static:/home/wger/static | ||
| - wger-media:/home/wger/media | ||
|
|
@@ -30,8 +34,16 @@ services: | |
| aliases: | ||
| - pmoves-wger-nginx | ||
| pmoves_external: {} | ||
| ports: | ||
| - "${WGER_HOST_PORT:-8010}:80" | ||
| # No host `ports:` — reachable only through Traefik (health.pmoves.ai), so the | ||
| # forward-auth Remote-User header cannot be spoofed by bypassing the proxy. | ||
|
Comment on lines
+37
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The supported AGENTS.md reference: pmoves/AGENTS.md:L169-L180 Useful? React with 👍 / 👎. |
||
| labels: | ||
| - traefik.enable=true | ||
| - traefik.docker.network=pmoves_external | ||
| - traefik.http.routers.health.rule=Host(`health.pmoves.ai`) | ||
| - traefik.http.routers.health.entrypoints=websecure | ||
| - traefik.http.routers.health.tls.certresolver=cf | ||
| - traefik.http.routers.health.middlewares=pmoves-forward-auth@file,pmoves-auth-redirect@file | ||
| - traefik.http.services.health.loadbalancer.server.port=80 | ||
| volumes: | ||
| - wger-static:/wger/static:ro | ||
| - wger-media:/wger/media:ro | ||
|
|
@@ -56,8 +68,22 @@ services: | |
| TZ: ${FIREFLY_TZ:-America/New_York} | ||
| # Neutralise Windows SSL_CERT_FILE leak | ||
| SSL_CERT_FILE: "" | ||
| ports: | ||
| - "${FIREFLY_PORT:-8075}:8080" # Default 8075; 8082 reserved for media-audio in docker-compose.yml | ||
| # SSO gateway: Firefly III has NO native OIDC (FR #10662) — remote_user_guard | ||
| # IS the documented SSO path. Auto-provisions the X-Auth-Email user on first | ||
| # header-auth. DISABLE_FRAME_HEADER lets it render behind the proxy. | ||
| AUTHENTICATION_GUARD: remote_user_guard | ||
| AUTHENTICATION_GUARD_HEADER: Remote-User | ||
| AUTHENTICATION_GUARD_EMAIL: X-Auth-Email | ||
| DISABLE_FRAME_HEADER: "true" | ||
| # No host `ports:` — reachable only through Traefik (wealth.pmoves.ai). | ||
| labels: | ||
| - traefik.enable=true | ||
| - traefik.docker.network=pmoves_external | ||
| - traefik.http.routers.wealth.rule=Host(`wealth.pmoves.ai`) | ||
| - traefik.http.routers.wealth.entrypoints=websecure | ||
| - traefik.http.routers.wealth.tls.certresolver=cf | ||
| - traefik.http.routers.wealth.middlewares=pmoves-forward-auth@file,pmoves-auth-redirect@file | ||
| - traefik.http.services.wealth.loadbalancer.server.port=8080 | ||
| volumes: | ||
| - firefly-storage:/var/www/html/storage | ||
| - ./integrations/pr-kits/firefly/nginx-buffers.conf:/etc/nginx/conf.d/zz_buffers.conf:ro | ||
|
|
@@ -146,9 +172,17 @@ services: | |
| SURREAL_PASS: ${OPEN_NOTEBOOK_SURREAL_PASS:-${SURREAL_PASS:?Set OPEN_NOTEBOOK_SURREAL_PASS or SURREAL_PASS}} | ||
| SURREAL_NAMESPACE: ${OPEN_NOTEBOOK_SURREAL_NAMESPACE:-open-notebook} | ||
| SURREAL_DATABASE: ${OPEN_NOTEBOOK_SURREAL_DATABASE:-open-notebook} | ||
| ports: | ||
| - "${OPEN_NOTEBOOK_UI_PORT:-8503}:8502" | ||
| - "${OPEN_NOTEBOOK_API_PORT:-5055}:5055" | ||
| # No host `ports:` — reachable only through Traefik (notebook.pmoves.ai). The | ||
| # UI (8502) is fronted; the API (5055) is internal (UI calls it on localhost). | ||
| # RemoteUserMiddleware in the fork (Task 8) trusts the injected Remote-User. | ||
| labels: | ||
| - traefik.enable=true | ||
| - traefik.docker.network=pmoves_external | ||
| - traefik.http.routers.notebook.rule=Host(`notebook.pmoves.ai`) | ||
| - traefik.http.routers.notebook.entrypoints=websecure | ||
| - traefik.http.routers.notebook.tls.certresolver=cf | ||
| - traefik.http.routers.notebook.middlewares=pmoves-forward-auth@file,pmoves-auth-redirect@file | ||
| - traefik.http.services.notebook.loadbalancer.server.port=8502 | ||
|
|
||
| jellyfin-ext: | ||
| image: ${JELLYFIN_IMAGE:-ghcr.io/powerfulmoves/pmoves-jellyfin:pmoves-latest} | ||
|
|
@@ -165,8 +199,16 @@ services: | |
| environment: | ||
| JELLYFIN_PublishedServerUrl: ${JELLYFIN_PUBLISHED_URL:-http://localhost:8096} | ||
| JELLYFIN_MEDIA_ROOT: ${JELLYFIN_MEDIA_ROOT:-/media} | ||
| ports: | ||
| - "8096:8096" | ||
| # No host `ports:` — reachable only through Traefik (media.pmoves.ai). NOTE: | ||
| # NO forward-auth middleware here — Jellyfin's OIDC plugin (Task 9) handles | ||
| # auth itself; the plugin's web UI must reach /oidc/* unauthenticated. | ||
| labels: | ||
| - traefik.enable=true | ||
| - traefik.docker.network=pmoves_external | ||
| - traefik.http.routers.media.rule=Host(`media.pmoves.ai`) | ||
| - traefik.http.routers.media.entrypoints=websecure | ||
| - traefik.http.routers.media.tls.certresolver=cf | ||
| - traefik.http.services.media.loadbalancer.server.port=8096 | ||
| volumes: | ||
| - ./data/jellyfin/config:/config | ||
| - ./data/jellyfin/cache:/cache | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| services: | ||
| sso-auth: | ||
| build: { context: ., dockerfile: services/sso-auth/Dockerfile } | ||
| image: ${SSO_AUTH_IMAGE:-pmoves/sso-auth:local} | ||
| container_name: pmoves-sso-auth | ||
| restart: unless-stopped | ||
| environment: | ||
| SUPABASE_JWT_SECRET: ${SUPABASE_JWT_SECRET:-${JWT_SECRET}} | ||
| GOTRUE_URL: ${GOTRUE_URL:-http://supabase-gotrue:9999} | ||
| PUBLIC_BASE_URL: ${SSO_PUBLIC_BASE_URL:-https://auth.pmoves.ai} | ||
| SSO_COOKIE_DOMAIN: ${SSO_COOKIE_DOMAIN:-.pmoves.ai} | ||
| JELLYFIN_OIDC_CLIENT_ID: ${JELLYFIN_OIDC_CLIENT_ID} | ||
| JELLYFIN_OIDC_CLIENT_SECRET: ${JELLYFIN_OIDC_CLIENT_SECRET} | ||
| JELLYFIN_OIDC_REDIRECT_URIS: ${JELLYFIN_OIDC_REDIRECT_URIS:-https://media.pmoves.ai/sso/OID/redirect/pmoves} | ||
| networks: { pmoves_app: { aliases: [pmoves-sso-auth] }, pmoves_external: {} } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With the default Useful? React with 👍 / 👎. |
||
| security_opt: [no-new-privileges:true] | ||
| cap_drop: [ALL] | ||
| # Traefik MUST see this container to resolve `sso-auth@docker` — the service | ||
| # the auth.pmoves.ai router (defined on the traefik container) points at. | ||
| # Under exposedbydefault=false a container is invisible without | ||
| # traefik.enable=true, and dual-homed it needs the explicit network. Without | ||
| # these the login page never registers and SSO breaks at the front door. | ||
| labels: | ||
| - traefik.enable=true | ||
| - traefik.docker.network=pmoves_external | ||
| - traefik.http.services.sso-auth.loadbalancer.server.port=8080 | ||
| networks: | ||
| pmoves_app: { external: true } | ||
| pmoves_external: { external: true } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| services: | ||
| traefik: | ||
| image: ${TRAEFIK_IMAGE:-traefik:v3.3} | ||
| container_name: pmoves-traefik | ||
| restart: unless-stopped | ||
| command: | ||
| - --providers.docker=true | ||
| - --providers.docker.exposedbydefault=false | ||
| - --providers.file.filename=/etc/traefik/dynamic.yml | ||
| - --entrypoints.web.address=:80 | ||
| - --entrypoints.websecure.address=:443 | ||
| - --entrypoints.web.http.redirections.entrypoint.to=websecure | ||
| - --certificatesresolvers.cf.acme.dnschallenge=true | ||
| - --certificatesresolvers.cf.acme.dnschallenge.provider=cloudflare | ||
| - --certificatesresolvers.cf.acme.email=${ACME_EMAIL:-ops@pmoves.ai} | ||
| - --certificatesresolvers.cf.acme.storage=/letsencrypt/acme.json | ||
| environment: | ||
| CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN} | ||
| ports: ["443:443", "80:80"] | ||
| volumes: | ||
| - /var/run/docker.sock:/var/run/docker.sock:ro | ||
| - ./config/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro | ||
| - traefik-acme:/letsencrypt | ||
| networks: { pmoves_external: {} } | ||
| labels: | ||
| - traefik.enable=true | ||
| - traefik.http.routers.auth.rule=Host(`auth.pmoves.ai`) | ||
| - traefik.http.routers.auth.entrypoints=websecure | ||
| - traefik.http.routers.auth.tls.certresolver=cf | ||
| - traefik.http.routers.auth.service=sso-auth@docker | ||
| volumes: { traefik-acme: {} } | ||
| networks: | ||
| pmoves_external: { external: true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Exclude Jellyfin from the ForwardAuth path in this diagram.
The diagram places
media.pmoves.aibehind ForwardAuth, conflicting with the OIDC-only Jellyfin design and the plan’s explicit “NO forward-auth middleware” instruction. Show Jellyfin routing directly to its public OIDC-capable endpoint.🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 15-15: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents