Active Directory and LDAP sign-in, directory synchronisation and OAuth2 bearer tokens
-
Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes three things this guide relies on. A Control Panel signed in to a server on another computer no longer reads or writes its own computer's
hMailServer.inifor the[LDAP]values, which it cannot read from the server, where it showed that file or the defaults as the server's; the sidebar is regrouped, and Ctrl+K takes every old page title to its page. With SMTP logging on, 6.3.6 and every earlier version that offered them wrote the bearer token ofAUTH XOAUTH2andAUTH OAUTHBEARERand the lines of SCRAM-SHA-256, OAUTHBEARER and GSSAPI into the SMTP log in the clear; 6.3.7 hides every line of a SASL exchange but the user name ofAUTH LOGINand the authentication identity ofAUTH PLAIN. An account given theserveradminrole with a list of domains is, over COM as over REST, confined to those domains and no longer the server's administrator, so a script that runs the directory synchronisation's preview and apply as such an account needs the role given with no list. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in to an account only when it is the account's stored address, compared in lower case after a domain alias and the default domain, for the name an OAuth2 bearer token carries as for a password (work item 104), so a name that differs from the stored address in more than the case of the letters A to Z, or has white space at either end, may no longer sign in. On Windows anOAuth2PublicKeyFilewhose path holds characters outside ASCII may be refused withHM7536(one outside the code page, which worked up to 6.3.7, is refused), and then the key verifies no token: give the file, and the folders above it, an ASCII name before upgrading. See the 6.3.8 announcement.This topic covers three ways an organisation's directory or identity provider vouches for somebody on hMailServer 6.3.4's mail protocols: checking a password against Active Directory or another LDAP directory, creating mailboxes from that directory, and accepting OAuth2 bearer tokens. Know this first: 6.3.4 keeps settings in the database, but the
[LDAP]section stays inhMailServer.inion each server, with no REST route and no environment name; the server re-reads it within two seconds of a change, with no restart. The OAuth2 keys are ordinary[Settings]keys, stored like any other.Three mechanisms, three questions
Mechanism The question it answers Control Panel page (Windows) LDAP directory authentication Is this the password of the directory user the account is linked to? Access & abuse protection → Directory authentication (LDAP) Directory synchronisation Which mailboxes should exist, according to the directory? Access & abuse protection → Directory synchronisation OAuth2 bearer tokens Did the identity provider sign this token, and which mailbox does it name? Access & abuse protection → Authentication All three are off by default, and an upgrade switches none of them on: the
[LDAP]section is absent until you add it, andOAuth2Enableddefaults to 0.Where the
[LDAP]section lives- Windows. The Control Panel's page writes the
hMailServer.INIof its own machine. Connected to another machine, its Save is disabled, although "the connection test still works with the values typed here". Work on the server, over Remote Desktop. - Linux. No Control Panel and no COM. The file is
/etc/hmailserver/hMailServer.ini,0640 root:hmailserver:sudoedit /etc/hmailserver/hMailServer.ini. - Not REST or
hmctl. The REST ini routes andhmctl ini sethandle[Settings]keys only. - Per server. On a warm standby each node has its own
[LDAP]; keep the two alike by hand. ServicePasswordis a line of that file. It is not among the[Settings]secrets 6.3.4 seals under the stored-secrets key; the Control Panel's field for it is write-only.
The sign-in keys
Key Default What it does Enabled0 The master switch Serverempty The directory host; required Port0 0 means 636 for LDAPS, 389 otherwise Security2 2 LDAPS, 1 StartTLS, 0 plain; anything else is LDAPS VerifyCertificate1 Check the directory's certificate AllowUnprotectedPassword0 Permit a simple bind over a plain connection BindMethod0 0 simple; 1 Negotiate (Kerberos or NTLM through Windows SSPI) SearchBaseempty Required unless UserDnTemplateis set or the bind is NegotiateUserSearchFilter(&(objectCategory=person)(objectClass=user)(sAMAccountName=%u))How the user's entry is found UserDnTemplateempty Bind directly, without searching ServiceUsername,ServiceDomain,ServicePasswordempty What the search binds with TimeoutSeconds10 Bounds every directory operation; 1 to 90 FallbackToWindowsLogon0 Also try Windows LogonUser; Windows onlyLDAPS with
VerifyCertificate=1is the default and the recommended configuration, proven end to end against a Windows Server 2025 domain controller.Servermust be the name in the directory's certificate: the same setup pointed at the controller's IP address, with the right password, is refused, andHM5920reports a failure "unrelated to the credentials supplied". On Windows, put the issuing CA inLocalMachine\Root; when this was proven the service ran as LocalSystem, and aCurrentUser\Rootimport served it nothing. The bind needs no extra privilege underNT SERVICE\hMailServer, which a new 6.3.4 installation runs as. On Linux the certificate must chain to the system trust store.StartTLS (
Security=1) connects on 389 and upgrades before anything is sent. Plain LDAP is labelled Unprotected LDAP - cleartext (lab networks only): there a simple bind is refused (HM5921) rather than sent, unlessAllowUnprotectedPasswordis 1.Simple or Negotiate. A simple bind sends the password to the directory, so it belongs on LDAPS or StartTLS. Negotiate, Windows only, never sends it; it worked on 389 against a controller that had no LDAPS certificate yet, and suits a mail server not joined to the domain.
Search or template. A search binds with the service credentials, finds the user under
SearchBasewithUserSearchFilter, then binds as the user. It asks for two entries to see ambiguity: more than one match is a configuration fault and authenticates nobody.UserDnTemplateskips the search, so no service credential need be stored at all; with a simple bind, that is the configuration the feature was proven with.Linking an account to the directory
The directory is asked only about linked accounts; others keep their own password. The webmail's sign-in shares the IMAP and POP3 logon, so it takes the directory password too.
- Control Panel: the account dialog's Windows directory tab, with the domain (
ADDomain) and user name (ADUsername); Browse Active Directory searches the forest to fill them. Since 6.3.4 the tab has that name; it was called Directory, and the new Directory entry tab is the organisation's address book, unrelated to Active Directory. - REST:
PUT /api/v1/accounts/{address}withad_enabled,ad_domainandad_username. - COM (Windows):
IsAD,ADDomainandADUsername.
What a link changes. The account's stored password is no fallback: in the proof run, a linked account's local password was refused. Linked accounts bypass local hashing, so they are exempt from the minimum-hash policy and from password expiry, and cannot sign in with SCRAM-SHA-256; their clients use
PLAINorLOGINover TLS. App passwords work for them: the directory holds the password, but an app password is local, which is what makes a second factor possible for a directory account at all. The person cannot change the password in the webmail (POST /api/v1/me/passwordanswers 409), and Forgotten your password? cannot reset it, because this server does not hold it.Without LDAP, on Windows, a linked account is checked by
LogonUser, which needs the mail server's host joined to the domain. On a host that is not joined, Windows answers every domain name, real or misspelt, as a wrong password, so the account can never sign in;HM5911says so, naming the domain and the computer. An empty domain,.or the computer's own name means a local Windows account. Never leave the domain empty by accident: with[LDAP] Enabledoff, an account linked with an empty domain and the user nameadministratorwould accept the server's local Administrator password.FallbackToWindowsLogonnever applies to a wrong password: a directory's rejection deliberately does not fall through toLogonUser, because that would double every failed attempt and trip a lockout policy on a single wrong password. Leave it off on a host that is not domain-joined.Setting it up on Windows
- For LDAPS or StartTLS, put the directory's issuing CA in
LocalMachine\Rooton the mail server. - On the mail server, open Directory authentication (LDAP). Switch directory authentication on (
Enabled); until then a linked account is checked by WindowsLogonUser, not with these settings. Fill in the server, the security, the bind, and either the search orUserDnTemplate; the first card asks Is this configuration complete? - Test the connection with a directory user name, its AD domain and its password. It tries the values typed on the page.
- Save. The server re-reads
[LDAP]within two seconds. - Link one account and sign in with the right password and a wrong one. Over POP3 the proof run got
+OK Mailbox locked and readyand-ERR, and the log showed the bind and the rejection separately. - Link the others by hand, or with directory synchronisation.
Setting it up on Linux
The only directory path is an LDAP simple bind over LDAPS or StartTLS, proven against OpenLDAP's slapd.
- Make sure the directory's certificate chains to the system trust store, and use the name it carries.
- Add the section with
sudoedit /etc/hmailserver/hMailServer.ini, filling in the angle brackets. Without aUserSearchFilterline the search uses Active Directory's filter,(&(objectCategory=person)(objectClass=user)(sAMAccountName=%u)), which finds nobody in another directory such as OpenLDAP; there, write a filter that matches its user entries, with%ufor the name:
[LDAP] Enabled=1 Server=<the host name the directory's certificate carries> Security=2 VerifyCertificate=1 BindMethod=0 SearchBase=<the DN the users are under> UserSearchFilter=<that filter, unless the directory is Active Directory> ServiceUsername=<the search account> ServicePassword=<its password>- Two seconds later the server has it. Link an account over REST (
ad_enabled,ad_domain,ad_username) and sign in with the right password and a wrong one.
Linux has no Negotiate (
BindMethod=1is refused as a directory failure, not a wrong password:HM6420, reported once), noLogonUser(a linked account while[LDAP] Enabledis off is refused withHM6421, which names the settings to set;FallbackToWindowsLogonrefuses withHM6404), and no GSSAPI bind.When the directory cannot answer
So that an outage does not read as users mistyping, infrastructure failures are reported apart from credential rejections, throttled per error and endpoint, and
TimeoutSecondsbounds the wait.Code Meaning HM5920The directory could not be used (a certificate not matching Server, say). Reported at High: unrelated to the credentials, and every account on that directory is affectedHM5921A simple bind would have sent the password unprotected, so the logon was refused first HM5911Windows LogonUser: the host is not joined to the domain; at most once a minute per problemDirectory synchronisation: preview, then apply
Synchronisation reads the directory, works out which mailboxes should exist, and only when asked creates and updates them; it was verified end to end against a Windows Server 2025 domain controller. The planner writes nothing and the applier decides nothing, so a preview cannot promise what the apply would not do.
- Per-domain opt-in. A domain takes part only if its Active Directory domain name is set (
ADDomainNamein the domain dialog,ad_domain_nameover REST), so a search base one level too high cannot provision into unrelated hosted domains. - Nothing is ever deleted, account or message. The most it does is clear
Active, only when asked, and never on a truncated or empty enumeration or for a domain in which nothing was seen. - No password. A made account is marked
IsADand signs in through the directory from the start. - No existing account claimed. An account that is not linked, or is linked to another AD domain, is reported, not changed.
- Paged. Active Directory answers a search beyond
MaxPageSize(1000) with the first page and a success status, so the enumeration pages;SyncMaxUsersis the ceiling.
[LDAP]keyDefault SyncFilter(&(objectClass=user)(objectCategory=person)(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))SyncMailAttributemail;proxyAddressesworks, preferring the capitalisedSMTP:primarySyncUsernameAttribute,SyncDisplayNameAttributesAMAccountName,displayNameSyncMaxUsers5000; at most 100000 SyncScheduleMinutes0, off; 15 at least, 10080 (seven days) at most On Windows:
- Get authentication working first. Synchronisation binds as the service account, honouring
BindMethod, so Negotiate works where there is no LDAPS certificate. - Set the Active Directory domain name on each mail domain that takes part.
- Open Directory synchronisation, check the selection keys, choose the domain and whether absent accounts are marked inactive, and run Preview. It changes nothing.
- Read the report, including the existing accounts it reports and leaves alone.
- Apply. The button stays disabled until a preview of the same options has run.
Behind the page are
Settings.PreviewDirectorySync(DomainName, DisableMissing)andSettings.ApplyDirectorySync, for a server administrator and callable from a script. From a Control Panel on another machine the six selection boxes are read-only and, up to 6.3.6, show their defaults; since 6.3.7 the Control Panel no longer reads its own computer's file for the[LDAP]values, which it cannot read from the server, so it no longer shows them as the server's. Preview and Apply still run inside the server.The unattended schedule (
SyncScheduleMinutes) runs at start and then every so many minutes. It creates and updates but never disables: unattended is where nobody reads the report that would show a mailbox about to be taken away. The schedule is one of the tasks the server registers as it starts, and only ifSyncScheduleMinutesis set then, so restart the service once after first setting it. On Linux, with no COM, there is no preview or apply, and whether the schedule runs there is not documented: treat it as untested.OAuth2 bearer tokens on IMAP, POP3 and SMTP
With
OAuth2Enabled=1, SMTP, IMAP and POP3 offerXOAUTH2andOAUTHBEARER(RFC 7628), by default only over TLS (OAuth2RequireTLS=1). The server verifies the token's signature itself, takes the login name from the claimOAuth2UsernameClaimnames (email), and signs in to that mailbox; auser=the client asserts must match. The mailbox must already exist: a token never makes one.Key Default What it does OAuth2JwksUrlempty The provider's JWK Set; the token's kidpicks the key. https, or plain http to loopback onlyOAuth2JwksCacheSeconds3600 How long the set is kept OAuth2PublicKeyFileempty A PEM RSA or EC public key; the fallback OAuth2HmacSecretempty The HS256 key OAuth2AllowedAlgorithmsRS256HS256, RS256 and ES256 recognised; noneneverOAuth2Issuer,OAuth2Audienceempty Expected issandaud; empty turns the check offOAuth2IntrospectionUrlempty RFC 7662 endpoint asked whether a verified token is still active OAuth2IntrospectionClientId,OAuth2IntrospectionClientSecretempty Who this server introspects as, over HTTP Basic OAuth2IntrospectionCacheSeconds300 Verdict reuse, never past the token's expOAuth2IntrospectionFailOpen0 1 accepts a token the endpoint cannot judge, logging each An unknown
kid, which is what a rotation looks like, fetches the set again at once, at most once a minute; a failed fetch keeps the keys held.HttpProxy, when set, carries these fetches.Where to set them. Each applies when saved: on the Control Panel's Authentication page, with
PUT /api/v1/settings/ini/{name}and the administrator password, withhmctl ini set, or with COMSettings.SetIniSetting. Not inhMailServer.ini: since 6.3.4 the database holds them, and a line edited in the file is not used; Settings: when a save takes effect explains the store.- Choose the key source:
OAuth2JwksUrl, so a rotation needs no copied file; orOAuth2PublicKeyFile; orOAuth2HmacSecretwith HS256 allowed. - Set
OAuth2IssuerandOAuth2Audienceto what the provider writes into its tokens. Left empty, neither is checked: any token the provider's keys signed is accepted, whatever client it was issued to. - Optionally set introspection, so a revoked token stops working before it expires.
- Only then set
OAuth2Enabledto 1. - On Windows, read the OAuth2 / XOAUTH2 item of the Control Panel's External setup list.
Why that order. Switched on without usable key material, the mechanisms are still advertised, every token fails, and failed bearer sign-ins feed the auto-ban, so a correct client can have its address banned.
On Linux before the REST listener is up,
hmailserver --set-settingstores a key; a secret is given by name alone and typed at a prompt. A value stored this way reaches the running server at its next save, reinitialise or restart. Set the issuer and audience too, as step 2 says, putting your provider's values in place of these examples:sudo hmailserver --set-setting OAuth2JwksUrl=https://idp.example.com/keys sudo hmailserver --set-setting OAuth2Issuer=https://idp.example.com sudo hmailserver --set-setting OAuth2Audience=hmailserver sudo hmailserver --set-setting OAuth2Enabled=1 sudo systemctl restart hmailserverSince 6.3.4: a bearer token meets the domain's and the account's country policy, as a password does (a stolen token used to sign in from a refused country); an SMTP session signed in with a token can send more than one message (the second used to be refused with 550 "Login credentials no longer valid"); and
OAuth2HmacSecretandOAuth2IntrospectionClientSecretare sealed at rest once the installation has a stored-secrets key, which an upgrade does not make until an administrator runs--create-stored-secrets-key.Tested how. The regression fixtures play the identity provider on the loopback, to make it misbehave: RS256 and ES256, the JWK Set and its rotation, introspection and fail-open. A real provider's tokens are not yet part of them.
This is inbound only. The
OutboundOAuth2keys andFetchOAuth2Hostsare the server's own token for relaying through and collecting from Microsoft 365; Routes, relays and smart hosts covers relaying with it.Where these end: SCIM, single sign-on and Kerberos
The LDAP backend authenticates; SCIM provisions. Synchronisation reads the directory; SCIM is the other direction, Entra ID or Okta calling
/scim/v2under an API key whose role holdsprovision, and it removes accounts, which synchronisation never does. Single sign-on, OpenID Connect or SAML 2.0, opens the webmail and the Control Deck only, ending inPOST /api/v1/session; the protocols' bearer tokens are a separate, older mechanism with theOAuth2settings above, and single sign-on does not reach mail clients. On Windows, SASLGSSAPI(GssapiEnabled) lets a mail client sign in with a Kerberos ticket, mapped to the account linked to that directory user. SCIM provisioning and single sign-on sets those up; Signing in and Connecting a mail client show the person's side.What it does not do
- No Negotiate, Windows logon or GSSAPI bind on Linux. hMailServer on Linux lists the rest.
- No LDAP server here, and the webmail's organisation directory is typed in, not imported from Active Directory.
Where to read more
- Wiki: Settings Reference sections 9.2 and 11, Capabilities and Configuration, Control Panel Reference, Installing on Linux, Security Hardening, Sign-in and Identity, Architecture
- Documents: SettingsApply.md, ServiceAccount.md, Scim.md, PasskeysAndSingleSignOn.md, TestLab.md
- The Roadmap rows on LDAP, which record how each part was proven
- On this forum: the stored-secrets key, auto-ban and the warm standby
- The public documentation's Creating accounts from Active Directory and Modern authentication
Report a security flaw in any of this by SECURITY.md's routes: a confidential GitLab issue, or email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as one.
- Windows. The Control Panel's page writes the
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login