ipx-web strips only the Cf-Access header names, while ipx trusts whatever header trusted_header names from Caddy's address #323
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it.
ipx accepts the header named by [web] trusted_header as the signed-in identity when the TCP peer is in trusted_proxies (web.rs:244). Behind ipx-web every request comes from Caddy, so trusted_proxies has to list Caddy's address. The shipped Caddyfile removes client copies of only Cf-Access-Authenticated-User-Email and Cf-Access-Jwt-Assertion (Caddyfile:78-80). If an operator sets trusted_header to another name (docs/sso.md:181 suggests X-authentik-username, marked 'not verified'), leaves access_team/access_aud empty, and nothing in front overwrites or removes that header, then any client that reaches ipx-web can send that header with any account name, including the admin's. With auto_create_users on (the default), an unknown name gets a new account (web.rs:127-136). Whether a deployment is exposed is a configuration fact. Production, per CLAUDE.md, uses the Cf header with JWT verification, and in that mode the name comes from the verified token (web.rs:250-255).
Claimed root cause. The ingress strip list in web/Caddyfile is hard-coded to Cloudflare's header names. The header ipx trusts is a free configuration value, and nothing ties it to that list. Without access_team/access_aud, trust in the plain header rests entirely on the proxy having removed client copies.
Local check. {'local': 'On a scratch stack (ipx on SQLite with a dummy admin account 'dummy-admin', and ipx-web from Dockerfile.web with TUNNEL_PEER unset), set trusted_header = "X-authentik-username", trusted_proxies = [ipx-web's container address], and leave access_team/access_aud empty. From a client on another network, send GET /api/me through ipx-web with the header 'X-authentik-username: dummy-admin' and no cookie or token. The candidate is confirmed if the response is 200 and names dummy-admin. As a control, repeat with trusted_header = Cf-Access-Authenticated-User-Email: it should fall through to 401, because Caddy strips that header. Fix: have Caddy strip the configured header too (e.g. a TRUSTED_HEADER env placeholder in the @forged block), or have ipx refuse a plain trusted_header unless access() is set or the name is one ipx-web strips, and add that case to the vouched_name tests.', 'deployment': "The owner reads [web] in the deployed config.toml and confirms that trusted_header is Cf-Access-Authenticated-User-Email and that both access_team and access_aud are non-empty. The deployment is not exposed if so. If another header name is used, confirm that the hop in front of ipx-web always overwrites or removes the client's copy."}
Owner check (no probing). The owner reads [web] in the deployed config.toml and confirms that trusted_header is Cf-Access-Authenticated-User-Email and that both access_team and access_aud are non-empty. The deployment is not exposed if so. If another header name is used, confirm that the hop in front of ipx-web always overwrites or removes the client's copy.
Full detail:
/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md(fingerprintweb/Caddyfile:forged-header-strip-fixed-names-vs-configurable-trusted_header).Fixed in
df64353: the Caddyfile also strips the header named by TRUSTED_HEADER (default: Cloudflare's) from anyone but TUNNEL_PEER, documented in docs/configuration.md. Production sets nothing and uses Cloudflare's header, so it is unchanged. Deployed in ipx-web.