diff --git a/.env.example b/.env.example index 4b76f6f..a015805 100644 --- a/.env.example +++ b/.env.example @@ -49,19 +49,26 @@ APP_DOMAIN= ; Include all domains that can access this application ALLOWED_HOSTS=localhost,127.0.0.1 +; ============================================================================ +; REQUIRED FOR PRODUCTION -- READ BEFORE DEPLOYING -- TRUSTED_PROXIES +; ============================================================================ ; Trusted reverse proxy IPs, comma-separated -- e.g. the Authelia/nginx proxy. ; Set this to the IP address(es) of your reverse proxy. Authelia forward-auth ; headers (Remote-User / Remote-Groups) and forwarded client IPs are only ; trusted when REMOTE_ADDR is in this list. ; ; Leaving this EMPTY disables reverse-proxy verification entirely: the app then -; trusts Remote-User / Remote-Groups headers from ANY source. That is unsafe if -; the PHP backend is reachable directly (bypassing the proxy), because a client -; can then spoof those headers and log in as an admin. Only leave it empty when -; network topology guarantees PHP is reachable solely via the trusted proxy. +; trusts Remote-User / Remote-Groups headers from ANY source. If the PHP +; backend is reachable directly -- a misconfigured firewall rule, a container +; network accidentally exposing the port, SSRF from another internal service +; -- ANYONE can set Remote-User: admin themselves and fully impersonate any +; user, including an admin, with ZERO authentication. Only leave it empty when +; network topology guarantees PHP is reachable solely via the trusted proxy +; (e.g. local development), never in a real deployment. ; ; Exact IP match only (no CIDR). Example (single proxy): TRUSTED_PROXIES=10.10.10.27 ; Example (multiple): TRUSTED_PROXIES=10.10.10.27,10.10.10.28 +; ============================================================================ TRUSTED_PROXIES= ; Timezone (default: America/New_York) diff --git a/README.md b/README.md index ac755b4..7f0cabd 100644 --- a/README.md +++ b/README.md @@ -447,6 +447,21 @@ APP_DOMAIN=your.domain.example TIMEZONE=America/New_York ``` +**⚠️ REQUIRED FOR PRODUCTION — `TRUSTED_PROXIES`:** This app trusts Authelia +forward-auth headers (`Remote-User`, `Remote-Groups`, etc.) to identify who's +logged in. `TRUSTED_PROXIES` restricts that trust to requests that actually +came through your reverse proxy — **leaving it empty disables that check +entirely**, and anyone who can reach the PHP backend directly (a +misconfigured firewall rule, an exposed container port, SSRF from another +internal service) can set `Remote-User: admin` themselves and fully +impersonate any user with zero authentication. Set it to your reverse proxy's +IP address(es) before deploying anywhere reachable beyond your own machine: +```env +TRUSTED_PROXIES=10.10.10.27 +``` +`GET /api/health.php` reports a `warning` on the `trusted_proxies` check if +this is left empty, so it doesn't go unnoticed after deployment. + Matrix notification variables (all optional): ```env # hookshot generic webhook URL — send events to Matrix room diff --git a/api/health.php b/api/health.php index 8be48e4..dabf0fd 100644 --- a/api/health.php +++ b/api/health.php @@ -162,6 +162,21 @@ if ($maxExecTime === 0 || $maxExecTime >= $requirements['min_max_execution_time' ]; } +// Check 8: TRUSTED_PROXIES configured. Empty disables enforceTrustedProxy()'s +// allowlist entirely, meaning anything that can reach this app directly can +// spoof the Authelia forward-auth Remote-* headers and impersonate any user, +// including an admin. Not fatal (a fresh/dev install may not sit behind a +// proxy yet), but should never go unnoticed on a real deployment. +if (!empty($GLOBALS['config']['TRUSTED_PROXIES'] ?? [])) { + $checks['trusted_proxies'] = ['status' => 'ok', 'message' => 'configured']; +} else { + $checks['trusted_proxies'] = [ + 'status' => 'warning', + 'message' => 'TRUSTED_PROXIES is empty — forward-auth headers are NOT verified; ' + . 'anything that can reach this app directly can impersonate any user' + ]; +} + // Calculate response time $responseTime = round((microtime(true) - $startTime) * 1000, 2);