From c96de68f21de0f7ce4309afda00530c39099c185 Mon Sep 17 00:00:00 2001 From: inference-bot Date: Wed, 23 Sep 2026 08:03:21 -0600 Subject: [PATCH] Track Cloudron 10 follow-ups and add SSO migration plan --- operator-token-runbook.md | 18 ++++++ sso-migration.md | 125 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 sso-migration.md diff --git a/operator-token-runbook.md b/operator-token-runbook.md index efe050d..ba2746a 100644 --- a/operator-token-runbook.md +++ b/operator-token-runbook.md @@ -3,6 +3,24 @@ How the co-op's Cloudron credentials are organized, and how to rotate them without breaking the stack. +## Cloudron 10 follow-ups (tracked, Sept 2026) + +Two items surfaced by the Cloudron 10 release. Neither is urgent — both apps +still work — but both are scheduled deliberately so they're fixed *before* the +membership grows. + +1. **Migrate webmail off SnappyMail → Cloudron "Mail".** SnappyMail (RainLoop's + fork) is abandoned; Cloudron built its own replacement, "Mail," but it is + still catching up on features (their words). Keep SnappyMail for now; migrate + `mail.inference.coop` once "Mail" is feature-complete. Until then SnappyMail + is a known unmaintained dependency. + +2. **Migrate `dashboard` + `panel` from `proxyAuth` → OIDC.** Cloudron 10 is + phasing out proxyAuth/LDAP in favor of OIDC (93 packages on OIDC vs 11 on + proxyAuth). Our two custom apps (`member-dashboard`, `admin-panel`) still use + the `proxyAuth` addon; the portal and all App Store apps are already on OIDC. + See [SSO migration plan](sso-migration.md). + ## The two-token split There are **two separate** Cloudron API tokens, deliberately decoupled so diff --git a/sso-migration.md b/sso-migration.md new file mode 100644 index 0000000..18e49c1 --- /dev/null +++ b/sso-migration.md @@ -0,0 +1,125 @@ +# SSO migration plan — `proxyAuth` → OIDC + +Migrating our two custom apps (`member-dashboard`, `admin-panel`) off the legacy +`proxyAuth` addon to OpenID Connect, matching the rest of the stack and Cloudron +10's direction. + +**Status: planned, not started.** Both apps work today on `proxyAuth`; this is +future-proofing done deliberately while the membership is small. + +## Why + +- Cloudron 10 is moving packages from LDAP/proxyAuth → OIDC so that "only + Cloudron sees the password." Our two custom apps are on the legacy path + (proxyAuth); the portal and all App Store apps are already OIDC. +- proxyAuth is the minority path now (11 packages vs 93 on OIDC). Migrating + removes a dependency on a mechanism Cloudron is deprioritizing. + +## The core technical fact (shapes the whole plan) + +**proxyAuth and OIDC are fundamentally different auth models — this is a real +code change, not a manifest swap.** + +- **proxyAuth** puts an auth *wall* in front of the app. Cloudron handles login + and **injects the authenticated user's username via a header** + (`X-Remote-User` / `X-Forwarded-User`). The app just reads the header. +- **OIDC** does *not* inject a header. Cloudron exports `CLOUDRON_OIDC_*` env + vars (`CLOUDRON_OIDC_ISSUER`, `..._CLIENT_ID`, `..._CLIENT_SECRET`, + `..._DISCOVERY_URL`, `..._TOKEN_ENDPOINT`, `..._PROFILE_ENDPOINT`, etc.) and + the app must implement the full **authorization-code flow**: + 1. redirect to `CLOUDRON_OIDC_AUTH_ENDPOINT`, + 2. receive the code at `loginRedirectUri` (e.g. `/auth/openid/callback`), + 3. exchange it at `CLOUDRON_OIDC_TOKEN_ENDPOINT`, + 4. validate the ID token (RS256, keys from `CLOUDRON_OIDC_KEYS_ENDPOINT`), + 5. establish an app-side session cookie. + +So each app needs an OIDC client implementation (redirect, callback, token +exchange, session) where today it only reads a header. + +## Current state of the two apps + +Both read identity the same way — a small header-union helper: + +```python +def get_identity(request): + for header in ("x-remote-user", "x-forwarded-user", "x-auth-request-user", + "x-auth-request-email", "x-forwarded-email"): + val = request.headers.get(header) + if val: + return val.strip() + return "" +``` + +- **member-dashboard**: passes that identity (a Cloudron *username*, e.g. + `ntnsndr`) to the portal's `/broker/*` as `X-Member-User`. The portal resolves + username → email (it holds the Cloudron admin token). +- **admin-panel**: checks the username against an `ADMIN_USERNAMES` allowlist, + then calls the portal's `/admin/*` with `X-Admin-Token`. + +## Open question to resolve first (before code) + +The portal's broker resolves **username → email**, but OIDC's userinfo / ID +token may return the **email** directly (or the username, or both). Which claim +Cloudron's OIDC profile endpoint exposes determines whether the dashboard keeps +sending `X-Member-User` (username) or switches to `X-Member-Email` (email). Both +work today (the broker accepts either and normalizes to email), but we should +confirm what the ID token actually contains rather than assume. + +**Action:** once the OIDC client is wired in a test app, log the decoded ID +token claims and note whether `email`, `preferred_username`, and/or `sub` are +present. Then pick the identity field accordingly. + +## Migration steps + +### Phase 1 — add OIDC plumbing to each app (no manifest change yet) + +For each app, add a small OIDC client that: + +1. Reads `CLOUDRON_OIDC_*` env vars. +2. On an unauthenticated request, redirects to the auth endpoint. +3. Handles the callback (`loginRedirectUri`) — exchange code for ID token, + validate signature + issuer + audience. +4. Sets a signed session cookie; reads it on subsequent requests. +5. Replaces `get_identity()` with "read identity from the validated session" + (falling back to the header during the transition). + +The dashboard's `/logout` link and the panel's `ADMIN_USERNAMES` allowlist stay; +only the *identity source* changes. + +### Phase 2 — swap the manifest addon + +Replace `"proxyAuth": {}` with `"oidc": { "loginRedirectUri": "/auth/openid/callback", "logoutRedirectUri": "/" }` in each `CloudronManifest.json`, then redeploy. + +### Phase 3 — verify + +- A member can log into `dashboard.inference.coop` and see their usage/keys. +- A non-admin is **denied** on `panel.inference.coop`; an admin gets in. +- The broker still resolves identity correctly (usage/keys load). +- Logout works and re-presents the login. + +### Phase 4 — remove the header fallback + +Once OIDC is verified, drop the `X-Remote-User`/`X-Forwarded-User` fallback so +the apps no longer trust a spoofable header (a header the app now controls is +fine internally, but a client-supplied `X-Remote-User` would otherwise be +trusted if the wall were ever bypassed). + +## Rollback + +Until Phase 4, the header fallback means we can revert the manifest to +`proxyAuth` and redeploy with a single change. Keep that path open until +verification passes. + +## Notes / gotchas + +- **The portal itself is already OIDC** (via the `oidc` addon) but does *not* + actually implement a client — its `oidc` block has `loginRedirectUri: "/"`, + and it's effectively used for the admin/broker token surfaces, not member + login. Don't pattern-match on the portal; the dashboard/panel need a real + client implementation. +- **`admin-panel`'s allowlist is orthogonal** to SSO — `ADMIN_USERNAMES` must + still gate *which* authenticated users reach the panel. OIDC only changes how + we learn *who* the user is. +- **Do the dashboard first**, the panel second (the dashboard has more members + relying on it, so it validates the pattern; the panel is admin-only and lower + traffic, so a bug there is less visible).