From 4f0845b09471cb933aa5ccfd750f760834ab5e17 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:12:12 -0700 Subject: [PATCH] Fixhancement: PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT (#13281) --- docs/configuration.md | 28 ++++++++++++++----- docs/migration-v3.md | 5 +++- src/paperless/settings/__init__.py | 17 +++++++++-- src/paperless/tests/settings/test_settings.py | 27 ++++++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b4e200f95..9bbb63402 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -522,22 +522,36 @@ do CORS calls. Set this to your public domain name. fail2ban with log entries for failed authorization attempts. Value should be IP address(es). - This setting also controls allauth's - [`ALLAUTH_TRUSTED_PROXY_COUNT`](https://docs.allauth.org/en/latest/account/configuration.html), - which is set to the number of proxies listed here. Without this, - allauth cannot determine the client IP address for rate limiting when - running behind a reverse proxy, resulting in a `403 Forbidden` on login. + By default, this setting also controls allauth's trusted proxy count, + which is set to the number of proxies listed here. Override that default + with [`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT`](#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT) + when the list length does not match the number of proxy hops. Defaults to empty string. +#### [`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT=`](#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT) {#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT} + +: Sets allauth's +[`ALLAUTH_TRUSTED_PROXY_COUNT`](https://docs.allauth.org/en/latest/common/rate_limits.html#configuration). +This is the number of trusted proxy **hops** represented in each +`X-Forwarded-For` header, not the number of IP addresses through which those +proxies may be reached. For example, a single dual-stack proxy is one hop even +when its IPv4 and IPv6 addresses are both listed in +[`PAPERLESS_TRUSTED_PROXIES`](#PAPERLESS_TRUSTED_PROXIES). + + Only trust `X-Forwarded-For` when untrusted clients cannot connect directly + to Paperless-ngx. + + Defaults to the number of entries in `PAPERLESS_TRUSTED_PROXIES`. + #### [`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER=`](#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER) {#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER} : Sets allauth's -[`ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](https://docs.allauth.org/en/latest/account/configuration.html). +[`ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](https://docs.allauth.org/en/latest/common/rate_limits.html#configuration). Use this when your reverse proxy sets a dedicated header for the real client IP instead of `X-Forwarded-For`, for example `X-Real-IP` (nginx) or `CF-Connecting-IP` (Cloudflare). When set, this takes precedence over -[`PAPERLESS_TRUSTED_PROXIES`](#PAPERLESS_TRUSTED_PROXIES). +`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT`. Defaults to none. diff --git a/docs/migration-v3.md b/docs/migration-v3.md index a8b75480b..7370de977 100644 --- a/docs/migration-v3.md +++ b/docs/migration-v3.md @@ -324,8 +324,11 @@ option since v1.8.0. Allauth changed how it determines the client IP address for login rate limiting. Users running behind a reverse proxy may need to set [`PAPERLESS_TRUSTED_PROXIES`](configuration.md#PAPERLESS_TRUSTED_PROXIES), +[`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT), [`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER), -or both, to avoid `403 Forbidden` errors on login. +or a combination of these settings to avoid `403 Forbidden` errors on login. +The proxy count is the number of proxy hops in `X-Forwarded-For`, which may +differ from the number of configured proxy IP addresses. ## Minimum CPU Requirements (NumPy Baseline) diff --git a/src/paperless/settings/__init__.py b/src/paperless/settings/__init__.py index 546b09b80..55aac5a91 100644 --- a/src/paperless/settings/__init__.py +++ b/src/paperless/settings/__init__.py @@ -460,11 +460,22 @@ def _parse_paperless_url(): PAPERLESS_URL = _parse_paperless_url() + +def _get_allauth_trusted_proxy_count(trusted_proxies: list[str]) -> int: + count = get_int_from_env( + "PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT", + len(trusted_proxies), + ) + if count < 0: + raise ImproperlyConfigured( + "PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT must be zero or greater", + ) + return count + + # For use with trusted proxies TRUSTED_PROXIES = get_list_from_env("PAPERLESS_TRUSTED_PROXIES") -# Derive allauth's proxy count from the same list so X-Forwarded-For is trusted -# correctly when users have configured PAPERLESS_TRUSTED_PROXIES. -ALLAUTH_TRUSTED_PROXY_COUNT = len(TRUSTED_PROXIES) +ALLAUTH_TRUSTED_PROXY_COUNT = _get_allauth_trusted_proxy_count(TRUSTED_PROXIES) ALLAUTH_TRUSTED_CLIENT_IP_HEADER = os.getenv( "PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER", ) diff --git a/src/paperless/tests/settings/test_settings.py b/src/paperless/tests/settings/test_settings.py index 0694d9360..e897febd3 100644 --- a/src/paperless/tests/settings/test_settings.py +++ b/src/paperless/tests/settings/test_settings.py @@ -3,7 +3,9 @@ from unittest import TestCase from unittest import mock import pytest +from django.core.exceptions import ImproperlyConfigured +from paperless.settings import _get_allauth_trusted_proxy_count from paperless.settings import _get_search_language_setting from paperless.settings import _parse_paperless_url from paperless.settings import default_threads_per_worker @@ -35,6 +37,31 @@ class TestThreadCalculation(TestCase): self.assertLessEqual(default_workers * default_threads, i) +def test_allauth_trusted_proxy_count_defaults_to_trusted_proxies( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT", raising=False) + + assert _get_allauth_trusted_proxy_count(["proxy-v4", "proxy-v6"]) == 2 + + +def test_allauth_trusted_proxy_count_can_be_overridden( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT", "1") + + assert _get_allauth_trusted_proxy_count(["proxy-v4", "proxy-v6"]) == 1 + + +def test_allauth_trusted_proxy_count_rejects_negative_values( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT", "-1") + + with pytest.raises(ImproperlyConfigured, match="must be zero or greater"): + _get_allauth_trusted_proxy_count([]) + + @pytest.mark.parametrize( ("env_value", "expected"), [