-- Trusted reverse-proxy depth for client-IP extraction (#2453). -- -- X-Forwarded-For grows left-to-right: each proxy APPENDS the peer it -- received the request from. For client -> CDN -> own-proxy -> Minstrel the -- app sees XFF = [client, CDN] with RemoteAddr = own-proxy. So the real -- client sits at XFF[len - hops], where hops counts the proxies you trust: -- -- 0 no proxy in front — use the socket peer, ignore XFF entirely -- 1 one reverse proxy (nginx / Caddy / Traefik terminating TLS) -- 2 a CDN in front of your own proxy (Cloudflare -> nginx -> Minstrel) -- -- Default 1: a publicly reachable Minstrel needs a TLS terminator in front of -- it, and recording that terminator's own address for every session makes the -- active-sessions surface (#370) useless — created_ip and last_ip would both -- be the proxy, so the "address changed" signal could never fire. -- -- The cost, stated on the admin card rather than buried: hops >= 1 DECLARES -- that a proxy exists. If one doesn't, a client can forge X-Forwarded-For and -- choose what its own session row shows, which defeats exactly the compromise -- detection #370 exists for. That is inherent to the trusted-hop model, which -- is why 0 is a first-class setting and not a hidden escape hatch. -- -- Upper bound 10 guards a typo turning into "trust the whole header"; no real -- deployment chains ten proxies. CREATE TABLE network_settings ( id boolean PRIMARY KEY DEFAULT true, trusted_proxy_hops int NOT NULL DEFAULT 1, CONSTRAINT network_settings_singleton CHECK (id = true), CONSTRAINT network_settings_hops_range CHECK (trusted_proxy_hops >= 0 AND trusted_proxy_hops <= 10) ); INSERT INTO network_settings (id) VALUES (true) ON CONFLICT (id) DO NOTHING;