Track Cloudron 10 follow-ups and add SSO migration plan
This commit is contained in:
1 parent
d6e697a0c3
commit
c96de68f21
2 files changed
+143
No files matched your search
@@ -3,6 +3,24 @@
|
|||||||
How the co-op's Cloudron credentials are organized, and how to rotate them
|
How the co-op's Cloudron credentials are organized, and how to rotate them
|
||||||
without breaking the stack.
|
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
|
## The two-token split
|
||||||
|
|
||||||
There are **two separate** Cloudron API tokens, deliberately decoupled so
|
There are **two separate** Cloudron API tokens, deliberately decoupled so
|
||||||
|
|||||||
@@ -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).
|
||||||
Reference in new issue
Block a user