๐Ÿ›ก๏ธ Proxy Authentication

August 12, 2026 ยท View on GitHub

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

VariableDescriptionDefault
PROXY_AUTH_SECRETRequired. Shared secret to verify requests.-
PROXY_AUTH_USER_HEADERRequired. Header containing the username.-
PROXY_AUTH_ROLE_HEADERHeader containing user groups/roles.-
PROXY_AUTH_ROLE_SEPARATORSeparator for multiple roles in the header.|
PROXY_AUTH_ADMIN_ROLERole name that grants admin access.admin
PROXY_AUTH_LOGOUT_URLURL 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.

Running Pulse 5.x? Role gating behaves differently there and needs two extra steps โ€” see Pulse 5.x (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)

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

IssueCheck
401 UnauthorizedVerify X-Proxy-Secret matches PROXY_AUTH_SECRET. Check if headers are being stripped by intermediate proxies.
Not AdminCheck 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 FailsEnsure 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

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:

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.