Skip to content

feat(sso): per-app integration — RS256/JWKS OIDC, Open-Notebook + Jellyfin, Layer-2 proof-of-proxy (Phase 2) - #2229

Merged
POWERFULMOVES merged 7 commits into
mainfrom
feat/sso-gateway-apps
Jul 26, 2026
Merged

POWERFULMOVES merged 7 commits into
mainfrom
feat/sso-gateway-apps

Conversation

@POWERFULMOVES

Copy link
Copy Markdown
Owner

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

  • RS256 + JWKS OIDC providerid_token now signed RS256 with a real /oidc/jwks (the Jellyfin plugin ships Microsoft.IdentityModel and validates via JWKS; HS256 could never work). The RSA key self-provisions (load-or-generate to a persisted /data file) — no fragile PEM in the url-safe CHIT/env pipeline; mount a shared key for multi-node.
  • Task 8 — Open-Notebook RemoteUserMiddleware (fork 7e67910, gitlink bumped).
  • CRITICAL header-spoof closed (3 reviews) — Layer-2 proof-of-proxy: sso-auth emits X-Forward-Auth-Secret on /auth/verify, Traefik forwards it (overwriting client values), and the app honors Remote-User only with that secret + a fail-closed TRUST_REMOTE_USER_HEADER opt-in.
  • Task 9 — Jellyfin plugin oidc-rbac.zip@v1.0.8 baked with entrypoint copy-on-start past the /config mount (fork f80afdee0, gitlink bumped) — build + copy verified.
  • brand_defaults seeds the SSO config + generates the gateway secrets.

Two production bugs the live e2e caught (fixed here)

  1. sso-auth was on pmoves_app+pmoves_external only, but GoTrue is on pmoves_api → it couldn't reach GoTrue at all. Added pmoves_api.
  2. SSO_FORWARD_AUTH_SECRET / OIDC_SIGNING_KEY were absent from the environment: block → never injected regardless of provisioning. Added both (+ a volume for the OIDC key).

Verified

  • 28 unit tests (jwt_verify / login / oidc) — role check, all open-redirect vectors, fail-closed-on-outage, RS256 round-trip + JWKS, auto-generate/persist.
  • Live: sso-auth deployed against the running stack — healthy, real SUPABASE_JWT_SECRET wired, /oidc/jwks serving the self-provisioned key, gotrue /health: 200 from sso-auth.

Known follow-up (data-tier prerequisite, NOT this PR)

The full login → 4-app walk + Layer-1 network segmentation are blocked by a pre-existing broken GoTrue auth schema on the node (wrong DB role / empty search_path + a forked migration set), which needs a DB-superuser repair — documented in pmoves/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

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@POWERFULMOVES, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bc03c8b-b129-4305-af6e-98fad0136317

📥 Commits

Reviewing files that changed from the base of the PR and between 8be4972 and 9953743.

📒 Files selected for processing (13)
  • PMOVES-Jellyfin
  • PMOVES-Open-Notebook
  • pmoves/config/traefik/dynamic.yml
  • pmoves/docker-compose.sso.yml
  • pmoves/docs/handoffs/gotrue-auth-schema-repair-2026-07-25.md
  • pmoves/env.shared.example
  • pmoves/services/sso-auth/Dockerfile
  • pmoves/services/sso-auth/app.py
  • pmoves/services/sso-auth/config.py
  • pmoves/services/sso-auth/oidc.py
  • pmoves/services/sso-auth/tests/test_login.py
  • pmoves/services/sso-auth/tests/test_oidc.py
  • pmoves/tools/brand_defaults.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sso-gateway-apps

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added compose Compose files / service Dockerfiles docs Documentation services Service source under pmoves/services/ config pmoves/config(s)/ changes labels Jul 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread pmoves/env.shared.example
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=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +301 to +303
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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +39 to +43
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Base automatically changed from feat/sso-gateway to main July 25, 2026 23:44
POWERFULMOVES and others added 7 commits July 25, 2026 20:02
… 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>
@POWERFULMOVES
POWERFULMOVES force-pushed the feat/sso-gateway-apps branch from 554c86c to 9953743 Compare July 26, 2026 00:02
@github-actions

Copy link
Copy Markdown
Contributor

Docker Hardening Validation

Hardening Validation Report

Validated: Sun Jul 26 00:03:10 UTC 2026

Services Checked

PMOVES.AI Docker Hardening Validation

[INFO] Checking: pmoves/docker-compose.hardened.yml

[INFO] Validating: hi-rag-gateway-v2
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: extract-worker
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: langextract
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: presign
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: render-webhook
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: retrieval-eval
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: pdf-ingest
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: jellyfin-bridge
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: invidious-companion-proxy
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: ffmpeg-whisper
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: media-video
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: media-audio
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: hi-rag-gateway-v2-gpu
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: hi-rag-gateway-gpu
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: deepresearch
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: supaserch
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: publisher-discord
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: mesh-agent
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: nats-echo-req
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: nats-echo-res
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: comfy-watcher
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: grayjay-plugin-host
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: agent-zero
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: p7-room-orchestrator
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: archon
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: channel-monitor
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: pmoves-yt
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: notebook-sync
[PASS] Non-root user: 65532:65532
[PASS] Read-only filesystem
[PASS] All capabilities dropped
[PASS] No-new-privileges enabled
[WARN] No resource limits

[INFO] Validating: supabase_service_role_key
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

[INFO] Validating: supabase_jwt_secret
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

[INFO] Validating: p7_control_token
[WARN] No user directive
[WARN] No read_only directive
[WARN] No cap_drop: ["ALL"]
[WARN] No no-new-privileges
[WARN] No resource limits

======================================
Summary: 112 passed, 43 warnings, 0 errors

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compose Compose files / service Dockerfiles config pmoves/config(s)/ changes docs Documentation services Service source under pmoves/services/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant