feat(sso): per-app integration — RS256/JWKS OIDC, Open-Notebook + Jellyfin, Layer-2 proof-of-proxy (Phase 2) - #2229
Conversation
|
Warning Review limit reached
Next review available in: 59 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (13)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 554c86c8f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| SSO_FORWARD_AUTH_SECRET= | ||
| # App-side opt-in: header-trust is OFF unless this is truthy AND the secret above | ||
| # verifies (fail-closed default). Set on the header-trusting apps (open-notebook). | ||
| TRUST_REMOTE_USER_HEADER= |
There was a problem hiding this comment.
Inject SSO proof settings into external Notebook
When Notebook is started through the Traefik-facing open-notebook-ext service, setting this flag has no effect: pmoves/docker-compose.external.yml:161-174 explicitly defines the container environment but includes neither TRUST_REMOTE_USER_HEADER nor SSO_FORWARD_AUTH_SECRET (--env-file only supplies Compose interpolation). Consequently the new fail-closed remote-user middleware remains disabled or cannot verify Traefik's secret, so users authenticated by ForwardAuth still encounter Notebook's password authentication. Pass both settings into that service.
Useful? React with 👍 / 👎.
| for key in ("SSO_FORWARD_AUTH_SECRET", "JELLYFIN_OIDC_CLIENT_SECRET"): | ||
| if _is_blank_or_placeholder(_get_kv(text, key)): | ||
| text = _set_kv(text, key, _strong_random(32)) |
There was a problem hiding this comment.
Register generated SSO secrets in the CHIT manifest
These credentials are generated into env.shared, but neither SSO_FORWARD_AUTH_SECRET nor JELLYFIN_OIDC_CLIENT_SECRET has an entry in pmoves/chit/secrets_manifest_v2.yaml or its generated v1 manifest. The bundle-based production funnel projects only manifest entries, so secrets-funnel-sync-from-bundle cannot restore these values and each node can instead generate unrelated local credentials, breaking proxy-secret or OIDC-client agreement across deployments. Add both entries to the v2 manifest and regenerate v1 through the canonical funnel.
AGENTS.md reference: AGENTS.md:L92-L97
Useful? React with 👍 / 👎.
| pem = rsa.generate_private_key(public_exponent=65537, key_size=2048).private_bytes( | ||
| serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, | ||
| serialization.NoEncryption()).decode() | ||
| path.parent.mkdir(parents=True, exist_ok=True) | ||
| path.write_text(pem) |
There was a problem hiding this comment.
Serialize first-time OIDC key creation
On a fresh volume, concurrent initial calls to /oidc/jwks and /oidc/token can both observe no key file, generate different keys, and overwrite the same path because the synchronous FastAPI handlers run in a thread pool. Each request may then cache or sign with a different PEM under the same fixed kid, while later JWKS responses expose whichever write won, causing freshly issued ID tokens to fail verification. Use an atomic create/read-back operation or a lock so every caller resolves the same persisted key.
Useful? React with 👍 / 👎.
… provider) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…of fix) 3 security reviews flagged bare Remote-User trust as CRITICAL (any pmoves_app peer forges the header off-proxy). Defence-in-depth Layer 2: - config: forward_auth_secret (env SSO_FORWARD_AUTH_SECRET) - /auth/verify emits X-Forward-Auth-Secret on 200 (only when configured) - Traefik authResponseHeaders forwards it, OVERWRITING any client value so it can't be smuggled through the proxy; apps verify it before trusting Remote-User - env.shared.example: SSO_FORWARD_AUTH_SECRET, OIDC_SIGNING_KEY, TRUST_REMOTE_USER_HEADER - +2 tests (secret emitted on 200; omitted on 401). 27 passed. Layer 1 (network segmentation) + open-notebook app-side verify follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… proof-of-proxy) Task 8 + Layer-2 hardening: open-notebook RemoteUserMiddleware trusts Remote-User only with the X-Forward-Auth-Secret proof-of-proxy (fail-closed opt-in). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ke, Task 9) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_ensure_sso_credentials (idempotent, in the secrets-funnel chain): - config defaults: SSO_PUBLIC_BASE_URL, SSO_COOKIE_DOMAIN, GOTRUE_URL, JELLYFIN_OIDC_CLIENT_ID=pmoves-jellyfin, JELLYFIN_OIDC_REDIRECT_URIS, ACME_EMAIL=pmoves@pmoves.ai - generated once when blank: SSO_FORWARD_AUTH_SECRET, JELLYFIN_OIDC_CLIENT_SECRET (token_urlsafe), OIDC_SIGNING_KEY (RSA-2048 PEM, \n-escaped for env_file safety since _set_kv rejects real newlines; sso-auth un-escapes at load). So the funnel provisions the whole SSO secret surface — no hand-editing env.shared. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… pipeline PEM) A large RSA PEM doesn't fit the url-safe CHIT/env secrets pipeline. Genuine clean pattern: oidc._load_or_generate_private_key resolves the key by precedence — explicit OIDC_SIGNING_KEY env override > persisted key file (OIDC_SIGNING_KEY_PATH, default /data/oidc_signing_key.pem) > generate RSA-2048 once + persist (0600). - Dockerfile: pmoves-owned /data + VOLUME (persist across recreates; mount a shared key file for multi-node JWKS consistency) - brand_defaults: stop generating OIDC_SIGNING_KEY (service self-provisions); keeps SSO_FORWARD_AUTH_SECRET/JELLYFIN_OIDC_CLIENT_SECRET (need manifest reg) - +1 test (auto-generate + persist). 28 passed. VERIFIED LIVE: /oidc/jwks returns RS256 key auto-generated + persisted to /data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (live e2e) The live e2e caught two production-breaking bugs in the PR#1 compose: - sso-auth was on pmoves_app+pmoves_external only, but GoTrue is on pmoves_api -> could not resolve/reach it (every login would fail). Added pmoves_api. - SSO_FORWARD_AUTH_SECRET + OIDC_SIGNING_KEY were absent from the environment: block, so compose never injected them regardless of provisioning. Added both. - Added sso-oidc-key volume to persist the self-provisioned OIDC key across recreates. Also: handoff doc for the pre-existing broken GoTrue auth schema (forked migration set + wrong DB role) that blocks the login e2e — needs DB-superuser repair, separate from this PR. sso-auth verified correct to the GoTrue boundary (gotrue /health=200). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
554c86c to
9953743
Compare
Docker Hardening ValidationHardening Validation ReportValidated: Sun Jul 26 00:03:10 UTC 2026Services CheckedPMOVES.AI Docker Hardening Validation[INFO] Checking: pmoves/docker-compose.hardened.yml [INFO] Validating: hi-rag-gateway-v2 [INFO] Validating: extract-worker [INFO] Validating: langextract [INFO] Validating: presign [INFO] Validating: render-webhook [INFO] Validating: retrieval-eval [INFO] Validating: pdf-ingest [INFO] Validating: jellyfin-bridge [INFO] Validating: invidious-companion-proxy [INFO] Validating: ffmpeg-whisper [INFO] Validating: media-video [INFO] Validating: media-audio [INFO] Validating: hi-rag-gateway-v2-gpu [INFO] Validating: hi-rag-gateway-gpu [INFO] Validating: deepresearch [INFO] Validating: supaserch [INFO] Validating: publisher-discord [INFO] Validating: mesh-agent [INFO] Validating: nats-echo-req [INFO] Validating: nats-echo-res [INFO] Validating: comfy-watcher [INFO] Validating: grayjay-plugin-host [INFO] Validating: agent-zero [INFO] Validating: p7-room-orchestrator [INFO] Validating: archon [INFO] Validating: channel-monitor [INFO] Validating: pmoves-yt [INFO] Validating: notebook-sync [INFO] Validating: supabase_service_role_key [INFO] Validating: supabase_jwt_secret [INFO] Validating: p7_control_token ====================================== |
What
PR #2 of the SSO gateway (stacked on #2221 /
feat/sso-gateway) — the per-app integration + the OIDC-signing rework needed to make Jellyfin real. Delivers Tasks 8, 9, and the RS256/JWKS + Layer-2 hardening; the live e2e caught and fixed two production-breaking compose bugs along the way.Highlights
id_tokennow signed RS256 with a real/oidc/jwks(the Jellyfin plugin shipsMicrosoft.IdentityModeland validates via JWKS; HS256 could never work). The RSA key self-provisions (load-or-generate to a persisted/datafile) — no fragile PEM in the url-safe CHIT/env pipeline; mount a shared key for multi-node.RemoteUserMiddleware(fork7e67910, gitlink bumped).sso-authemitsX-Forward-Auth-Secreton/auth/verify, Traefik forwards it (overwriting client values), and the app honorsRemote-Useronly with that secret + a fail-closedTRUST_REMOTE_USER_HEADERopt-in.oidc-rbac.zip@v1.0.8baked with entrypoint copy-on-start past the/configmount (forkf80afdee0, gitlink bumped) — build + copy verified.brand_defaultsseeds the SSO config + generates the gateway secrets.Two production bugs the live e2e caught (fixed here)
sso-authwas onpmoves_app+pmoves_externalonly, but GoTrue is onpmoves_api→ it couldn't reach GoTrue at all. Addedpmoves_api.SSO_FORWARD_AUTH_SECRET/OIDC_SIGNING_KEYwere absent from theenvironment:block → never injected regardless of provisioning. Added both (+ a volume for the OIDC key).Verified
sso-authdeployed against the running stack — healthy, realSUPABASE_JWT_SECRETwired,/oidc/jwksserving the self-provisioned key,gotrue /health: 200fromsso-auth.Known follow-up (data-tier prerequisite, NOT this PR)
The full
login → 4-appwalk + Layer-1 network segmentation are blocked by a pre-existing broken GoTrue auth schema on the node (wrong DB role / emptysearch_path+ a forked migration set), which needs a DB-superuser repair — documented inpmoves/docs/handoffs/gotrue-auth-schema-repair-2026-07-25.md. GoTrue was left stable; the SSO gateway is verified correct to that boundary.Deferred to a follow-up
Layer-1 network segmentation; Jellyfin provider config + live OIDC round-trip; manifest registration of the short SSO secrets; the least-privilege GoTrue role repoint.
🤖 Generated with Claude Code