LDAP Server
Many organisations keep their users in an LDAP directory such as OpenLDAP or Microsoft Active Directory. Stalwart can use that directory as the source of its accounts and passwords. Users then sign in to mail with the credentials they already use elsewhere, and the server accepts mail for the addresses recorded in the directory. Stalwart works with directories that implement RFC 4511.
The server queries the directory at two moments: when a user signs in, and when mail arrives for an address in a domain the directory serves. For each entry it finds, it keeps a local account (Directories).
To connect a directory, create a Directory object of type Ldap (in the WebUI under Settings › Authentication › Directories). Then set its id as directoryId on the Authentication singleton or, in the Enterprise edition, on the domains the directory serves.
Account provisioning
Section titled “Account provisioning”The server does not copy accounts from the directory in advance. It creates a local account the first time it needs one: when the user signs in, or when a message arrives for an address and the directory returns the entry. An account can therefore receive mail before its user has ever signed in.
Each later lookup updates the local account from the directory. The description and group memberships are refreshed, and any new aliases are added. Aliases removed from the directory are kept, however.
SCIM changes this behaviour. In a domain opened to SCIM provisioning, the directory neither creates nor updates accounts. A sign-in by an account that SCIM has not provisioned then fails with Accounts in this domain are provisioned through SCIM, just-in-time provisioning is disabled.
Nothing is ever removed automatically. An entry deleted from the directory stops appearing in query results, so mail for its address is no longer accepted. The server cannot observe the deletion itself, however. The local account and its mailbox therefore remain, and keep counting against the licensed account limit, until an administrator deletes them. For the identity provider to delete and suspend accounts explicitly, enable SCIM provisioning for the domain. Provisioning models describes how the two interact.
Connection
Section titled “Connection”url is the address of the directory server. Its scheme decides how the connection is encrypted:
- With an
ldap://URL, the connection starts unencrypted. EnablinguseTlsupgrades it with STARTTLS before anything is sent. - With an
ldaps://URL, conventionally on port 636, the connection uses TLS from the start, souseTlshas no effect.
Use one of the two whenever the directory is not on the same host. Both authentication modes send secrets over this connection: in password-comparison mode the directory sends password hashes, and in bind mode the server sends the user’s password. allowInvalidCerts accepts certificates that fail validation and is meant for testing only.
timeout bounds both the establishment of a connection and each LDAP operation. It is 30 seconds by default. When several servers are configured (see Secondary servers), a connection attempt can take that long for each server before it fails.
The server does not open a new connection for every request. It keeps a pool of connections to the directory, and poolMaxConnections caps the number of simultaneous connections. Three timeouts bound the steps of handing out a connection from the pool:
poolTimeoutWaitlimits how long a request waits for a free connection.poolTimeoutCreatelimits how long opening a new connection may take.poolTimeoutRecyclelimits how long a used connection may take to be checked before reuse.
Occasional reconnects by the directory do not cause failed sign-ins. When a login fails because a pooled connection was dropped or had gone stale, the server retries once on a fresh connection before reporting the failure.
Secondary servers
Section titled “Secondary servers”A directory often runs on several replicated servers, so that sign-ins keep working when one of them is down. List the backups in urlFailover. When the server in url cannot be reached, the backups are tried in order, and the first one that accepts a connection becomes the active server.
The server returns to the primary as soon as it can. While a backup is active, the primary is probed at most once a minute, and the server switches back as soon as the primary responds. Each switch is logged as a store.ldap-warning event with the message Switched active LDAP server. A transport error during an operation is retried once on a new connection, as it is without failover.
The following settings make ldap1 the primary server and ldap2 and ldap3 its backups:
{ "url": "ldaps://ldap1.example.org", "urlFailover": {"ldaps://ldap2.example.org": true, "ldaps://ldap3.example.org": true}}Service account
Section titled “Service account”The server needs its own account in the directory to search it. This service account is set in bindDn and bindSecret. Every search starts at baseDn, for example dc=example,dc=org, and the service account needs read access to the user and group entries under it.
The service account is needed in both authentication modes, because recipient lookups and group resolution always use it. When bindDn is empty, the server searches anonymously, which most directories do not allow.
bindSecret can hold the password itself, or read it from an environment variable or a file, which keeps the password out of the configuration. The following settings read it from the LDAP_BIND_PASSWORD environment variable:
{ "baseDn": "dc=example,dc=org", "bindDn": "cn=stalwart,ou=services,dc=example,dc=org", "bindSecret": {"@type": "EnvironmentVariable", "variableName": "LDAP_BIND_PASSWORD"}}Authentication modes
Section titled “Authentication modes”A password can be checked in two places: by the directory, or by the server against the hash stored in the directory. bindAuthentication chooses between them. Bind authentication is enabled by default.
Bind authentication
Section titled “Bind authentication”With bind authentication, the directory decides whether the password is correct. The server uses the service account to search for the user’s entry with filterLogin. It then binds to the directory as that entry, with the password the user supplied. Because the directory performs the check, this mode works with directories that do not reveal password hashes, which includes Active Directory and most hardened OpenLDAP installations.
Bind mode has one drawback. Since no hash is read, the server cannot tell from the password itself that it has changed. It detects a change through attrSecretChanged, an attribute whose value changes whenever the password does, and then signs out the user’s OAuth applications. If the attribute is not configured, or not present on the directory entries, OAuth tokens stay valid after a password change in the directory.
Password comparison
Section titled “Password comparison”When bindAuthentication is set to false, the server uses password comparison instead. It reads the password hash with the service account, from the attribute named in attrSecret, and verifies the supplied password against it. Two conditions follow: the hash must be in one of the supported formats, and the service account must be allowed to read it.
In both modes, a wrong password is reported as invalid credentials. Sign-in failures do not reveal whether the account exists in the directory.
Lookup filters
Section titled “Lookup filters”The server locates entries with three LDAP search filters. Each filter contains placeholders, which the server replaces with the value being looked up after LDAP escaping:
{user}stands for the whole value. It can also be written{username},{email}or, as in older configurations,?.{local}stands for the part before the@.{domain}stands for the part after the@.
A filter without a placeholder fails to load with Missing parameter placeholders in value .... A filter with an unknown placeholder fails with Unknown LDAP filter placeholder: <name>.
Each filter serves one lookup:
filterLoginfinds the entry for the name a user signs in with. By default it is(&(objectClass=inetOrgPerson)(mail=?)).filterRecipientfinds the user or group entry for a recipient address. By default it is(|(&(objectClass=inetOrgPerson)(|(mail=?)(mailAlias=?)))(&(objectClass=groupOfNames)(|(mail=?)(mailAlias=?)))).filterMemberOffinds the groups of an entry, with?replaced by the entry’s DN. By default it is(&(objectClass=groupOfNames)(member=?)). It is needed only when user entries do not carry their group memberships.
The defaults suit directories built on inetOrgPerson and groupOfNames. A directory that uses posixAccount and posixGroup entries needs other filters. The following filters find users by uid when they sign in, and users or groups by mail or mailAlias when mail arrives:
{ "filterLogin": "(&(objectClass=posixAccount)(uid=?))", "filterRecipient": "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(mailAlias=?)))"}Attribute mapping
Section titled “Attribute mapping”Once an entry is found, the server reads the account’s information from its attributes. The following fields name those attributes. Each field is a set, so several attributes can be listed for it:
| Field | Information | Default |
|---|---|---|
attrEmail | Primary address | mail |
attrEmailAlias | Aliases | mailAlias |
attrDescription | Display name | description |
attrMemberOf | Groups the entry belongs to | memberOf |
attrClass | Object class, to tell users from groups | objectClass |
attrSecret | Password hash, in password-comparison mode | userPassword |
attrSecretChanged | Value that changes with the password, in bind mode | pwdChangeTime |
The server tells users from groups by their class. An entry whose class equals groupClass is a group, and any other entry is a user. groupClass is groupOfNames by default.
Quotas, roles and permissions are not read from the directory. They are set on the local account in Stalwart (Quotas).
Active Directory
Section titled “Active Directory”Active Directory needs settings that differ from the defaults in three respects. It does not reveal password hashes, so bind authentication is required. It uses its own object classes. It also records group membership in memberOf.
The following object is a starting point for the domain example.org, where users sign in with their user principal name and receive mail at the address in mail. It keeps bind authentication, matches user and group entries, reads the display name from displayName and detects password changes through pwdLastSet:
{ "@type": "Ldap", "description": "Active Directory", "url": "ldaps://dc1.example.org", "baseDn": "dc=example,dc=org", "bindDn": "CN=Stalwart,OU=Service Accounts,DC=example,DC=org", "bindSecret": {"@type": "EnvironmentVariable", "variableName": "LDAP_BIND_PASSWORD"}, "bindAuthentication": true, "filterLogin": "(&(objectClass=user)(userPrincipalName=?))", "filterRecipient": "(|(&(objectClass=user)(mail=?))(&(objectClass=group)(mail=?)))", "filterMemberOf": "(&(objectClass=group)(member=?))", "attrEmail": {"mail": true}, "attrDescription": {"displayName": true}, "attrMemberOf": {"memberOf": true}, "attrSecretChanged": {"pwdLastSet": true}, "groupClass": "group"}