Authentication
The admin signs people in with local accounts, OpenID Connect or headers set by a trusted reverse proxy. Each user has a role. API clients authenticate with OAuth 2.0 instead: see REST API.
Local accounts
- Passwords are hashed with PBKDF2-HMAC-SHA256 (600 000 iterations) and must be at least 12 characters.
- Accounts live in
users.json. - Manage them in the admin or with
redirect-proxy user ….
OpenID Connect
- Uses the authorization code flow with PKCE,
stateandnonce. - The ID token signature is verified against the provider JWKS, and
iss,aud,exp,nbfand (when present)azpare checked. - The provider metadata and JWKS are cached for an hour, and the last good ones are used for up to a day more while the provider cannot be reached. A plain
http://issuer on another host gets a startup warning. - To allow users, set
allowed_emails,allowed_domains(checked against verified emails) and/orallowed_groups(fromgroups_claim). With all three lists empty the server does not start: setallow_any_account = trueto accept every account of the provider (a provider of your own, never a public one such as Google, where anyone can make an account). It then logs a warning at startup (andcheck-configreports it). - An email counts as verified only when the ID token says
email_verified: true. Providers that never send that claim, such as Entra ID, can only be restricted withallowed_groups. - Register
<admin.public_url>/auth/oidc/callbackas the redirect URI at the provider. - After signing in, you land on the page that asked for it, as with a password.
- With
logout_at_provider = true, signing out also ends the session at the provider (itsend_session_endpoint), which sends you back to the login page: register<admin.public_url>/loginas a post-logout redirect URI there.
Trusted proxy headers
With auth.header enabled, the user named in user_header (X-Forwarded-User) is signed in. Set user_header = "" to use email_header (X-Forwarded-Email) instead. The two are never combined. The header is only accepted when the TCP peer is in trusted_proxies, and a request carrying several values in it is refused. Configure the proxy to set (not append to) that header, and never expose the admin port directly in this mode. The session ends on the first request that arrives without the header (signed out at the proxy, or a replayed cookie). A user and role have one session: a client that does not keep cookies (a script, a monitoring check) reuses it rather than starting a session, and an auth.login audit entry, on every request.
Narrow trusted_proxies
The default, ["127.0.0.1/32", "::1/128"], trusts every process on the host, not only the authenticating proxy: any local service that can be made to send a request to the admin port (a website behind server.upstream with a server-side fetch, a monitoring agent, a shell user), and redirect-proxy's own upstream connections when server.upstream points at the admin port, can sign in as anyone by setting the header. Set trusted_proxies to the address the proxy connects from, and only that: its container or pod address, or a dedicated address no other process uses (a separate network or interface). Keep the admin listener (admin.listen) off addresses other clients can reach, and never point server.upstream at the admin port.
The website behind server.upstream
The redirect listener drops these identity headers (and their _ spellings) from clients outside auth.header.trusted_proxies, even those in access_log.trusted_proxies, only while auth.header is enabled. When the website behind server.upstream trusts such headers itself (behind oauth2-proxy, for instance), make sure the proxy in front sets them on every request, so that a client cannot send its own.
Roles
Every session has a role, and each includes the one before:
| Role | May |
|---|---|
viewer | See the rules (with their details), test URLs, export, change their own password. |
editor | Also create, change, delete, enable and disable rules, import, and reload. |
admin | Also manage local users and API clients, and read the audit log (which records sign-ins with client addresses). |
Pages and buttons a role cannot use are not shown, and the server answers 403 to their requests. Roles come from:
- Local accounts: the role stored with the user, chosen when the user is created (
redirect-proxy user add <name> --role editor, or the admin's users page) and changed there or withredirect-proxy user role <name> <role>. A change applies to the user's sessions at once. Accounts created before roles are administrators, and the last administrator cannot be demoted or deleted. Every role can change its own password on the account page. - OpenID Connect:
admin_groups,editor_groupsandviewer_groupsname groups ofgroups_claim. A user gets the highest role their groups give, anddefault_role(viewer) when they are in none of them. Setadmin_groups, ordefault_role = "admin", for someone to manage users and API clients: the server warns at startup when no one can get the admin role. The role is set at sign-in: a change of groups at the provider applies at the next sign-in, at the latest afteradmin.session_ttl. - Trusted proxy headers: with
role_header(for exampleX-Forwarded-Role), the proxy sets the role of each request; a missing, repeated or unknown value givesviewer, and the session follows changes of the header. Without it, every proxy user getsdefault_role(viewer).
API clients are not affected: they have scopes (see REST API).
Security measures
- Sessions. Server-side and revocable, using signed
HttpOnly,SameSite=Laxcookies.- A session ends after
session_ttl, or aftersession_idle_timeoutwithout a request. - A local account's sessions end when the user is deleted or their password changes, including from the CLI. Changing your own password keeps you signed in, in a new session.
- SSO logins are bound to the browser that started them.
- Signing in ends the session the browser held before (except a proxy-header session, which the user's other browsers share).
- The server keeps a hash of each session token, not the token: a memory or crash dump gives no session to replay.
- A session ends after
- Passwords. Changing a local password, yours or another user's, needs your current password and a local session: administrators signed in with SSO or proxy headers cannot change local passwords. Deleting a user or an API client, and rotating a secret, ask for a confirmation.
- CSRF. Every form carries a token, and
Origin/Refererare checked. - Login throttling. Over 15 minutes, a client may fail 10 times for one account and 20 times in all; an account accepts 100 failures from all clients. IPv6 clients are counted by /64, since one host usually holds a whole /64. A browser where the account signed in with its password before (a signed
rp_devicecookie, kept 90 days) is not held back by the account's limit, so failures from elsewhere cannot lock its owner out. The API token endpoint allows 10 failures for one client id and 30 in all per client address. There is no limit per client id alone: secrets are too long to guess, so such a limit would only let anyone lock a pipeline out. Token requests are limited to 64 KB. A client may start 30 SSO logins that it does not complete (completed ones are not counted, so many people behind one address can sign in) and fail 20 SSO callbacks; requests to them a browser says are not navigations of the page (Sec-Fetch-Mode,Sec-Fetch-Dest), such as images or frames on another site, are refused uncounted, so that a page cannot spend its visitors' budget. The API accepts 100 invalid access tokens from one client before answering 429 without checking more. A throttled request gets a 429 withRetry-After. An attempt is counted before the password is checked, so parallel guesses get no more than the limits. Password checks also run at most a few at once (half the CPU cores, at least 2): past that, a login gets a 503 withRetry-After: 1, so that a burst of logins leaves CPU for the redirect listener. Behind a proxy, list it inaccess_log.trusted_proxiesso that the client address is the forwarded one: otherwise every client has the proxy's address and one client's failures throttle everyone (the server warns at startup whenadmin.public_urlis not on the machine and the list is empty). - Input limits. Admin forms are limited to 64 KB (imports: like the API,
api.max_body_bytes) and usernames to 64 bytes. - HTTP headers. A strict Content-Security-Policy (no inline scripts),
X-Frame-Options: DENYandnosniff. Whenadmin.public_urlishttps://, responses carryStrict-Transport-Security(so does the API on its own port whenapi.public_urlishttps://) and the session and SSO state cookies are named__Host-rp_sessionand__Host-rp_oidc_state, which a sibling subdomain cannot set or shadow.
Behind a reverse proxy. Keep the
Hostheader unchanged so that the origin check passes. Serve the admin over HTTPS so that cookies get theSecureflag, and sendX-Forwarded-Proto.
