Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LDAP Authentication

Tuwunel can authenticate password logins against an LDAP directory. When a user logs in with m.login.password, Tuwunel locates them in the directory, verifies the password by binding as that user, and creates a Matrix account on first successful login if one does not yet exist.

LDAP support is a compile-time feature. It is not part of the default build — you must compile with --features ldap (or use a release artifact that already includes it) before any of the configuration below has effect.

Enabling LDAP

LDAP is configured under a single [global.ldap] section. The minimum to enable it is the enable flag and a uri:

[global.ldap]
enable = true
uri = "ldaps://ldap.example.org:636"
base_dn = "ou=users,dc=example,dc=org"

The URI scheme decides the transport: ldap:// is plaintext, ldaps:// upgrades to TLS using the system’s installed CAs.

When enable = true, every m.login.password request is routed through LDAP first. Local password authentication still happens as a fallback when the LDAP search returns zero matches — useful for the bootstrap admin account or for service users that should not exist in the directory. If the LDAP search returns two or more matches the login is rejected.

Bind modes

Tuwunel supports three bind strategies, selected by what you put in bind_dn and bind_password_file.

A service account binds to the directory, searches for the user, then Tuwunel re-binds as the user with the supplied password to verify it. This mode is required if you want admin synchronization (see below).

[global.ldap]
enable = true
uri = "ldaps://ldap.example.org:636"
base_dn = "ou=users,dc=example,dc=org"
bind_dn = "cn=ldap-reader,dc=example,dc=org"
bind_password_file = "/etc/tuwunel/.ldap_bind_password"
filter = "(&(objectClass=person)(memberOf=cn=matrix,ou=groups,dc=example,dc=org))"

The bind password is read from bind_password_file rather than placed inline in the config. The file must be readable by the Tuwunel process and must not be empty.

Anonymous search-then-bind

If your directory permits anonymous searches, omit both bind_dn and bind_password_file. Tuwunel skips the initial bind and queries the directory unauthenticated, then re-binds as the user to verify the password.

Direct bind

If bind_dn contains the literal substring {username}, Tuwunel skips the search entirely and binds directly with that DN, substituting the user’s localpart for {username} and using the supplied login password as the bind password:

[global.ldap]
enable = true
uri = "ldaps://ldap.example.org:636"
bind_dn = "cn={username},ou=users,dc=example,dc=org"

This is the simplest mode but has two limitations: it cannot apply a search filter (so anyone in the bind DN’s subtree can log in), and admin synchronization does not work because Tuwunel never gets a chance to query the directory under a service account.

Configuration reference

FieldDefaultDescription
enablefalseMaster switch for LDAP login. Has no effect unless the binary was compiled with --features ldap.
uriLDAP server URI. ldap://host:389 for plaintext, ldaps://host:636 for TLS.
base_dn""Subtree under which user searches are rooted.
bind_dnDN used for the initial bind. Contains {username} for direct-bind mode; otherwise identifies a service account. Omit for anonymous search.
bind_password_filePath to a file containing the password for bind_dn. Ignored in direct-bind mode (the user’s login password is used).
filter"(objectClass=*)"LDAP search filter applied during user lookup. Supports {username} substitution.
uid_attribute"uid"Attribute that uniquely identifies the user. Returned entries must contain the user’s localpart in this attribute (or in name_attribute).
name_attribute"givenName"Secondary attribute checked for the localpart. Useful when login should match either an account name or a display name.
admin_base_dn""Subtree for the admin search. Falls back to base_dn when empty.
admin_filter""Filter that selects administrative users. Empty disables admin synchronization entirely. Supports {username} substitution.

The localpart match is case-insensitive — Tuwunel sends a lowercased version of the localpart through {username} substitution and accepts an entry if either the original or lowercased form appears in uid_attribute or name_attribute.

Admin synchronization

Setting admin_filter to a non-empty value turns the LDAP directory into the source of truth for who is a Tuwunel admin. On every successful LDAP login, Tuwunel runs a second search rooted at admin_base_dn (or base_dn if empty) using admin_filter. Membership in the result set is compared against the user’s current admin status in Tuwunel:

  • In LDAP admin set, not a Tuwunel admin → granted admin.
  • Not in LDAP admin set, currently a Tuwunel admin → admin revoked.
  • Otherwise → no change.

Two examples:

# Admins are users with a custom objectClass.
admin_filter = "(objectClass=tuwunelAdmin)"

# Admins are members of an LDAP group, looked up under a different subtree.
admin_base_dn = "ou=admins,dc=example,dc=org"
admin_filter = "(uid={username})"

Admin synchronization only runs in the search-then-bind modes. In direct-bind mode the admin search is silently skipped — manage admins manually with !admin users make-admin and !admin users revoke-admin if you need that combination.

Account lifecycle

The first time a user successfully authenticates against LDAP, Tuwunel auto-creates a local Matrix account for them (the same way Synapse, Nextcloud, and Jellyfin behave). The account is registered with origin "ldap" and a placeholder password value — the local password field is never consulted for an LDAP user, so they can only log in by re-authenticating against the directory.

Subsequent logins reuse the existing account and only update admin status if admin_filter is configured.

Deactivating a user in LDAP prevents future logins but does not automatically deactivate or delete the corresponding Matrix account. Use !admin users deactivate if you also want to remove access to existing sessions and devices.

Admin commands for testing

Two admin commands invoke the LDAP code paths directly without going through the login API. They are useful for verifying that filter, uid_attribute, bind_dn, and TLS configuration produce the expected results.

CommandDescription
!admin query users search-ldap @alice:example.orgRun the configured search for a user and print the matching DNs along with their admin status. Returns an empty list if the filter matches nothing.
!admin query users auth-ldap "cn=alice,ou=users,dc=example,dc=org" "<password>"Attempt a direct bind with the given DN and password. Use this to confirm credentials and TLS setup; the password is logged in plaintext to the admin room, so revoke or rotate afterwards.

Both commands are gated by the ldap build feature.

Disabling password login for non-LDAP users

Tuwunel’s LDAP integration always falls back to local password verification when the LDAP search returns no matches. To enforce LDAP-only login for everyone (apart from accounts that authenticate via SSO), pair LDAP with a restrictive filter that matches every legitimate user, and remove or invalidate local passwords for accounts that should no longer be able to log in directly. Alternatively, set login_with_password = false and rely on identity providers for non-LDAP users.