Repository navigation
Feature Guide Forward Auth
Protect proxy hosts with CPM's built-in identity provider — no external IdP required.
- Overview
- How It Works
- Enable Forward Auth on a Proxy Host
- Non-Standard Ports
- Excluded Paths
- Groups
- Per-Host Access Control
- Login Methods
- Session Management
- Portal Login Rate Limits
- Comparison with Authentik Integration
- Generic Forward Auth (Authelia etc.)
- Identity Header Stripping
- Troubleshooting
The Forward Auth Portal turns CPM into an identity provider for your proxy hosts. Instead of deploying a separate service like Authentik or Authelia, CPM handles authentication directly:
- Login portal with credential and OAuth support
- User groups with membership management
- Per-host access lists (users and/or groups)
- Session cookies scoped per protected host
- A visitor requests a forward-auth-protected proxy host.
- Caddy's
forward_authdirective calls CPM's verify endpoint. - If the visitor has a valid session cookie, the request proceeds to the upstream.
- If not, the visitor is redirected to CPM's login portal, with the URL they asked for in the portal's
rdparameter. - After successful login, CPM issues a session cookie and redirects back.
- Subsequent requests include the cookie and pass verification automatically.
(since v1.13.1)
Caddy chooses the proxy host from the raw Host header, while CPM resolves it from the forwarded host, and the two could differ. CPM now also requires the proxy host it resolves to be the one Caddy routed the request through; if they differ, verify and callback reject the request. The verify and callback subrequests carry the ID of the proxy host whose route issued them (X-CPM-Proxy-Host-Id, set by Caddy next to the forward-auth proof header).
The forwarded host must be plain DNS labels (letters, digits and -) or a bracketed IPv6 literal. Anything the URL parser would rewrite, such as percent-encoding, non-ASCII characters or IPv4 shorthands, is not accepted. A site served on a non-default port also needs that port in FORWARD_AUTH_ALLOWED_PORTS (see Non-Standard Ports).
(since v1.13.1)
The protected URL is passed to the portal encoded, so &, #, + and % in its path or query string come back intact after login. The portal ignores ?rid= when ?rd= is present, so a rid hidden in the protected URL's own query string cannot replace the redirect target. A portal link that repeats rd or rid is refused with "This sign-in link is invalid. Open the site you were trying to reach again."
- Open Proxy Hosts and create or edit a host.
- Scroll to the Forward Auth section and enable it.
- Choose which users and/or groups may access the host.
- Optionally configure excluded paths (see below).
- Click Save / Create.
The host's Caddy config is regenerated with a forward_auth handler pointing to CPM's verify endpoint and a callback route for the login flow.
(since v1.13.1)
Protected sites are expected on the default ports 80 and 443. Caddy matches hosts without looking at the port, so CPM accepts a redirect target or session origin on any other port only when that port is listed in FORWARD_AUTH_ALLOWED_PORTS.
If browsers reach protected sites on another port (for example Caddy published as 8443:443, or behind NAT), add the port to .env (comma-separated for several):
FORWARD_AUTH_ALLOWED_PORTS=8443Then recreate the web container with docker compose up -d (docker compose restart does not re-read .env).
For a port that is not listed:
- The portal shows "This site is served on port 8443, which is not allowed for forward authentication. Ask the administrator to add it to FORWARD_AUTH_ALLOWED_PORTS."
- Existing forward-auth sessions for sites on that port stop validating.
- The web container logs
[forward-auth] Rejected <host>:8443 because port 8443 is not listed in FORWARD_AUTH_ALLOWED_PORTS. …, at most once per port per hour.
Upgrading from v1.12.0 or earlier: if browsers reach your forward-auth sites on a non-standard port, set
FORWARD_AUTH_ALLOWED_PORTSas part of the upgrade. Otherwise users are sent to the portal and cannot sign in.
See Environment Variables Reference for all forward-auth variables.
Both CPM's built-in forward auth and Authentik forward auth let you define paths that bypass authentication while the rest of the host stays protected.
- Protected Paths mode (whitelist): only the listed paths require authentication.
- Excluded Paths mode (blacklist): all paths require authentication except the listed ones.
If Protected Paths is set, it takes precedence and Excluded Paths is ignored.
- Edit a proxy host with forward auth enabled.
- Expand the Protected Paths or Excluded Paths field, whichever mode you want.
- Enter comma-separated path patterns using Caddy's glob syntax, e.g.
/share/*, /rest/*, /public/*. - Save.
Patterns are matched literally — a trailing * is required to match everything beneath a prefix. /rest/ alone matches only that exact path, not /rest/anything.
Excluded-path routes are inserted before the catch-all auth route in the generated Caddy config so they are served without authentication.
Example for Navidrome (music server): set Excluded Paths to /share/*, /rest/* so Navidrome's sharing links and REST API (used by mobile clients) work without authentication, while the main web UI stays protected.
Groups let you manage access at scale instead of per-user.
- Open Groups in the sidebar.
- Click New Group.
- Enter a name (e.g. "Engineering", "External Contractors").
- Add members from the user list.
- Click Create.
When editing a proxy host's forward auth settings, select the group in the access list. All current and future members of that group gain access automatically.
Add or remove members from the group page at any time. Changes take effect on the next request (no restart needed).
Each forward-auth-protected host maintains its own access list of allowed users and groups.
Key points:
- Access is separate from the user's role — even admins must be explicitly added.
- A user gains access if they are listed directly or belong to an allowed group.
- Removing a user from all allowed groups and the direct list revokes access immediately.
The forward auth login portal supports:
-
Credentials — a username and password. The portal looks the account up by the email
<username>@localhost, so this works only for accounts with such an email, such as the primary admin created fromADMIN_USERNAME. The sign-in username used on/login(see Feature Guide User Management#sign-in-usernames) is not accepted here. Other users sign in with OAuth, or sign in to the dashboard first; the portal then uses that session automatically. - OAuth — if OAuth is configured, users can sign in via the OAuth provider
Both methods create a forward auth session independent of the dashboard session.
(since v1.13.1) A name typed on the portal or on /login reaches one account only. CPM refuses a sign-in username that is another account's portal name (the <name> of its email <name>@localhost), and an email address <name>@localhost when another account signs in on /login with <name> or with that address. Names that already overlap are reported in the web container log on every start (see Troubleshooting#sign-in-username-warnings-on-startup).
- Forward auth sessions are stored server-side with a session cookie.
- Sessions are scoped to the protected host's domain.
- Sessions are revoked when a user's status changes away from "active".
- Changing or setting a user's password signs out all of their forward-auth sessions, and so does applying a changed
ADMIN_PASSWORDto the primary admin (since v1.13.1). See Feature Guide User Management. - Deleting a user also deletes their forward-auth sessions and access grants (since v1.13.1).
- Portal logins are rate-limited with their own settings, separate from the dashboard login page. See Portal Login Rate Limits.
(since v1.13.1)
Credential logins on the portal are limited by LOGIN_MAX_ATTEMPTS (default 5), LOGIN_WINDOW_MS (default 300000, 5 minutes) and LOGIN_BLOCK_MS (default 900000, 15 minutes). The dashboard login page uses Better Auth's own limit (AUTH_RATE_LIMIT_*) instead.
Three limits apply. When any of them is reached, the portal answers 429 "Too many login attempts. Please try again later."
| Limit | What is counted | Effect |
|---|---|---|
| Per client | Failures from one client, against any account |
LOGIN_MAX_ATTEMPTS failures within LOGIN_WINDOW_MS block the client for LOGIN_BLOCK_MS
|
| Per account and client | Failures from one client against one account | Same thresholds; blocks that client for that account |
| Per account | Failures against one account from all clients combined, counted over one hour (or LOGIN_WINDOW_MS if longer) from the first failure |
Reaching the account ceiling blocks the account for LOGIN_BLOCK_MS
|
- IPv6 clients are counted per /64 prefix.
- The account ceiling is
LOGIN_MAX_ATTEMPTS× (⌈account window ÷ min(LOGIN_WINDOW_MS,LOGIN_BLOCK_MS)⌉ + 1), where the account window is one hour orLOGIN_WINDOW_MSif longer, and at least 10 ×LOGIN_MAX_ATTEMPTS. With the defaults it is 65, which is more than one client can reach within its own limits. - One client cannot lock an account, but a few together can. With the defaults each client can make about 48 failures per hour (4 per 5-minute window) without being blocked, so a dual-stack host (IPv4 plus IPv6) or two /64s can reach the account ceiling. This is inherent to a per-account limit.
- A successful login clears the client's own counters, but not the account counter.
- Attempts that are still being checked count towards every limit, so extra concurrent attempts get
429. - Unknown, disabled and password-less accounts get the same
401"Invalid credentials" as a wrong password and take as long to reject. An expired or invalid sign-in link is rejected before the password is checked ("Invalid or expired redirect intent. Please try again."). - Usernames longer than 256 characters and request bodies over 16 KiB are refused.
- The counters are kept in memory, so recreating the web container clears them.
The stock docker-compose.yml does not pass LOGIN_MAX_ATTEMPTS, LOGIN_WINDOW_MS or LOGIN_BLOCK_MS to the web container. To change them, add them to the web service's environment:.
The per-client limits use the client's address:
- By default this is the rightmost
X-Forwarded-Forentry, the address the nearest proxy saw. When clients connect to Caddy directly (Caddy is the outermost proxy in front of CPM), that is the real client. A client-sentX-Real-IPis no longer trusted. - When port 3000 is reachable directly, clients control
X-Forwarded-For, so the per-client limits are only best effort. Expose the portal (BASE_URL) through Caddy or another proxy that overwritesX-Forwarded-For, and do not publish port 3000 to untrusted networks. - Behind a CDN, the rightmost entry is the CDN edge, so all visitors would share one counter. Set
TRUSTED_CLIENT_IP_HEADERto the CDN's client IP header (e.g.cf-connecting-ip), but only if the origin accepts connections from the CDN alone. Otherwise clients can forge the header. - Leave
TRUSTED_CLIENT_IP_HEADERunset when Caddy is the outermost proxy, because Caddy passesX-Real-IPandCF-Connecting-IPthrough unchanged. A request without a usable value in the configured header falls back toX-Forwarded-For. An invalid header name is ignored and logged asTRUSTED_CLIENT_IP_HEADER is not a valid header name: "<value>".
The same client address is used for the rate limit of the instance sync endpoint on slaves (see Feature Guide Instance Sync).
| Feature | Forward Auth Portal | Authentik Integration |
|---|---|---|
| External service required | No | Yes (Authentik instance) |
| Login portal | Built into CPM | Authentik's portal |
| User management | CPM's user/group system | Authentik's user system |
| OAuth support | Via CPM's OAuth config | Native to Authentik |
| Header forwarding | Session user info | Authentik identity headers |
| Setup complexity | Low | Medium-High |
Use the built-in portal for simple setups. Use Authentik when you need its advanced features (SCIM, LDAP, multi-factor, etc.).
Besides the built-in portal and the Authentik integration, hosts can be protected by any forward-auth server — with first-class presets for Authelia. This also implements the split browser vs API pattern from issue #188 for mixed UI + API services (Klipper/Moonraker, Spoolman, ...).
- Open Proxy Hosts and create or edit a host.
- Expand Generic Forward Auth and enable it.
- Pick a provider preset:
-
Authelia — prefills the endpoint
/api/authz/forward-authand theRemote-*copy headers. - Custom — any forward-auth server; you set the endpoint and copy headers yourself.
-
Authelia — prefills the endpoint
- Set the Auth Server URL (e.g.
http://authelia:9091) and the Auth Endpoint. - Save.
Global defaults for these fields can be set under Settings → Forward Auth Defaults and are synced to slave instances.
Mixed services need different behavior for humans and machines:
-
Browser requests (an
Accept: text/htmlheader withoutX-Requested-With) go through the normal forward-auth flow: the auth server's redirect to its login portal passes through untouched. -
Everything else — API clients and WebSocket handshakes — can get a plain 401 instead of a login page. Enable "Return 401 for non-browser clients" and CPM generates two routes: the browser route keeps the redirect flow, while the API route converts any 3xx from the auth server into
401 Unauthorized. This matters for clients that cannot follow a login redirect mid-handshake (slicers, WebSocket consumers, mobile apps).
For Authelia's /api/authz/forward-auth endpoint the auth server performs this negotiation itself (302 for browsers, 401 for API clients) — the split still helps when the endpoint always redirects, and it guarantees WebSocket handshakes never receive an HTML login page.
To point Authelia at the right portal for redirects, append the portal URL to the endpoint, e.g. /api/authz/forward-auth?authelia_url=https://auth.example.com.
Some services authenticate machine clients themselves (e.g. Moonraker's X-Api-Key). Add such header names under Bypass Headers — requests carrying any of them skip forward auth entirely and go straight to the upstream, which enforces its own key check. This is the maintainable, UI-driven replacement for hand-writing customPreHandlersJson subroutes.
Spoofing protection: every header CPM copies from the auth response (e.g. Remote-User) is automatically stripped from incoming requests on all of the host's routes, so a client cannot forge an identity to the upstream. Authorization, Proxy-Authorization and Cookie are exempt; see Identity Header Stripping.
Same semantics as the other forward-auth modes:
- Protected Paths — only the listed paths require authentication.
- Excluded Paths — all paths require authentication except the listed ones.
| Forward Auth Portal (CPM) | Authentik Integration | Generic Forward Auth | |
|---|---|---|---|
| Auth service | CPM itself | Authentik outpost | Authelia / any forward-auth server |
| User directory | CPM users & groups | Authentik | Authelia (or the external IdP) |
| Browser login | CPM portal | Authentik portal | Auth server's portal redirect |
| API clients | 302 to portal | JSON 401 | 401 via split mode (or auth-server negotiation) |
| Upstream-managed API keys | Excluded paths only | Excluded paths only | First-class bypass headers |
On each request it lets through, the Forward Auth Portal passes the user to the upstream in these headers:
| Header | Value |
|---|---|
X-CPM-User-Id |
The account's id. It does not change, so key users on it. |
X-CPM-User |
The sign-in username, or the email address for an account without one. CPM keeps it from belonging to two accounts, but an administrator can change it. Up to v1.13.1 it was the display name, which users, OAuth providers and ADMIN_USERNAME choose and which is not unique. |
X-CPM-Email |
The email address. |
X-CPM-Groups |
The user's group names, comma-separated. |
Each forward-auth mode deletes client-supplied copies of the identity headers it forwards before any handler that reaches the upstream. This applies to protected, excluded, catch-all and location routes alike, so a client cannot forge an identity:
| Mode | Headers removed from incoming requests |
|---|---|
| Forward Auth Portal (CPM) |
X-CPM-User, X-CPM-Email, X-CPM-Groups, X-CPM-User-Id
|
| Authentik Integration | The configured copy headers (since v1.13.1; older releases did not strip them) |
| Generic Forward Auth | The configured copy headers |
Since v1.13.1:
-
Underscore spellings. Each header is removed in every mix of
-and_(X-CPM-User,X_CPM_User,X_CPM-User, …). Caddy treats-and_as different characters, but CGI/WSGI-style upstreams fold both into the same variable (HTTP_X_CPM_USER), so they could otherwise read a forgedX_CPM_Useras the real header. -
Client credentials are kept. For Authentik and generic forward auth,
Authorization,Proxy-AuthorizationandCookieare never stripped before authentication, even when they are listed as copy headers. They carry the client's own credentials, which excluded paths, access-list basic auth and the auth server itself may need. On protected routes, one listed as a copy header is replaced by the auth server's value, or, since v1.13.2, removed when the auth server returns none (v1.13.0 and v1.13.1 passed the client's value on to the upstream). On excluded paths and API bypass routes the client's credentials reach the upstream unchanged. - Authentik copy header names that are not valid header names are ignored, as in the generic mode.
- Verify the proxy host's domain resolves to Caddy.
- Check that the forward auth callback route is reachable.
- Ensure
BASE_URLis set correctly in.env. - If the site is reached on a port other than 80/443, list that port in
FORWARD_AUTH_ALLOWED_PORTS(see Non-Standard Ports).
The portal shows "This site is served on port <port>, which is not allowed for forward authentication…" and the web container logs [forward-auth] Rejected <host>:<port> …. Add the port to FORWARD_AUTH_ALLOWED_PORTS and recreate the web container (docker compose up -d). See Non-Standard Ports.
- Wait for
LOGIN_BLOCK_MS(15 minutes by default), or recreate the web container to clear the in-memory counters. - If many users are blocked at once, they probably share one client address, e.g. a CDN edge in front of Caddy. Set
TRUSTED_CLIENT_IP_HEADERas described in Client address. - A single account can also be blocked by failures from several clients (see Portal Login Rate Limits). Check the audit log for
forward_auth_login_failedevents.
The portal URL contains rd or rid more than once. Open the protected site again to get a fresh sign-in link.
- Confirm the user is in the host's forward auth access list (directly or via group).
- Check the user's status is "active" on the Users page.
- If the portal's credential form answers "Invalid credentials" for a correct password, the account's email is probably not
<username>@localhost. Sign in with OAuth or through the dashboard instead (see Login Methods).
- Ensure
OAUTH_ENABLED=trueand OAuth credentials are configured in.env.
- Check that the browser actually sends
Accept: text/html(normal navigation does). - A request with
X-Requested-Withset is treated as an API call — this is intentional. - If the API/WebSocket split is not needed, disable "Return 401 for non-browser clients" so one unified route serves everyone.
- Enable "Return 401 for non-browser clients" (API split), or use an auth endpoint that negotiates browser vs API itself (Authelia's
/api/authz/forward-auth). - For clients the upstream authenticates itself, add the header to Bypass Headers (e.g.
X-Api-Key).
- Feature Guide Proxy Hosts - Proxy host configuration
- OAuth Authentication Setup - Configure OAuth providers
- Feature Guide User Management - Users, passwords and sign-in usernames
- Feature Guide Access Lists - HTTP Basic Auth (alternative to forward auth)
-
Environment Variables Reference -
FORWARD_AUTH_ALLOWED_PORTS,TRUSTED_CLIENT_IP_HEADERandLOGIN_* - Security Configuration - Production security hardening
Need help? Open an issue with your host configuration and relevant logs.