Repository: sissbruecker/linkding

Issue:
fail2ban support
Would be nice to have fail2ban support in swag for linkding, some log that can be used for unsuccesful attempts

maybe this can be partly used: 

https://github.com/HerraLampila/django2ban


Discussion:
Second this, would love to see log/fail2ban support
is/should this part of a "normal" software, or should you solve that via a ingress/reverse proxy.
i.e. https://traefik.io/traefik/ then https://plugins.traefik.io/plugins/628c9ebcffc0cd18356a979f/fail2-ban
I personally use a reverse proxy, nginx proxy manager (NPM), however, I've found that there have been repeated login attempts on other self-hosted platforms I use.

Traefik is overkill for (most) people's purposes in my opinion. Being able to simply spin up a docker container with fail2ban to block repeated attempts works well. Paperless-ngx is an example out of the box they support it simply by providing a log file with attempts and instructions to filter (https://github.com/paperless-ngx/paperless-ngx/wiki/Using-Security-Tools-with-Paperless-ngx#fail2ban)

Similar with Vaultwarden (https://github.com/dani-garcia/vaultwarden/wiki/Fail2Ban-Setup)

It's a security enhancement/nice to have, and, thank you for the project its great!


Imho fail2ban should be part of the reverse proxy configuration. Reason is that the reverse proxy have to filter IPs of login attacks - not the host behind. Linkding should provide a qualified log file that mark unauthorised logins / login fails ... this log can provide by the OS via rsyslog to each target and can parse there.  This is e.g. the way to handle login fails via fail2ban for nextcloud. 
On Tue, Jan 30, 2024 at 02:06:55AM -0800, Fischmuetze wrote:
>   Linkding should provide a qualified log file that mark
>   unauthorised logins / login fails ... this log can provide by the OS via
>   rsyslog to each target and can parse there. This is e.g. the way to handle
>   login fails via fail2ban for nextcloud.

IMHO no container should log to a logfile but to STDOUT and STDERR instead, 
this is bad practice.

This is how I "fixed" the fail2ban, it is not fool proof (in other words, if you keep logging in and out you will also get banned, but hey who does that?""

I use fail2ban in [SWAG](https://docs.linuxserver.io/general/swag/).

### linkding.conf in filter.d
```
# A fail2ban filter for unauthorized log messages

[Definition]


failregex = ^<HOST>.*"(GET|POST|HEAD) \/login\/.*".(200).*"https:\/\/.*\/login.*$
```


### Add this to jail.local
```
[linkding]
enabled  = true
port     = http,https
filter   = linkding
logpath  = /config/log/nginx/access.log

```



Patch:
diff --git a/siteroot/urls.py b/siteroot/urls.py
index 5431f02d..4b14f4fb 100644
--- a/siteroot/urls.py
+++ b/siteroot/urls.py
@@ -34,6 +34,11 @@ def get_context_data(self, **kwargs):
         context["enable_oidc"] = settings.LD_ENABLE_OIDC
         return context
 
+    def form_invalid(self, form):
+        response = super().form_invalid(form)
+        response.status_code = 401
+        return response
+
 
 urlpatterns = [
     path("admin/", linkding_admin_site.urls),


Test patch:
diff --git a/bookmarks/tests/test_login_view.py b/bookmarks/tests/test_login_view.py
index 3eb25bdd..5aa55a66 100644
--- a/bookmarks/tests/test_login_view.py
+++ b/bookmarks/tests/test_login_view.py
@@ -1,7 +1,7 @@
 from django.test import TestCase, override_settings
 from django.urls import path, include
 
-from bookmarks.tests.helpers import HtmlTestMixin
+from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
 from siteroot.urls import urlpatterns as base_patterns
 
 # Register OIDC urls for this test, otherwise login template can not render when OIDC is enabled
@@ -9,7 +9,19 @@
 
 
 @override_settings(ROOT_URLCONF=__name__)
-class LoginViewTestCase(TestCase, HtmlTestMixin):
+class LoginViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
+
+    def test_failed_login_should_return_401(self):
+        response = self.client.post("/login/", {"username": "test", "password": "test"})
+        self.assertEqual(response.status_code, 401)
+
+    def test_successful_login_should_redirect(self):
+        user = self.setup_user(name="test")
+        user.set_password("test")
+        user.save()
+
+        response = self.client.post("/login/", {"username": "test", "password": "test"})
+        self.assertEqual(response.status_code, 302)
 
     def test_should_not_show_oidc_login_by_default(self):
         response = self.client.get("/login/")