diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index c3705b3cacd498a849f05009ad72c610bfb2b894..142e2c83723c8fb41d2eae464fd9d01cfee8e1ae 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -234,7 +234,7 @@ if _settings.get("ldap.uri", None):
     )
 
     # Enable Django's integration to LDAP
-    AUTHENTICATION_BACKENDS.append("django_auth_ldap.backend.LDAPBackend")
+    AUTHENTICATION_BACKENDS.append("aleksis.core.util.ldap.LDAPBackend")
 
     AUTH_LDAP_SERVER_URI = _settings.get("ldap.uri")
 
@@ -243,6 +243,13 @@ if _settings.get("ldap.uri", None):
         AUTH_LDAP_BIND_DN = _settings.get("ldap.bind.dn")
         AUTH_LDAP_BIND_PASSWORD = _settings.get("ldap.bind.password")
 
+    # Keep local password for users to be required to proveide their old password on change
+    AUTH_LDAP_SET_USABLE_PASSWORD = True
+
+    # Keep bound as the authenticating user
+    # Ensures proper read permissions, and ability to change password without admin
+    AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True
+
     # The TOML config might contain either one table or an array of tables
     _AUTH_LDAP_USER_SETTINGS = _settings.get("ldap.users.search")
     if not isinstance(_AUTH_LDAP_USER_SETTINGS, list):
diff --git a/aleksis/core/util/ldap.py b/aleksis/core/util/ldap.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a17cbbb5aea91030fab467c21897826f74eb6aa
--- /dev/null
+++ b/aleksis/core/util/ldap.py
@@ -0,0 +1,34 @@
+"""Utilities and extensions for django_auth_ldap."""
+
+from django.core.exceptions import PermissionDenied
+
+from django_auth_ldap.backend import LDAPBackend as _LDAPBackend
+
+
+class LDAPBackend(_LDAPBackend):
+    default_settings = {"SET_USABLE_PASSWORD": False}
+
+    def authenticate_ldap_user(self, ldap_user, password):
+        """Authenticate user against LDAP and set local password if successful.
+
+        Having a local password is needed to make changing passwords easier. In
+        order to catch password changes in a universal way and forward them to
+        backends (like LDAP, in this case), getting the old password first is
+        necessary to authenticate as that user to LDAP.
+
+        We buy the small insecurity of having a hash of the password in the
+        Django database in order to not require it to have global admin permissions
+        on the LDAP directory.
+        """
+        user = ldap_user.authenticate(password)
+
+        if not user:
+            # Fail early and do not try other backends
+            raise PermissionDenied("LDAP failed to authenticate user")
+
+        if self.settings.SET_USABLE_PASSWORD:
+            # Set a usable password so users can change their LDAP password
+            user.set_password(password)
+            user.save()
+
+        return user