Pulse/docs/PROXY_AUTH.md
rcourtman 46f11fffc4 Document the proxy auth header trust boundary
The proxy auth docs never told operators that the identity and role headers
must be replaced rather than appended, nor that Pulse has to be unreachable
except through the proxy. Both are prerequisites for the scheme being safe at
all, and neither is enforceable from inside Pulse.

The append case is the sharp edge: Pulse reads the first value of a repeated
header, so a client-supplied role header that arrives ahead of the proxy's
value decides the admin verdict. The client does not need the shared secret to
do it, because the proxy attaches the secret itself. Verified against a scratch
instance: sending "X-Proxy-Roles: user" then "admin" yields 403 while "admin"
then "user" yields 200 on /api/system/settings.

Documented rather than fixed in code on purpose. Matching any value instead of
the first would make injection strictly easier, and rejecting repeated headers
outright would break identity providers that legitimately emit one header per
group. The trust boundary is the proxy's to hold.
2026-08-12 09:47:55 +01:00

4.2 KiB

🛡️ Proxy Authentication

Authenticate users via your existing reverse proxy (Authentik, Authelia, Cloudflare Zero Trust, etc.).

🚀 Quick Start

  1. Generate Secret: Create a strong random string.
  2. Configure Pulse:
    PROXY_AUTH_SECRET=your-random-secret
    PROXY_AUTH_USER_HEADER=X-Authentik-Username
    
  3. Configure Proxy: Set the proxy to send X-Proxy-Secret and the user header.

⚙️ Configuration

Variable Description Default
PROXY_AUTH_SECRET Required. Shared secret to verify requests. -
PROXY_AUTH_USER_HEADER Required. Header containing the username. -
PROXY_AUTH_ROLE_HEADER Header containing user groups/roles. -
PROXY_AUTH_ROLE_SEPARATOR Separator for multiple roles in the header. |
PROXY_AUTH_ADMIN_ROLE Role name that grants admin access. admin
PROXY_AUTH_LOGOUT_URL URL to redirect to after logout. -

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.

⚠️ 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)

docker-compose.yml:

environment:
  - PROXY_AUTH_SECRET=secure-secret
  - PROXY_AUTH_USER_HEADER=X-Authentik-Username

Traefik Middleware:

headers:
  customRequestHeaders:
    X-Proxy-Secret: "secure-secret"

Authelia (Nginx)

location / {
    auth_request /authelia;
    proxy_set_header X-Proxy-Secret "secure-secret";
    proxy_set_header Remote-User $upstream_http_remote_user;
    proxy_pass http://pulse:7655;
}

Cloudflare Tunnel

  1. Zero Trust Dashboard: Applications → Add Application.
  2. Settings: HTTP Settings → HTTP Headers.
  3. Add Header: X-Proxy-Secret = your-secret.
  4. Pulse Config: PROXY_AUTH_USER_HEADER=Cf-Access-Authenticated-User-Email.

🔧 Troubleshooting

Issue Check
401 Unauthorized Verify X-Proxy-Secret matches PROXY_AUTH_SECRET. Check if headers are being stripped by intermediate proxies.
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

Use curl to simulate a proxy request:

curl -H "X-Proxy-Secret: your-secret" \
     -H "X-Authentik-Username: admin" \
     http://localhost:7655/api/state