authenticateSSO (server.js) and makeAuthenticatePage (lib/pageauth.js) take identity and authorization straight from the Remote-User, Remote-Name, Remote-Email and Remote-Groups request headers, without checking where the request came from.
Through NPM this is fine, because the Authelia auth_request overwrites those headers. But the server listens on 0.0.0.0:8080, so any host on the LAN can connect directly and supply the headers itself. Choosing the group gets it through the admin/employee check, and it is then treated as whichever user it named, admin included. Since Pulse runs commands on every worker, this amounts to running commands across the fleet without a login.
A side effect is that upsertUser writes whatever the request claims (name, email, groups) into the users table.
Found by a read-only check on 2026-10-26. A direct request to :8080 carrying Remote-User and a non-matching group got the 403 group message instead of the 401 "Not authenticated" message. So the identity header was accepted, and only the group check stopped it. From the internet it is not reachable: NPM sends the request to Authelia first.
Fix
Do what Tinker Tickets does (see tinker_tickets#94 and AuthMiddleware::enforceTrustedProxy):
Add a TRUSTED_PROXIES setting in the env, e.g. TRUSTED_PROXIES=10.10.10.27 for NPM.
In both middlewares, if req.socket.remoteAddress, after stripping the ::ffff: prefix, is not in that list, reject any request that carries Remote-* headers. Never fall back to trusting them.
Fail closed: if TRUSTED_PROXIES is unset, refuse the headers and log a startup warning. The "empty = off" default is what #94 had to document around.
Optionally, also bind to the LAN interface and/or firewall :8080 so only NPM and the workers can reach it.
Related: the WebSocket has no authentication at all (separate issue).
## Problem
`authenticateSSO` (server.js) and `makeAuthenticatePage` (lib/pageauth.js) take identity and authorization straight from the `Remote-User`, `Remote-Name`, `Remote-Email` and `Remote-Groups` request headers, without checking where the request came from.
Through NPM this is fine, because the Authelia `auth_request` overwrites those headers. But the server listens on `0.0.0.0:8080`, so any host on the LAN can connect directly and supply the headers itself. Choosing the group gets it through the `admin`/`employee` check, and it is then treated as whichever user it named, admin included. Since Pulse runs commands on every worker, this amounts to running commands across the fleet without a login.
A side effect is that `upsertUser` writes whatever the request claims (name, email, groups) into the `users` table.
**Found by** a read-only check on 2026-10-26. A direct request to `:8080` carrying `Remote-User` and a non-matching group got the 403 group message instead of the 401 "Not authenticated" message. So the identity header was accepted, and only the group check stopped it. From the internet it is not reachable: NPM sends the request to Authelia first.
## Fix
Do what Tinker Tickets does (see tinker_tickets#94 and `AuthMiddleware::enforceTrustedProxy`):
- Add a `TRUSTED_PROXIES` setting in the env, e.g. `TRUSTED_PROXIES=10.10.10.27` for NPM.
- In both middlewares, if `req.socket.remoteAddress`, after stripping the `::ffff:` prefix, is not in that list, reject any request that carries `Remote-*` headers. Never fall back to trusting them.
- **Fail closed:** if `TRUSTED_PROXIES` is unset, refuse the headers and log a startup warning. The "empty = off" default is what #94 had to document around.
- Optionally, also bind to the LAN interface and/or firewall `:8080` so only NPM and the workers can reach it.
Related: the WebSocket has no authentication at all (separate issue).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
authenticateSSO(server.js) andmakeAuthenticatePage(lib/pageauth.js) take identity and authorization straight from theRemote-User,Remote-Name,Remote-EmailandRemote-Groupsrequest headers, without checking where the request came from.Through NPM this is fine, because the Authelia
auth_requestoverwrites those headers. But the server listens on0.0.0.0:8080, so any host on the LAN can connect directly and supply the headers itself. Choosing the group gets it through theadmin/employeecheck, and it is then treated as whichever user it named, admin included. Since Pulse runs commands on every worker, this amounts to running commands across the fleet without a login.A side effect is that
upsertUserwrites whatever the request claims (name, email, groups) into theuserstable.Found by a read-only check on 2026-10-26. A direct request to
:8080carryingRemote-Userand a non-matching group got the 403 group message instead of the 401 "Not authenticated" message. So the identity header was accepted, and only the group check stopped it. From the internet it is not reachable: NPM sends the request to Authelia first.Fix
Do what Tinker Tickets does (see tinker_tickets#94 and
AuthMiddleware::enforceTrustedProxy):TRUSTED_PROXIESsetting in the env, e.g.TRUSTED_PROXIES=10.10.10.27for NPM.req.socket.remoteAddress, after stripping the::ffff:prefix, is not in that list, reject any request that carriesRemote-*headers. Never fall back to trusting them.TRUSTED_PROXIESis unset, refuse the headers and log a startup warning. The "empty = off" default is what #94 had to document around.:8080so only NPM and the workers can reach it.Related: the WebSocket has no authentication at all (separate issue).