From b8d61b9e1240be042ab86e724e37910dedda5e9d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 12 Aug 2026 12:16:24 +0100 Subject: [PATCH] Sync shipped PROXY_AUTH.md with the repo doc The proxy-auth documentation commits this morning updated docs/PROXY_AUTH.md without refreshing the copy shipped in frontend-modern/public/docs, so the docsLinks shipped-content sync test went red on the next push that ran the frontend suite. --- frontend-modern/public/docs/PROXY_AUTH.md | 37 +++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend-modern/public/docs/PROXY_AUTH.md b/frontend-modern/public/docs/PROXY_AUTH.md index 03cd41510..830ee82f9 100644 --- a/frontend-modern/public/docs/PROXY_AUTH.md +++ b/frontend-modern/public/docs/PROXY_AUTH.md @@ -23,10 +23,21 @@ Authenticate users via your existing reverse proxy (Authentik, Authelia, Cloudfl | `PROXY_AUTH_ADMIN_ROLE` | Role name that grants admin access. | `admin` | | `PROXY_AUTH_LOGOUT_URL` | URL to redirect to after logout. | - | -If `PROXY_AUTH_ROLE_HEADER` and `PROXY_AUTH_ADMIN_ROLE` are configured, admin access fails closed unless the role header is present and contains the configured admin role. A missing or blank role header still authenticates the user, but the user is treated as non-admin. +Setting `PROXY_AUTH_ROLE_HEADER` turns on role gating. From then on admin access fails closed unless the role header is present and contains the admin role — `PROXY_AUTH_ADMIN_ROLE` if you set it, otherwise the `admin` default. A missing or blank role header still authenticates the user, but the user is treated as non-admin. If you intentionally want every proxy-authenticated user to be an admin, leave `PROXY_AUTH_ROLE_HEADER` unset and protect Pulse entirely at the proxy/IdP layer. +Running Pulse 5.x? Role gating behaves differently there and needs two extra steps — see [Pulse 5.x (end-of-life)](#pulse-5x-end-of-life). + +## ⚠️ Header Trust Boundary + +Pulse trusts these headers completely — they *are* the identity and the privilege decision. Two deployment requirements make that safe, and both are yours to enforce: + +1. **Your proxy must _replace_ these headers, never append to them.** On every request the proxy has to discard any client-supplied copy of `X-Proxy-Secret`, your user header, and your role header, then set its own. Pulse reads the **first** value of a repeated header, so a client-supplied `X-Proxy-Roles: admin` that arrives ahead of your proxy's value wins — and because the proxy supplies the shared secret itself, the client never needs to know it. The examples below use nginx `proxy_set_header` and Traefik `customRequestHeaders`, which replace; be careful with anything that adds a header rather than setting it. +2. **Pulse must not be reachable except through the proxy.** Anyone who can connect directly and knows `PROXY_AUTH_SECRET` can assert any username and any role. Bind Pulse to the proxy's network or to localhost. + +Neither of these can be enforced from inside Pulse: a forged header and a genuine one are indistinguishable once they arrive. + ## 📦 Examples ### Authentik (with Traefik) @@ -65,7 +76,7 @@ location / { | Issue | Check | | :--- | :--- | | **401 Unauthorized** | Verify `X-Proxy-Secret` matches `PROXY_AUTH_SECRET`. Check if headers are being stripped by intermediate proxies. | -| **Not Admin** | Verify `PROXY_AUTH_ROLE_HEADER` is set and contains `PROXY_AUTH_ADMIN_ROLE`. | +| **Not Admin** | Check the role header actually reaches Pulse and contains the admin role **exactly** — matching is case-sensitive and compares whole roles, so `Admins` and `authentik Admins` do not match the `admin` default. Set `PROXY_AUTH_ADMIN_ROLE` to your IdP's admin group name. Pulse logs a warning at startup when it falls back to the default. | | **Logout Fails** | Ensure `PROXY_AUTH_LOGOUT_URL` is set to your IdP's logout endpoint. | ### Verify Headers @@ -75,3 +86,25 @@ curl -H "X-Proxy-Secret: your-secret" \ -H "X-Authentik-Username: admin" \ http://localhost:7655/api/state ``` + +## Pulse 5.x (end-of-life) + +The 5.x line is end-of-life and does not receive fixes. **Upgrade to 6.2.2 or later**, which is where proxy-auth role gating behaves as documented above. + +If you cannot upgrade immediately, 5.x needs two configuration changes before role gating actually restricts anyone. **Both are required** — either one alone leaves administrator access open: + +1. **Set `PROXY_AUTH_ADMIN_ROLE` explicitly.** 5.x never applies the documented `admin` default. While it is empty, 5.x skips role checking altogether and treats every proxy-authenticated user as an administrator, whatever their role header says. +2. **Send a non-empty role header on every authenticated request.** 5.x only evaluates roles when that header carries a value; an absent or empty header leaves the user an administrator. A user with no groups in your identity provider commonly produces exactly that, so have the proxy send a placeholder such as `none` rather than nothing. Any non-empty value that does not contain your admin role correctly resolves to non-admin. + +Verify both with a request carrying a non-admin role — it should be refused: + +```bash +curl -si -H "X-Proxy-Secret: your-secret" \ + -H "X-Authentik-Username: testuser" \ + -H "X-Proxy-Roles: none" \ + http://localhost:7655/api/system/settings | head -1 +``` + +Expected: `HTTP/1.1 403 Forbidden`. Repeat it with the role header omitted entirely — that must also be refused. If either returns `200`, admin access is still open. + +6.2.2 and later fix both behaviors: role gating activates on the presence of the role header alone, and an absent or blank header resolves to non-admin.