feat(auth): SA3 — trusted-proxy X-Forwarded-For for rate limiting
_client_ip trusted the first X-Forwarded-For hop unconditionally, so anyone able to reach the app port could forge XFF and dodge the login/register/reset/demo rate limits. Now trust XFF ONLY when the request's socket peer is a configured reverse proxy (settings.trusted_proxy_ips, e.g. the VPS Caddy's WireGuard peer IP), and take the RIGHTMOST entry — the client our proxy actually saw and appended, immune to a client pre-seeding a fake XFF. A request from any other peer (hitting the port directly) is keyed on its real socket IP, so XFF can't be forged to bypass the limits. New TRUSTED_PROXY_IPS env (empty default = no proxy, use the direct peer). Documented in .env.example, docs/self-hosting.md, README. Unit-verified against spoof-through-proxy and direct-bypass cases.
This commit is contained in:
parent
2eca56d68a
commit
7297dbd2a8
5 changed files with 52 additions and 7 deletions
|
|
@ -52,6 +52,14 @@ class Settings(BaseSettings):
|
|||
# Origin of a separately served frontend dev server (enables CORS). Empty in production.
|
||||
frontend_origin: str = ""
|
||||
|
||||
# Reverse-proxy IPs whose X-Forwarded-For we trust for the real client IP (rate limiting). This
|
||||
# is the address the proxy CONNECTS FROM as seen by the app — e.g. the WireGuard peer IP of the
|
||||
# VPS Caddy front door. A request from any other peer (someone hitting the app port directly)
|
||||
# is NOT trusted and its raw socket IP is used, so it can't spoof its rate-limit identity via
|
||||
# XFF. Empty = trust no proxy (use the direct peer) — correct for a directly-exposed / local-dev
|
||||
# deploy. Comma-separated. See auth._client_ip + the deploy docs.
|
||||
trusted_proxy_ips: str = ""
|
||||
|
||||
# --- Outbound email (onboarding: access-request + approval notices) ---
|
||||
# Gmail SMTP + App Password by default. All optional: if unset, email is skipped
|
||||
# (fail-soft) and onboarding still works via in-app notifications.
|
||||
|
|
@ -226,6 +234,10 @@ class Settings(BaseSettings):
|
|||
def admin_email_set(self) -> set[str]:
|
||||
return {e.strip().lower() for e in self.admin_emails.split(",") if e.strip()}
|
||||
|
||||
@property
|
||||
def trusted_proxy_ip_set(self) -> frozenset[str]:
|
||||
return frozenset(p.strip() for p in self.trusted_proxy_ips.split(",") if p.strip())
|
||||
|
||||
@property
|
||||
def app_base(self) -> str:
|
||||
"""Public origin of the deployed app, derived from the OAuth redirect URL
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue