Authentication & ingress auth
Access classes & ingress authentication
Section titled “Access classes & ingress authentication”Ingress authentication is driven by each project’s access class
(deployment_controller.access_classes), specifically its access_requirement:
None(e.g. thepublicclass) — open, no auth. App routers carry only the host rule.Authenticated/Member(e.g. theprivateclass) — the reconciler stamps a Traefik forwardAuth middleware on the app’s router.
For a non-None class, the reconciler emits (see render_traefik_labels in
labels.rs):
traefik.http.middlewares.<router>-auth.forwardauth.address = http://rise:3000/api/v1/auth/ingress?project=<project>&signin_redirect=1traefik.http.middlewares.<router>-auth.forwardauth.authResponseHeaders = X-Auth-Request-Email,X-Auth-Request-Usertraefik.http.routers.<router>.middlewares = <router>-auth@dockerThe
http://rise:3000host shown here is the value for the standalone Compose stack, where the backend is a service namedrise. It is whateverdeployment_controller.auth_backend_urlresolves to — on the host-dev path (mise br docker) that ishttp://host.docker.internal:3000(see Local development), notrise:3000.
Traefik issues a subrequest to /api/v1/auth/ingress (the
deployment_controller.auth_backend_url, internally http://rise:3000) before
proxying each request:
- A valid Rise session cookie →
200plusX-Auth-Request-Email/X-Auth-Request-User, which Traefik copies onto the forwarded request. - No / invalid cookie → because the middleware sets
signin_redirect=1, the handler returns a302to the same-host/.rise/auth/signinpage (build_signin_redirect_urlinauth/handlers.rs). This mirrors the Kubernetes nginxauth-signinflow.
The redirect target host is reconstructed from the X-Forwarded-Proto /
X-Forwarded-Host / X-Forwarded-Uri headers, so the user is sent to the login
page on the app’s own host and returned to where they started after signing
in. The configured auth_signin_url is only a degraded fallback used when
X-Forwarded-Host is absent.
Fail-closed
Section titled “Fail-closed”The backend refuses to start if any access class with a non-None
requirement is configured while deployment_controller.auth_backend_url is
empty (build_app_state in src/server/state.rs). Without the backend URL the
forwardAuth middleware cannot be wired, and those projects would otherwise be
served publicly with no authentication. Either set auth_backend_url (e.g.
http://rise:3000) or change the requirement to None.
/.rise routing
Section titled “/.rise routing”The session cookie that forwardAuth reads must be set on the app’s host, not
the control plane. To serve the login / ingress-auth endpoints on every app host,
the base Compose file adds a high-priority Traefik router on the rise-backend
container:
traefik.http.routers.rise-dotrise.rule = PathPrefix(`/.rise`)traefik.http.routers.rise-dotrise.priority = 1000priority=1000 exceeds the app routers’ default (rule-length) priority, so a
request for /.rise/... on an app host is served by the Rise backend rather than
the app. This mirrors the Kubernetes /.rise Ingress path. In the local overlay
the same router is re-stamped as rise-dotrise-web on the plain web
entrypoint (since websecure/le do not exist there).
App → backend resolution (local dev)
Section titled “App → backend resolution (local dev)”Apps that validate the rise_jwt cookie or perform OIDC discovery against the
public issuer/control-plane host (e.g. rise.localhost) must be able to reach
the Rise backend at that host. In a local stack the public host resolves to
the app container’s own loopback, not the backend — so those calls would fail.
To fix this locally the Docker controller stamps HostConfig.extra_hosts on
every managed app container, mapping the configured alias host
(RISE_APP_BACKEND_HOST_ALIAS=rise.localhost, which populates
deployment_controller.app_backend_host_aliases) to the backend. There are two
cases, depending on where the backend runs:
- Containerized backend (standalone Compose): the alias is mapped to the
backend’s IP on the shared
rise_defaultnetwork, resolved at reconcile startup from theauth_backend_urlhost (e.g.rise) via Docker DNS (resolve_backend_ipinsrc/server/state.rs). The captured IP is fixed at container-create time; if the backend restarts with a new IP, existing app containers keep the old entry until recreated — acceptable for local dev. - Host-run backend (
mise br docker): the task setsRISE_APP_BACKEND_IP=host-gateway(thedeployment_controller.app_backend_ipsetting), so the controller stampsrise.localhost:host-gatewayverbatim, skipping DNS. Docker resolves the specialhost-gatewayvalue to the host gateway per container at create time, on both Docker Desktop and Linux — so there is no captured IP and no staleness.
Production needs nothing here — and the alias list is empty by default
(app_backend_ip is unset too). Public DNS resolves rise.${RISE_DOMAIN} to
Traefik, which terminates TLS and forwards to the backend; injecting an
extra_hosts override in production would wrongly bypass Traefik and break TLS.
Authentication / Dex
Section titled “Authentication / Dex”The backend uses a single OIDC issuer for both server-side token exchange and browser logins. The issuer URL must resolve identically from inside the Rise backend container and from the user’s browser (OIDC “split-horizon”).
Local / dev: rise-dex
Section titled “Local / dev: rise-dex”Local and dev stacks use the issuer http://rise-dex:5556/dex, which is what the
bundled dev/dex/config.yaml actually serves. rise-dex resolves:
- inside the network — via the container name plus an explicit
rise-dexnetwork alias on therise_defaultnetwork; and - on the host/browser — via the
rise-dex → 127.0.0.1/etc/hostsentry added bymise run setup, plus the published:5556host port.
Using one hostname that resolves the same everywhere avoids issuer-mismatch failures where the backend and the browser would otherwise disagree on the issuer URL.
Production caveat (important)
Section titled “Production caveat (important)”The bundled Dex is a demo IdP (in-memory storage, static passwords —
admin@example.com / password). For production, prefer an external IdP: set
auth.issuer, auth.client_id and auth.client_secret to the external
provider’s values and drop the dex service from the stack.
The base Compose file does not publish Dex publicly; the backend reaches it
only over the internal rise_default network. Exposing the demo IdP at
dex.${RISE_DOMAIN} is opt-in via the docker-compose.standalone.demo-idp.yaml
overlay, intended only for a throwaway demo/evaluation stack:
docker compose -f docker-compose.standalone.yaml \ -f docker-compose.standalone.demo-idp.yaml up -dIf you do run the bundled Dex in production (demo overlay layered), two
config changes in dev/dex/config.yaml are both required — Dex enforces
each independently:
- Issuer. Dex validates that the issuer it serves equals its configured
issuer. The shipped config advertiseshttp://rise-dex:5556/dex; whenDEX_ISSUERpoints athttps://dex.${RISE_DOMAIN}(the base file’s default), change the servedissuerindev/dex/config.yamlto match — otherwise Dex rejects the discovery/token requests. - Redirect URI. The backend builds the OAuth callback as
{public_url}/api/v1/auth/callback, i.e.https://rise.${RISE_DOMAIN}/api/v1/auth/callbackin production. Dex rejects anyredirect_urinot instaticClients[rise-backend].redirectURIs, which only lists the local hosts. Addhttps://rise.${RISE_DOMAIN}/api/v1/auth/callbackto that list — this is the only redirect URI the backend registers, including for the custom-domain/.riseflow (the IdP callback always lands here, after which the backend redirects internally to/.rise/auth/complete). Without this, login fails withinvalid redirect_urieven after fixing the issuer.