diff --git a/docker/main/rootfs/usr/local/nginx/conf/proxy.conf b/docker/main/rootfs/usr/local/nginx/conf/proxy.conf index 7ea8ba84c..a3aacc309 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/proxy.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/proxy.conf @@ -7,7 +7,7 @@ proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-URI $request_uri; proxy_set_header X-Forwarded-Ssl on; -proxy_set_header X-Forwarded-For $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; ## Basic Proxy Configuration diff --git a/frigate/api/app.py b/frigate/api/app.py index 6ece92f2c..bd2357be6 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -87,8 +87,9 @@ def create_app( app.camera_error_image = None app.stats_emitter = stats_emitter app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None - # initialize the rate limiter for the login endpoint + # update the request_address with the x-forwarded-for header from nginx app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) + # initialize the rate limiter for the login endpoint limiter.init_app(app) if frigate_config.auth.failed_login_rate_limit is None: limiter.enabled = False diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 3f11a66b0..95eb1939f 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -2,6 +2,7 @@ import base64 import hashlib +import ipaddress import json import logging import os @@ -13,7 +14,6 @@ from pathlib import Path from flask import Blueprint, current_app, jsonify, make_response, request from flask_limiter import Limiter -from flask_limiter.util import get_remote_address from joserfc import jwt from peewee import DoesNotExist @@ -24,8 +24,51 @@ logger = logging.getLogger(__name__) AuthBp = Blueprint("auth", __name__) + +def get_remote_addr(): + route = list(reversed(request.headers.get("x-forwarded-for").split(","))) + logger.debug(f"IP Route: {[r for r in route]}") + trusted_proxies = [] + for proxy in current_app.frigate_config.auth.trusted_proxies: + try: + network = ipaddress.ip_network(proxy) + except ValueError: + logger.warn(f"Unable to parse trusted network: {proxy}") + trusted_proxies.append(network) + + # return the first remote address that is not trusted + for addr in route: + ip = ipaddress.ip_address(addr.strip()) + logger.debug(f"Checking {ip} (v{ip.version})") + trusted = False + for trusted_proxy in trusted_proxies: + logger.debug( + f"Checking against trusted proxy: {trusted_proxy} (v{trusted_proxy.version})" + ) + if trusted_proxy.version == 4: + ipv4 = ip.ipv4_mapped if ip.version == 6 else ip + if ipv4 in trusted_proxy: + trusted = True + logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}") + break + elif trusted_proxy.version == 6 and ip.version == 6: + if ip in trusted_proxy: + trusted = True + logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}") + break + if trusted: + logger.debug(f"{ip} is trusted") + continue + else: + logger.debug(f"First untrusted IP: {str(ip)}") + return str(ip) + + # if there wasn't anything in the route, just return the default + return request.remote_addr or "127.0.0.1" + + limiter = Limiter( - lambda: get_remote_address, + get_remote_addr, storage_uri="memory://", ) diff --git a/frigate/config.py b/frigate/config.py index 163e99367..dca5bd4e0 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -136,6 +136,10 @@ class AuthConfig(FrigateBaseModel): default="1/second;5/minute;20/hour", title="Rate limits for failed login attempts.", ) + trusted_proxies: Optional[List[str]] = Field( + default=[], + title="Trusted proxies for determining IP address to rate limit", + ) # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 hash_iterations: int = Field(default=600000, title="Password hash iterations") diff --git a/web/src/components/auth/AuthForm.tsx b/web/src/components/auth/AuthForm.tsx index a3746f3a8..7d6f0bc1f 100644 --- a/web/src/components/auth/AuthForm.tsx +++ b/web/src/components/auth/AuthForm.tsx @@ -59,7 +59,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { if (axios.isAxiosError(error)) { const err = error as AxiosError; if (err.response?.status === 429) { - toast.error("Exceeded rate limit. Try again in 1 minute.", { + toast.error("Exceeded rate limit. Try again later.", { position: "top-center", }); } else if (err.response?.status === 400) {