Skip to content

Add LDAP/Active Directory authentication support - #1869

Open
Hemsby wants to merge 2 commits into
TechnitiumSoftware:developfrom
Hemsby:feature/ldap-auth
Open

Add LDAP/Active Directory authentication support#1869
Hemsby wants to merge 2 commits into
TechnitiumSoftware:developfrom
Hemsby:feature/ldap-auth

Conversation

@Hemsby

@Hemsby Hemsby commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds native LDAP authentication to the DNS Server web console, enabling users to sign in with their Active Directory or LDAP directory credentials.

Features

  • Three connection modes — plain LDAP, StartTLS (port 389 + SSL), and LDAPS (port 636 + SSL). Mode is selected automatically based on port: Use SSL + port 636 = LDAPS, Use SSL + any other port = StartTLS.
  • Service account search — a bind DN/password locates the user in the directory before validating their credentials. Supports UPN (user@domain) and full DN formats.
  • Configurable user filter — defaults to (sAMAccountName={0}) for Active Directory; any RFC 4515 filter is supported.
  • Group mapping — maps LDAP groups (by CN) to local Technitium groups, e.g. Domain AdminsAdministrators.
  • Auto-provisioning — optionally creates a local account on first LDAP login, with optional restriction to mapped groups only.
  • Ignore SSL errors — for self-signed or internal CA certificates.
  • Test connection button — verifies the service account bind without saving config.
  • Docker/environment variable support — full headless configuration via DNS_SERVER_LDAP_* variables.

Environment Variables

Variable Description
DNS_SERVER_LDAP_ENABLED true/false
DNS_SERVER_LDAP_SERVER Hostname or IP
DNS_SERVER_LDAP_PORT Default: 389
DNS_SERVER_LDAP_USE_SSL true enables StartTLS (port 389) or LDAPS (port 636)
DNS_SERVER_LDAP_IGNORE_SSL_ERRORS true/false
DNS_SERVER_LDAP_BIND_DN Service account DN or UPN
DNS_SERVER_LDAP_BIND_PASSWORD Service account password
DNS_SERVER_LDAP_SEARCH_BASE e.g. DC=example,DC=com
DNS_SERVER_LDAP_USER_FILTER Default: (sAMAccountName={0})
DNS_SERVER_LDAP_GROUP_ATTRIBUTE Default: memberOf
DNS_SERVER_LDAP_ALLOW_SIGNUP true/false
DNS_SERVER_LDAP_ALLOW_SIGNUP_ONLY_FOR_MAPPED_USERS true/false
DNS_SERVER_LDAP_GROUP_MAP JSON array, e.g. [{"remoteGroup":"Domain Admins","localGroup":"Administrators"}]

Implementation Notes

Uses System.DirectoryServices.Protocols (Microsoft built-in library) for LDAP connectivity. On Linux, this requires libldap2 to be installed (apt install libldap2 on Debian/Ubuntu, dnf install openldap on RHEL/Rocky). This package is not included by the Technitium install script but is available on all major Linux distributions.

@ShreyasZare

Copy link
Copy Markdown
Member

Thanks for the PR. Is there any specific reason to prefer using Novell.Directory.Ldap.NETStandard instead of System.DirectoryServices.Protocols? Also which LDAP servers have you tested this PR with?

@Hemsby

Hemsby commented Jun 18, 2026 via email

Copy link
Copy Markdown
Contributor Author

@ShreyasZare

Copy link
Copy Markdown
Member

Thanks for the response. The concern I have here is that Novell.Directory.Ldap.NETStandard is an independent project which may stop getting updated anytime and may have compatibility issues in future. Whereas System.DirectoryServices.Protocols being supported by Microsoft can be expected to not have such issues.

@Hemsby

Hemsby commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Would you like me to change it to use System.DirectoryServices.Protocols?

@ShreyasZare

Copy link
Copy Markdown
Member

Would you like me to change it to use System.DirectoryServices.Protocols?

That will be really nice.

Also please rebase to the develop branch if possible so that merging it is easier due to too many refactoring changes. Thanks.

Adds native LDAP SSO authentication allowing users to sign in with
their Active Directory or LDAP directory credentials. Supports plain
LDAP, StartTLS (port 389), and LDAPS (port 636). Includes group-to-
role mapping, configurable auto-provisioning, and full environment
variable support for Docker deployments.

Uses System.DirectoryServices.Protocols (Microsoft built-in library).
On Linux, requires libldap2 to be installed.
@Hemsby
Hemsby force-pushed the feature/ldap-auth branch from 294ee56 to 5211b4c Compare June 29, 2026 08:20
@Hemsby

Hemsby commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Hi, I've updated the PR as requested. Switched from Novell.Directory.Ldap to System.DirectoryServices.Protocols and rebased onto develop.

Tested on Debian 13 with Active Directory. One Linux prerequisite: libldap2 needs to be installed (not included by the Technitium install script or the .NET runtime). On Debian/Ubuntu: apt install libldap2. On RHEL/Rocky: dnf install openldap.

Let me know if you need any changes.

@ShreyasZare

Copy link
Copy Markdown
Member

Thanks for the update! Will check it out soon.

@john8862

Copy link
Copy Markdown

Hi Hemsby, may I ask if you've ever test against FreeIPA and OpenLDAP? As far as I know their directory attribute structure could be very different. The Active Directory query (including recursive query) method may not apply to FreeIPA or OpenLDAP

LdapAuthProvider: fall back to a reverse group-membership search
(member/uniqueMember/memberUid) when the user entry has no values for
the configured group attribute. The default "memberOf" is an
AD-native reverse-linked attribute; stock OpenLDAP (without the
memberof overlay) doesn't populate it, so LDAP group-based role
mapping was silently doing nothing on that directory even though
login itself worked. Fallback is best-effort and only runs when the
primary lookup found no groups, so directories that already populate
memberOf (AD, OpenLDAP with the overlay) are unaffected.

Extensions.cs: TryQueryOrFormArray's colspan overload threw a raw
"Offset and length were out of bounds" exception when the submitted
value wasn't an exact multiple of colspan entries (e.g. ldapGroupMap
without the '|' delimiter). Now throws a clear DnsWebServiceException
instead, surfaced through the existing API error handling. This is
shared parsing code also used by ssoGroupMap and others, not LDAP
specific, but was found while testing the LDAP config endpoint.

Verified against a live OpenLDAP instance: both groupOfNames (member)
and posixGroup (memberUid) group mappings now correctly elevate
permissions without requiring the memberof overlay.
@Hemsby

Hemsby commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Good question, thanks for raising it. I tested this against a live OpenLDAP instance and found a real gap: basic authentication works fine out of the box (just set ldapUserFilter to (uid={0}) instead of the AD default), but group based role mapping was silently doing nothing.

The reason: the default group attribute (memberOf) is an Active Directory native reverse linked attribute that AD maintains automatically on the user object. Stock OpenLDAP does not populate anything equivalent unless the memberof overlay module is loaded, which is not on by default. So a user would log in successfully but always get default/unmapped permissions instead of whatever their LDAP group should have granted them, with no error surfaced.

Fixed by adding a fallback: when the primary group attribute lookup on the user entry comes back empty, the code now does a reverse search for groups that list the user as a member (member, uniqueMember, or memberUid, covering both groupOfNames and posixGroup style layouts). This only runs when the primary lookup finds nothing, so AD and OpenLDAP with the overlay are unaffected. Verified against both group types on a real OpenLDAP server, both now map correctly without requiring the overlay.

I have not tested it specifically for FreeIPA.

Also found and fixed an unrelated bug while testing: a malformed ldapGroupMap value used to throw a raw, confusing exception message instead of a clean validation error.

@john8862

Copy link
Copy Markdown

FreeIPA also has a bit different group membership structure than OpenLDAP. It has attribute called memberofindirect. Here are some examples:

memberof: cn=admins,cn=groups,cn=accounts,dc=example,dc=local
memberofindirect: cn=System: Preserve User,cn=permissions,cn=pbac,dc=example,dc=local

@Hemsby

Hemsby commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detail, that's really helpful. Good to know FreeIPA does maintain memberOf for direct membership, so the fallback I just added likely isn't even needed there. The memberofindirect case (inherited via nested groups or RBAC/PBAC) is a real gap though, neither the original code nor my fix handles that.

I'll deploy a FreeIPA instance to test against directly and get the code updated to handle it properly.

@Hemsby

Hemsby commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

I deployed a real FreeIPA instance (4.13.1) to test this properly rather than guess.

Turns out memberOfIndirect isn't actually retrievable over plain LDAP at all on a stock install. I verified this four separate ways (simple bind, GSSAPI bind, a fresh recheck, and a full attribute dump including operational attributes) and every one shows only memberOf on the wire, populated by the standard 389-ds MemberOf Plugin with the complete transitive closure, both direct and nested/indirect group membership included.

What threw me initially is that ipa user-show --raw --all does show a separate memberofindirect field. But that turns out to be the FreeIPA Python framework's own presentation layer splitting direct from indirect for human readable CLI output, not real LDAP data. Any actual LDAP client, including this PR's code, only ever sees memberOf, and it is already complete.

To confirm end to end, I set up a nested group case (user is a direct member of a child group, which is nested inside a parent group) and mapped the parent group to a Technitium role. Login correctly picked up the indirect membership and applied the mapped permissions, using the unmodified PR code with no changes needed.

So no code changes needed for FreeIPA, it already works correctly for group mapping including indirect membership. Thanks again for pointing this out, it was worth the real test to be sure rather than assume.

This PR has now had complete, real world testing against Active Directory, FreeIPA, and OpenLDAP.

@ShreyasZare

Copy link
Copy Markdown
Member

That's nice to know that FreeIPA and OpenLDAP was tested and are working well. I am planning to test this PR soon so this really helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants