diff --git a/Pipfile b/Pipfile
index 2684ef2da..0909f12b2 100644
--- a/Pipfile
+++ b/Pipfile
@@ -67,6 +67,7 @@ djangorestframework-guardian = "*"
 # Locked version until https://github.com/django/channels_redis/issues/332
 # is resolved
 channels-redis = "==3.4.1"
+django-ipware = "*"
 
 
 [dev-packages]
diff --git a/Pipfile.lock b/Pipfile.lock
index efe0eabbb..14237a002 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "99f415c5ce96020dc3fcb137dc15d47cc5431686bdce1ca42e6254a2719060a8"
+            "sha256": "0e1a26c5e9acb1d745f951f92d00d60272f83406467d90551e558972697b53cd"
         },
         "pipfile-spec": 6,
         "requires": {},
@@ -480,6 +480,14 @@
             "index": "pypi",
             "version": "==2.4.0"
         },
+        "django-ipware": {
+            "hashes": [
+                "sha256:602a58325a4808bd19197fef2676a0b2da2df40d0ecf21be414b2ff48c72ad05",
+                "sha256:878dbb06a87e25550798e9ef3204ed70a200dd8b15e47dcef848cf08244f04c9"
+            ],
+            "index": "pypi",
+            "version": "==4.0.2"
+        },
         "djangorestframework": {
             "hashes": [
                 "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8",
diff --git a/docs/configuration.md b/docs/configuration.md
index 5d4ed7a69..6c233c2e6 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -262,6 +262,14 @@ do CORS calls. Set this to your public domain name.
 
     Defaults to "<http://localhost:8000>".
 
+`PAPERLESS_TRUSTED_PROXIES=<comma-separated-list>`
+
+: This may be needed to prevent IP address spoofing if you are using e.g.
+fail2ban with log entries for failed authorization attempts. Value should be
+IP address(es).
+
+    Defaults to empty string.
+
 `PAPERLESS_FORCE_SCRIPT_NAME=<path>`
 
 : To host paperless under a subpath url like example.com/paperless you
diff --git a/src/paperless/apps.py b/src/paperless/apps.py
new file mode 100644
index 000000000..323099745
--- /dev/null
+++ b/src/paperless/apps.py
@@ -0,0 +1,15 @@
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+from paperless.signals import handle_failed_login
+
+
+class PaperlessConfig(AppConfig):
+    name = "paperless"
+
+    verbose_name = _("Paperless")
+
+    def ready(self):
+        from django.contrib.auth.signals import user_login_failed
+
+        user_login_failed.connect(handle_failed_login)
+        AppConfig.ready(self)
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
index 7fc384eaa..41f08f3e2 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -416,6 +416,13 @@ if _paperless_url:
         # always allow localhost. Necessary e.g. for healthcheck in docker.
         ALLOWED_HOSTS = [_paperless_uri.hostname] + ["localhost"]
 
+# For use with trusted proxies
+_trusted_proxies = os.getenv("PAPERLESS_TRUSTED_PROXIES")
+if _trusted_proxies:
+    TRUSTED_PROXIES = _trusted_proxies.split(",")
+else:
+    TRUSTED_PROXIES = []
+
 # The secret key has a default that should be fine so long as you're hosting
 # Paperless on a closed network.  However, if you're putting this anywhere
 # public, you should change the key to something unique and verbose.
diff --git a/src/paperless/signals.py b/src/paperless/signals.py
new file mode 100644
index 000000000..cedad7f67
--- /dev/null
+++ b/src/paperless/signals.py
@@ -0,0 +1,32 @@
+import logging
+
+from django.conf import settings
+from ipware import get_client_ip
+
+logger = logging.getLogger("paperless.auth")
+
+
+# https://docs.djangoproject.com/en/4.1/ref/contrib/auth/#django.contrib.auth.signals.user_login_failed
+def handle_failed_login(sender, credentials, request, **kwargs):
+    client_ip, is_routable = get_client_ip(
+        request,
+        proxy_trusted_ips=settings.TRUSTED_PROXIES,
+    )
+    if client_ip is None:
+        logger.info(
+            f"Login failed for user `{credentials['username']}`."
+            + " Unable to determine IP address.",
+        )
+    else:
+        if is_routable:
+            # We got the client's IP address
+            logger.info(
+                f"Login failed for user `{credentials['username']}`"
+                + f" from IP `{client_ip}.`",
+            )
+        else:
+            # The client's IP address is private
+            logger.info(
+                f"Login failed for user `{credentials['username']}`"
+                + f" from private IP `{client_ip}.`",
+            )