Users, roles & groups
The four roles
Section titled “The four roles”| Capability | Viewer | User | Operator | Admin |
|---|---|---|---|---|
| Log in to the dashboard | Yes | Yes | Yes | Yes |
| Access forward-auth apps (when granted) | Yes | Yes | Yes | Yes |
| Manage hosts and agents - edit, enable, delete | No | No | Only what their groups were granted | Yes |
| Create hosts | No | No | No | Yes |
| Manage certificates and access lists | No | No | No | Yes |
| Manage users, groups and settings | No | No | No | Yes |
| View analytics, logs and the audit log | No | No | No | Yes |
| Create and manage own API tokens | Yes | Yes | Yes | Yes |
New accounts default to user. The first administrator comes from first run. There is always at least one active administrator: a role change, a disable or a delete that would remove the last one is refused, and an OIDC role sync skips that demotion - even when two of them happen at the same moment.
Operator is the delegating role
Section titled “Operator is the delegating role”Its baseline is nothing, and it reaches exactly what its groups were granted. That shape is what makes grants safe to add: viewer and user gain nothing from a grant, so creating one can never widen an existing account. Someone has to be made an operator deliberately.
Grants apply to the dashboard. The management endpoints under /api/v1/ stay admin-only - an
operator’s API token gets the same user-scoped endpoints a user’s does.
Groups
Section titled “Groups”Groups are lists of users doing two jobs:
- Gating access to forward-auth protected apps.
- Deciding what an operator may manage, through Groups → Access: choose the capability - manage, or view only - then tick the proxy hosts, L4 hosts and agents the group should reach.
Viewing as a role
Section titled “Viewing as a role”To check what a delegated operator can actually reach, an administrator can view the dashboard as one: Users → View as a role (the eye beside the heading), or View as an operator in this group on a group. Pick operator, user or viewer, and for an operator, the groups to be in.
- Nobody is impersonated. CPM grants to roles and groups, not to people, so that is what it previews. The view lives on the administrator’s own session, and anything done while viewing is done - and recorded in the audit log - as them.
- It can only narrow. Pages, menus and every permission check follow the chosen role and groups, over the REST and GraphQL APIs with the same session too. It can’t be used to gain anything.
- A banner says so on every page, with Return to my view. The view also ends by itself after an hour.
- Two things wait until you return: creating an API token, which would carry your real role, and signing in to a host behind CPM’s forward auth.
Signing in
Section titled “Signing in”The sign-in screen asks for the username on its own, then the password. The dashboard’s /login
and the forward-auth portal work the same way.
- It is still one form. The password field is in the page from the start, only hidden, so a password manager can fill both at once - and when it has, Continue signs straight in.
- Any username continues. The first step never checks whether the account exists, or which provider it belongs to, so the screen cannot be used to find out which usernames are real.
- The username stays on screen for the second step, with a way to change it, so a typo in it can be told apart from a wrong password.
- The primary provider stands out. Its button is filled with a lighter tint of the accent; the other providers are plain. The solid accent stays with Sign in.
Caddy Proxy Manager
Enter the username for this instanceCAPTCHA
Section titled “CAPTCHA”Settings → Authentication → CAPTCHA puts a challenge on the username step of both the dashboard and the forward-auth portal: Google reCAPTCHA v2, hCaptcha, Cloudflare Turnstile, or a self-hosted Cap instance. With one configured, Continue only moves on once it is solved.
- The server enforces it, not just the form. A solve earns a pass for that one username, and the password endpoint refuses any sign-in without one - a script posting straight to the API gets nowhere.
- One solve, one password attempt. The pass is spent by the attempt whether the password was right or not, and the server remembers it was, so replaying it does nothing. After a wrong password the CAPTCHA appears again beside the password field, rather than sending you back to the username. An unused pass lapses after ten minutes, or when the controller restarts.
- A failed solve and a broken setup read differently. A wrong answer is refused as failed. The provider not answering, or refusing the secret or site key - a wrong Cap key pair included - is reported as unavailable, since that one is the administrator’s to fix.
- Each portal host can opt out. A host using the built-in forward auth has Require the sign-in CAPTCHA, on by default. Turned off, that host’s portal skips it - and its sign-in is open to scripted guessing again, held back only by the login throttles.
- Single sign-on is untouched, and in OIDC-only mode there is no username step to put it on.
- The sign-in pages load the provider’s script and are given a Content Security Policy that allows that provider’s origins, on those pages only.
Forgotten passwords and invitations
Section titled “Forgotten passwords and invitations”With email set up, the password step offers Forgot your password?, which emails a single-use link to a local account. Users can then also create an account by invitation, so its owner chooses the password, and send any local account a fresh link later.
Two-factor sign-in
Section titled “Two-factor sign-in”Anyone with a password can add a code from an authenticator app, from Profile → Two-factor sign-in. Turning it on asks for the password, shows a QR code and the key behind it, and takes one code to prove the app is set up. Ten backup codes follow, shown once - each signs in a single time if the phone is lost.
- It applies to the forward-auth portal too. A portal sign-in with a correct password asks for the code before it lets anyone through to the host. The portal doesn’t offer to remember the device; the dashboard does, for 30 days.
- Wrong codes lock the second factor. Ten in a row, across sign-ins and across the dashboard and portal, locks it for fifteen minutes.
- Single sign-on accounts are left to their provider. There is no password for a second factor to protect, so the section says so rather than offering it.
- Administrators can be required to use it. Settings → Authentication → Two-factor Sign-in sends an administrator who signs in with a password and hasn’t set it up to do that first, before anything else in the dashboard or the API answers them.
An administrator can reset another user’s second factor from Users, which also signs that user out everywhere. For the one administrator who has lost both the phone and the backup codes, the server can reset it from inside its own container:
docker compose exec web /app/cpm-server --reset-2fa adminIt asks the running server to do it, so the reset is in the audit log like any other, and it only
works from inside the container: the request is signed with a key derived from SESSION_SECRET and
answered only on the container’s own loopback. It removes the user’s passkeys too.
Passkeys
Section titled “Passkeys”A passkey signs in with a fingerprint, a face or the device’s PIN, and no password. Profile → Passkeys lists them, renames and removes them, and adds new ones; the sign-in screen and the forward-auth portal get a Sign in with a passkey button, and browsers that support it also offer saved passkeys in the username field’s autofill.
- A passkey is both factors. The authenticator must verify the person, not just that someone is present, and a passkey that did not is refused. So a passkey sign-in never asks for a two-factor code, and skips the CAPTCHA and the per-account login lockout, which exist to slow password guessing: there is no name to guess against, and no guess without the private key. Better Auth’s request limit (Settings → Authentication → Sign-in) still applies to each address.
- It does not count as two-factor sign-in. An administrator required to use two-factor sign-in who also has a password still has to set up an authenticator app.
- Adding one needs a recent sign-in. It is a new way into the account, so a session more than ten minutes old is asked to sign in again first - a borrowed or stolen one usually is older.
- It needs HTTPS and the Public URL’s hostname. Every passkey belongs to the hostname of the
Public URL, and browsers only offer it on that hostname or a subdomain of it, over HTTPS or
on
localhost. Athttp://192.168.1.10:3000the Profile section says why it can’t add one. Changing the Public URL to another hostname orphans every passkey already registered, which Settings → General → Instance warns about while any exist. - The portal works because it is on the Public URL. A host behind CPM’s forward auth still sends people to the portal on the dashboard’s address, where the passkey belongs.
- The last way in can’t be removed. Removing the only passkey of an account with no password and no linked provider is refused; with a passkey, the password can be removed instead.
- Not in OIDC-only mode, where the identity provider is the only way in.
An administrator can remove another user’s passkeys from Users, next to the two-factor reset, which signs them out everywhere too. Removing a passkey here doesn’t delete it from the device or password manager that holds it; it just no longer signs in.
OAuth / SSO
Section titled “OAuth / SSO”Any OAuth2/OIDC provider - Authentik, Keycloak, Auth0, and others. Accounts can be linked from the Profile page, so an existing local account gains SSO rather than becoming a duplicate.
A provider sign-in that is refused comes back to the sign-in screen with the reason. The common one is an email that already belongs to an account the provider is not linked to: sign in with the password, then link the provider from Profile. Once one is linked, Profile can also remove the password, after confirming the current one, so the account signs in through the provider only.
Two options worth knowing:
- Group-based role mapping. Members of a claimed group become admins, operators, users or viewers automatically, so role changes happen in your IdP rather than here. Role when no group matches, per provider, sets the role for anyone in none of them; it defaults to user.
- OIDC-only mode. Disables local accounts entirely. No bootstrap admin, no credential sign-in - every identity comes from the provider.
LDAP and Active Directory
Section titled “LDAP and Active Directory”Settings → Authentication → Directories (LDAP) adds an LDAP server, and its users sign in on the ordinary sign-in form and the forward-auth portal with their directory username and password. Nothing about a directory is set by environment variable.
- One directory is invisible. The form tries an account on this instance first, then the directory, so local accounts keep working and nobody has to pick. With several directories the form shows a Sign in with selector instead.
- How a user is found. A service account (the bind DN) searches the base DN with the user
filter, which has to match exactly one entry; then CPM binds as that entry with the typed
password. Or, with Bind as the user, there is no service account: the typed name fills a
bind name, such as
uid={username},ou=people,dc=example,dc=orgfor OpenLDAP. The typed name is escaped for the filter or the DN, so*or)(in it is just a character. OpenLDAP and Active Directory buttons fill in the usual filters. - Active Directory without a service account. Choose Bind as the user and the Active
Directory preset fills in a UPN,
{username}@ad.example.orgfrom the base DN; a down-level name such asEXAMPLE\{username}works too. This is the recommended setup when there is no service account. CPM binds with the user’s own password, then finds their entry with the user filter using the user’s own rights, and reads their groups, nested ones included, the same way. The entry found must be the account that bound - itsuserPrincipalNamefor a UPN, itssAMAccountNamefor a down-level name - or the sign-in is refused, so a filter matching too much can never sign someone in as another person. A UPN or down-level name can’t be escaped, so in these two forms a username may hold only letters, digits, dots, hyphens and underscores; anything else is refused like a wrong password. These forms needldaps://or StartTLS, and saving without either is refused, since every sign-in sends the user’s real password. - TLS is verified. Use
ldaps://, orldap://with StartTLS; a server that refuses the upgrade fails the sign-in rather than carrying on unencrypted. A private CA’s certificate can be pasted in. Turning verification off works, and says loudly what it gives up. - The first sign-in makes the account, when Let a first-time OAuth identity create an
account is on (Settings → Authentication → Sign-in); otherwise an administrator’s account
has to exist and be linked first. The account is tied to the entry’s
objectGUIDorentryUUID, which survive a rename, never to its name. - An existing account is joined only by choice. An entry whose email matches an existing account is refused, unless Link accounts by email is on for that directory. Turn that on only where administrators, not users, set the directory’s email addresses: whoever holds an address takes over the account with it, an administrator’s included.
- No password here. A directory account has no local password and can’t set one; Profile says its password is managed by the directory. Disabling someone in the directory stops their next password sign-in, but not a session they already have or a passkey they added - disable them in Users as well.
- A passkey lasts as long as the directory. For someone the directory is the only way in for, a passkey stops working while the directory is disabled, and deleting the directory removes their passkeys and signs them out.
- Groups work as for OAuth. Groups come from the entry’s
memberOf, or from a search for groups naming the user - Active Directory’s preset searches nested groups too. Each group is known by its name, the first part of its DN:CN=Proxy Admins,OU=Groups,...isProxy Admins. Role mapping, the group prefix and explicit group mappings behave exactly as they do for a provider. - The same door as a password. A directory sign-in goes through the CAPTCHA and the per-account lockout, asks for a two-factor code when the account has one, and is audited once it completes. Every refusal - a wrong password, an unknown name, a name matching two entries, a filter finding someone other than who bound, a directory that is down - answers “Invalid username or password”; the reason is in the server log.
- Test before saving. Test connection checks the address, the TLS handshake, the bind and the base DN, and with a test username and password signs in as that user and shows the groups and role it would get. Binding as the user has no account to test with, so without a test user it checks only the address and TLS, and says so. A stored bind password is only ever sent to the address and bind DN it was saved with: change either and it has to be typed again.
Two-factor sign-in can’t be set up on a directory account (it protects a local password, and there isn’t one), so Require two-factor sign-in for administrators leaves directory administrators to the directory, as it does OAuth ones.
Password policy
Section titled “Password policy”Production enforces strong passwords, and two independent throttles guard the auth endpoints: a request limit and a login lockout, both configurable in Settings. Accounts still on an older password hash can be forced to reset, which rehashes them on next sign-in.
An account that keeps failing is locked for a delay that doubles with each failure past the fifth,
from 1 second up to 15 minutes, whichever address the guesses come from. The number of free
failures, the first delay and the cap are fields under Settings → Authentication → Sign-in,
next to a switch that turns the lock off. A sign-in to a locked account gets a 429
with the code ACCOUNT_LOCKED, the wait in seconds as retryAfter in the body and a Retry-After
header, so the sign-in page and the forward-auth portal can say how long. It replaced
TOO_MANY_REQUESTS for that response. The per-address limit keeps its generic message.
Disable accounts after repeated failed sign-ins, off by default in the same place, goes further: once an account reaches that many failures (10 by default), it is disabled until an administrator enables it again. The count is the lock’s own, so it is forgotten a day after the last failure and a successful sign-in resets it. Turning it on means anyone who knows a username can disable that account by guessing at it, which is why it is off.
- The sign-in answers as for any wrong password, so the guesser is not told.
- The last active administrator is never disabled. It stays on the timed lock, and the administrators are notified either way.
- A directory account is left to its directory. A refused LDAP password names no CPM account, so it never disables one; use the directory’s own lockout.
- Enabling it on Users starts the count over, and the user’s page says an account was disabled
by failed sign-ins rather than by an administrator. It is audited as
user_disabled_failed_sign_ins.
If the account disabled is the only administrator’s, the server can enable it from inside its own container, the same way it resets a second factor:
docker compose exec web /app/cpm-server --enable-user admin