Muximux can optionally act as a security gate in front of your gateway sites. When enabled per-site, requests to that subdomain run through Muximux's session check before reaching the backend. Anonymous visitors are redirected to Muximux's login page; authenticated visitors are forwarded based on their role and group membership.
The gate sits in front of the backend - it does not replace the backend's own authentication if it has one. It's a layered defence: log in to Muximux once, gain access to every gated subdomain you have permission for.
Browser -> sonarr.example.com (Caddy)
|
v
forward_auth /api/auth/forward (Muximux Go server)
|
| session cookie valid + permitted ?
| no -> 302 to /login -> back to original URL after sign-in
| yes -> 200 with X-Muximux-User / X-Muximux-Role headers
v
reverse_proxy http://192.168.1.5:8989 (the backend)
✅ You want a single sign-on layer in front of multiple homelab apps
✅ One or more apps lack their own auth (Plex, Grafana, custom Flask app)
✅ You're already using Muximux as your dashboard with built-in / OIDC / forward-auth users
✅ All gated subdomains share a parent domain (*.example.com)
❌ You're hosting public-facing sites that need to be reachable anonymously
❌ Each backend already has battle-tested SSO you want to keep authoritative
❌ Your gated subdomains span different parent domains (app.alpha.com + app.beta.org)
The gate uses Muximux's session cookie as the auth source. For the cookie to be visible at every gated subdomain, you need:
- Muximux's dashboard and every gated site to share a parent domain. Example:
muximux.example.com,sonarr.example.com,grafana.example.comall share.example.com. - A consistent transport. Either all HTTPS (recommended) or all plain HTTP for local-only setups - browsers reject mixed-Secure cookies across the same domain.
server.session_cookie_domainconfigured so Muximux issues the cookie with the right scope.
The validator at config load enforces this. If a site has require_auth: true but isn't a subdomain of session_cookie_domain, Muximux exits at startup with a specific error pointing at the offending site.
In config.yaml:
server:
session_cookie_domain: ".example.com" # or "example.com", browser normalises
tls:
domain: "muximux.example.com"The leading dot is the browser-canonical form but optional - browsers treat both equally.
In Settings → Gateway, edit a site and tick Require Muximux login:
server:
gateway_sites:
- domain: sonarr.example.com
backend_url: http://192.168.1.5:8989
tls: auto
require_auth: true # gate this site
min_role: user # optional; "user" / "power-user" / "admin"
allowed_groups: # optional; case-insensitive; admins bypass
- family
- adminsRestart Muximux (or, for an in-flight gateway-site mutation, the Gateway tab's save does this for you). Caddy's reload picks up the new forward_auth directive on every gated site.
Identical to the App access rules used by the dashboard:
| Setting | Meaning |
|---|---|
require_auth: false |
No gate; anonymous traffic reaches the backend. |
require_auth: true, no min_role, no allowed_groups |
Any authenticated Muximux user can reach the backend. |
require_auth: true, min_role: power-user |
Users at power-user level or higher pass. Admins always pass. |
require_auth: true, allowed_groups: [family] |
Users in the family group pass. Admins bypass the group check. |
require_auth: true, both set |
User must satisfy both: meet the role bar AND be in at least one allowed group. Admins still bypass both. |
Group matching is case-insensitive (Family and family are the same group).
When Caddy calls /api/auth/forward for a gated site:
| Muximux returns | Caddy serves | User sees |
|---|---|---|
200 OK + X-Muximux-User + X-Muximux-Role |
Reverse-proxies to the backend with those headers attached | The app, normally |
302 Found → /login?next=<url> |
Redirects | Muximux login page; after sign-in, lands back at the original URL |
403 Forbidden |
Returns body verbatim | Small "you're signed in but lack permission" page with a sign-out link |
500 / 503 |
Returns body | "gateway misconfigured" message (rare; surfaces operator config errors) |
Backends receive X-Muximux-User and X-Muximux-Role headers on every forwarded request. Apps that honour these (rare but increasing) can use them as an external-auth source.
/logout on Muximux invalidates the session cookie. Because the cookie is scoped to .example.com, this clears it for every gated subdomain at once. Next request to any gated site triggers the login redirect again.
There is intentionally no per-site logout: the gate is a centralised SSO layer, and "log out of one app but not the others" doesn't compose cleanly with that model. If your backend has its own logout, that's separate - the gate doesn't propagate.
A couple of defence-in-depth measures sit on the gate endpoint:
- Only reachable from inside Muximux.
/api/auth/forwardis the receive side of Caddy's forward_auth call, so it only ever gets reached over the loopback interface during normal operation. The endpoint refuses non-loopback callers, which means a public visitor can't probe session validity or gateway-site configuration by hitting it directly. - Login-redirect target is locked to your configured sites. When the gate sends an anonymous visitor to
/login, the post-login bounce-back URL is built from the configuredgateway_sitesdomain rather than a header value the caller controls. Off-site redirect attacks via header injection are prevented by construction. - Audit log on every decision. Allows, denials, and permission-denied responses each leave an audit-log line with the host and the session's user/role.
| Muximux auth method | Gate behaviour |
|---|---|
auth.method: builtin |
Native users; session check is straightforward. |
auth.method: oidc |
Sessions backed by OIDC tokens; gate uses the same session check. Operators get OIDC users surfaced to gated apps via X-Muximux-User. |
auth.method: forwardauth (Authelia / Authentik) |
Muximux trusts an upstream Remote-User header to create a session, then the gate reads that session. Daisy-chained forward-auth works but adds a network hop; consider whether the upstream is already gating the same set of apps. |
| Symptom | Likely cause |
|---|---|
| Browser loops between gated site and login | Cookie isn't reaching the gated host. Verify session_cookie_domain covers both. Check the browser's cookie inspector. |
| 503 "configure server.tls.domain" on the gate | The gate doesn't know where to send the login redirect. Set server.tls.domain to your dashboard host. |
| Startup error "session_cookie_domain is required when any gateway site has require_auth=true" | You enabled require_auth without setting the cookie scope. Either set session_cookie_domain or unset the auth flag. |
| Startup error "gateway site X is not under server.session_cookie_domain" | A gated subdomain doesn't share the parent domain. Move the site or widen the cookie scope. |
| 403 on a gated site you should have access to | Your account's role or groups don't satisfy the site's rules. The forbidden page names the reason (role_insufficient or group_mismatch). |
| Backend doesn't recognise X-Muximux-User | Most backends ignore the header. The gate still enforces; the header is purely informational for backends that opt in. |
| WebSocket connection drops the gate | Caddy's forward_auth runs on the initial HTTP Upgrade request; once the WebSocket is up, the connection bypasses forward_auth (standard Caddy behaviour). |
server:
# Required when any site below has require_auth=true.
# The leading dot is optional; ".example.com" and "example.com" are equivalent.
session_cookie_domain: ".example.com"
gateway_sites:
- domain: sonarr.example.com
backend_url: http://192.168.1.5:8989
tls: auto
require_auth: true # default false
min_role: user # default ""; "user" / "power-user" / "admin"
allowed_groups: [family] # default []; case-insensitive; admins bypassEvery forward-auth result is recorded with source=audit:
forward-auth allowed source=audit host=sonarr.example.com user=alice role=user
forward-auth denied source=audit host=sonarr.example.com user="" result=no_session
forward-auth denied source=audit host=admin.example.com user=alice role=user result=role_insufficient
forward-auth denied source=audit host=family.example.com user=bob role=user result=group_mismatch
Use this for both observability and post-incident review. Failed access attempts are first-class events, not just operational noise.