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
crates/rise-backend-traefik/src/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 is reconstructed from the configured ingress_schema and
the 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 scheme comes from configuration rather than X-Forwarded-Proto
because forwardAuth derives that header from Traefik’s own connection, which is
plain HTTP when a load balancer terminates TLS in front of it. 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.
OIDC authentication
Section titled “OIDC authentication”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 identity provider
Section titled “Production identity provider”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 Docker
configuration reads the issuer from DEX_ISSUER for any OIDC provider; set it
to your provider’s exact issuer URL and set OIDC_CLIENT_SECRET to the registered
client secret. If the client ID differs from rise-backend, supply
OIDC_CLIENT_ID in the rise service’s environment through a Compose override.
Register {public_url}/api/v1/auth/callback with your provider.
Bundled Dex for demos
Section titled “Bundled Dex for demos”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 -dWhen exposing the bundled Dex through the demo overlay, 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/callbackwith the demo overlay. 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.