Files
felhom.eu/documentation/audits/visitors-2026-10-01/A/sweep/sweep-2.md
T

28 lines
6.4 KiB
Markdown

# Catalog sweep 2/4 — who reads the visitor's address (READ in source at each pinned tag; not measured live)
Subagent report, 2026-10-01 evening, copied verbatim in substance. Apps: gitea glance gokapi grafana gramps-web
home-assistant homebox homepage immich jellyfin karakeep kimai komga mealie.
| App (tag) | Reads forwarded headers? | How / setting (default) | Used for | Verdict for the new chain | Evidence |
|---|---|---|---|---|---|
| gitea 1.27.3 | Only from a trusted peer; default trust = loopback, so traefik is not trusted | chi proxy: X-Real-IP first, then XFF count-from-right (`REVERSE_PROXY_LIMIT`=1); `REVERSE_PROXY_TRUSTED_PROXIES` (default `127.0.0.0/8,::1/128`) | logs ("Failed authentication attempt … from"), `InitialIP`; no IP lockout | SAFE AS IS (sees traefik). Trusting 172.16.0.0/12 would show the LAN client, but cloudflared on the tunnel (X-Real-IP wins). Never forgeable | modules/setting/security.go:133-137; routers/common/middleware.go:33,123-133; chi-middleware/proxy v1.1.1 middleware.go:50-73; routers/web/auth/auth.go:310 |
| glance v0.8.5 | Only with `server.proxied: true` (off; not in our seed) | leftmost XFF when on | login limit by IP, only with `auth:` (our seed has none) | SAFE AS IS. **Never set `proxied: true`** (leftmost) | internal/glance/glance.go:364-389; auth.go:24-25,142-160 |
| gokapi v1.9.6 | always: first parseable XFF, X-Real-IP, peer | leftmost, no setting | download log only when `SaveIp` (seed: false); no login limit | SAFE AS IS (unused) | internal/logging/Logging.go:43-48,65-93 |
| grafana 13.2.3 | always | X-Real-IP first, then leftmost XFF; no trust setting | lockout by USERNAME (5/5 min); IP lockout OFF by default; logs, session client-IP | SAFE AS IS (X-Real-Ip is traefik-set: cloudflared on the tunnel, LAN client on the LAN). **Do not turn on IP lockout** | pkg/web/context.go:71-96; conf/defaults.ini:498-507; loginattemptimpl/login_attempt.go:65-99 |
| gramps-web v25.6.0 | no (flask_limiter `get_remote_address` = TCP peer) | peer | `1/second` on token/login/register, keyed on the peer = ONE bucket for all | SAFE AS IS, no change from Part A | gramps-web-api v3.3.0 ratelimiter.py:5-9; token.py:73,104,128,143 (API version INFERRED from `FROM dmstraub/gramps-webapi:latest`) |
| home-assistant 2026.9.4 | yes; template sets `use_x_forwarded_for: true`, `trusted_proxies: [172.16.0.0/12]` | walks from the RIGHT skipping trusted; all trusted → leftmost; non-IP entry → 400 | `ip_ban` (threshold -1 = off); **LAN privilege**: `local_only` users, remember-me preselect | SAFE AS IS and **FIXED by Part A**: today every tunnel visitor arrives as cloudflared's PRIVATE address → "local" → a `local_only` user can sign in from the internet. After: skips 172.16.253.2, takes the real public client | components/http/forwarded.py:83-144; http/auth_util.py:15-43; util/network.py:51-53 |
| homebox 0.26.2 | only with `HBOX_OPTIONS_TRUST_PROXY=true` (default false, not set) | X-Real-IP first, then leftmost XFF | login/forgot/reset limiter keyed `IP\|path` (5/min) | SAFE AS IS (not forgeable; but one bucket = a stranger can lock everyone out — as today). Turning trust on helps the LAN only (X-Real-Ip = cloudflared on the tunnel); also makes it trust X-Forwarded-Host — optional, small gain | backend/app/api/middleware.go:454-490,556-575; internal/sys/config/conf.go:74,179-184 |
| homepage v1.13.2 | no | — | no auth; Host check on `/api/*` | SAFE AS IS | src/middleware.js:3-18 |
| immich v3.2.4 | yes (Express trust proxy) | walks from the RIGHT (proxy-addr); `IMMICH_TRUSTED_PROXIES` default linklocal,uniquelocal (+loopback) | logs only; no IP lockout | SAFE AS IS and **sees the real client** after Part A | server/src/app.common.ts:49; config.repository.ts:327; auth.service.ts:71 |
| jellyfin 10.11.11 | only when `KnownProxies` set (default empty) | ASP.NET ForwardedHeaders from the RIGHT | **LAN privileges** (remote access per user, remote bitrate, public user list, restart for non-admins, ForgotPassword); lockout per user | **NEEDS A SETTING — and a RISK TODAY that Part A alone does not change**: the TCP peer is traefik (private) → every internet visitor is "LAN". Fix: KnownProxies `172.16.0.0/12` in `network.xml` (no env var) | ApiServiceCollectionExtensions.cs:169-190,282-326; NetworkManager.cs:309-340,942-960; UserManager.cs:595-596 |
| karakeep 0.33.2 | always (`request-ip`) | X-Client-IP, then LEFTMOST XFF, then CF-Connecting-IP, … | login + tRPC limits keyed by IP — **only with `RATE_LIMITING_ENABLED=true`** (default false, not set) | SAFE AS IS (limiter off). **Do not turn the limiter on** — after Part A its key would be the client-written leftmost | apps/web/server/auth.ts:129-137; packages/trpc/lib/rateLimit.ts:21-39; request-ip src/index.js:39-41,59-97 |
| kimai 2.67.0 | only from `TRUSTED_PROXIES` (image default `nginx,localhost,127.0.0.1`) | Symfony: from the RIGHT, dropping trusted | login throttling 5/5 min (Symfony default username+IP plus a per-IP limit — from Symfony docs, not Kimai code); IP-keyed limiters (session-ID guard, password reset, old API tokens) | **NEEDS A SETTING**: today every key is traefik → one attacker trips the IP limiters for all. `TRUSTED_PROXIES=127.0.0.1,172.16.0.0/12` → tunnel real client, LAN client | Dockerfile:256; config/packages/security.yaml:70-72; rate_limiter.yaml |
| komga 1.28.0 | always (`forward-headers-strategy: framework`) | LEFTMOST | the authentication-activity audit IP only | SAFE AS IS for security; the audit IP becomes client-written on the tunnel (today: cloudflared). `SERVER_FORWARDHEADERSSTRATEGY=native` would fix it — not without a live test | application.yml:63; LoginListener.kt:30-96 |
| mealie v3.28.0 | yes; and `/api/auth/token` reads raw XFF | LEFTMOST | log lines; lockout per ACCOUNT | SAFE AS IS; the logged IP becomes client-written on the tunnel. No setting fixes it | routes/auth/auth.py:141-147; credentials_provider.py:41-54 |
Notes from the report: leftmost readers (glance if `proxied`, gokapi, karakeep, komga, mealie) use the address for nothing
or for logs/audit only as configured — two switches must stay off (karakeep `RATE_LIMITING_ENABLED`, glance `proxied`).
Not checked live: ASP.NET (Jellyfin) and HA with a 3-entry XFF (HA refuses when X-Forwarded-Proto has neither 1 entry nor
as many as XFF — traefik sends 1); Kimai's exact throttling keys. Side observations: glance's seed has no `auth:` (public
dashboard); homepage `/api/*` refuses a Host not in `HOMEPAGE_ALLOWED_HOSTS` (inferred, not set by the template).