Skip to content

RFC: opt-in trusted reverse-proxy header authentication (forward-auth / SSO) #798

@sntgl

Description

@sntgl

Summary

Add an opt-in mode where CloudCLI trusts an authenticated-username header injected by an upstream reverse proxy doing forward-auth (Authelia, oauth2-proxy, Authentik, Cloudflare Access, …), so users behind SSO don't have to log in a second time.

Problem

When CloudCLI runs behind an authenticating reverse proxy using forward-auth, the proxy already verifies the user and passes the identity downstream as a request header (e.g. Remote-User). Today CloudCLI ignores it and still shows its own bcrypt/JWT login, so the user authenticates twice (SSO challenge + CloudCLI password). There is currently no inbound SSO / reverse-proxy auth mode. (#744 is the opposite direction — host→plugin identity via HMAC — not this.)

Proposed behavior

Mirror the well-established pattern from Gitea (ENABLE_REVERSE_PROXY_AUTHENTICATION + REVERSE_PROXY_TRUSTED_PROXIES) and Grafana ([auth.proxy] + whitelist):

  • TRUSTED_PROXY_AUTH=true — opt-in; off by default → zero behavior change.
  • TRUSTED_PROXY_USER_HEADER (default Remote-User) — header carrying the username.
  • TRUSTED_PROXY_CIDRS (default 127.0.0.0/8,::1/128, loopback-only) — the identity header is honored only when the request's direct socket peer is within these ranges. We check socket.remoteAddress, not X-Forwarded-For, so a client reaching the app directly cannot spoof the header.
  • Single-user invariant preserved: if an account already exists, the header is accepted only if it matches that user; otherwise it is refused (no silent multi-user creation — consistent with the /register guard). On a fresh instance the proxy identity provisions the single user.
  • REST (authenticateToken), the /api/auth/status gate (so the SPA skips the login form) and the WebSocket upgrade (verifyClient) are all covered.

Security notes (for reviewers)

Open questions

  1. CIDR matching — I kept a small dependency-free matcher + node:test unit tests (IPv6 currently loopback/exact-match only). Would you prefer pulling in ipaddr.js for full IPv6 prefix support? It's already transitively present via expressproxy-addr, but I didn't want to add an undeclared/lockfile dependency without your call.
  2. Multi-user — is this the seam where you'd eventually want optional multi-user, or keep it strictly single-user? (I kept it single-user.)
  3. Header namingRemote-User (Authelia/Caddy convention) vs Gitea/Grafana's X-WEBAUTH-USER. Configurable either way; just confirming the doc default.

I have a working implementation running in my homelab (Caddy + Authelia forward-auth) and am happy to open a PR — following the "discuss first" note in CONTRIBUTING before doing so. Implementation is AGPL, same as the project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions