LINUXOR.SK ... open source notes ...

Email 06 - Active Directory integration

category: solutionz · date: 2019-12-31 · updated: 2026-10-02 · author: LALA

Email Solution · Previous: Internal servers: Postfix · Next: Dovecot authentication

The email servers hold no user database. Who exists, who may send, who may send to the internet and who is on which distribution list are all questions that Postfix puts to Active Directory at the moment a message arrives, through five small LDAP map files. This Article describes what was put into the directory for that, and takes each of the five queries apart.

Why the directory

The requirement was one sentence: user accounts, email aliases and email groups are stored in Microsoft Active Directory (FR7), and the directory is reached over a secured channel, LDAPS (FR8). The reason behind it is the kind of user this system has. Most "users" are devices, servers and applications that send notifications; each of them gets an account of its own, and the people who run the directory already create, disable and audit accounts. An address is switched on by a group membership and switched off by disabling the account, without anybody logging in to a mail server.

The design divides the accounts into two kinds. Email senders are the accounts of devices, servers and applications. Email readers are people who open the webmail and, by the design, do not send. An account's userPrincipalName is its mail address and its login at the same time, which is why the mail domain of each site is the name of its AD domain: ad.example.net in site 1 and ad-dc2.example.net in site 2.

Directory servers

SiteServerAddressKind
1DC1-A-CAD00110.11.88.145bare metal
1DC1-B-CAD00110.11.88.146bare metal
1DC1-A-VCAD00110.11.16.209virtual
1DC1-A-VCAD00210.11.16.210virtual
2DC2-A-VCAD00110.12.16.209virtual
2DC2-A-VCAD00210.12.16.210virtual

The table is the design's. The site 2 files name both servers of site 2. For site 1 only the first concept of 2018 shows files, and they name the two virtual servers only, by DNS name:

ini
server_host     = ldaps://dc1-a-vcad001.adm.example.net:636, ldaps://dc1-a-vcad002.adm.example.net:636

As built in site 2 the same line holds addresses, and the comment above it says why:

ini
# Due to problems in LDAP clients/LDAP libraries in dual IP stacks (IPv4, IPv6) environments, this configuration (parameter "server_host") contains only IPv4 addresses.
server_host     = ldaps://10.12.16.209:636, ldaps://10.12.16.210:636

The mail servers are dual-stack, and the design records the outcome in one sentence: "Active Directory servers are integrated only on IPv4 addresses due to problems in LDAP libraries." What exactly failed is not written down anywhere in the Source material. Postfix tries the servers of server_host in the order given, so the second domain controller is the fallback of the first.

The channel is LDAPS on port 636 and nothing else: start_tls = no, version = 3, and the firewall rule of the first design opens TCP 636 from the mail server to the directory servers. None of the five files sets tls_ca_cert_file or tls_require_cert, and the install notes do not show the CA of the organisation being given to the LDAP client. The default of tls_require_cert is no, then and now, so the connection was encrypted but the certificate of the domain controller was not verified.

Accounts and organisational units

ObjectWherePurpose
vmail_svcCN=vmail_svc,OU=Users_svcbind account for the queries of Postfix and Dovecot
User accountsOU=Users_STDpeople; webmail
Device accountsOU=msx_SVC,OU=Users_SVCdevices, servers and applications that send mail
Authorization groupsOU=RBAC_GROUPS,OU=RBAChold the accounts, one group per organisation
Role groupsOU=RBAC_ROLES,OU=RBAChold the authorization groups; the mail servers know only these
Distribution listsOU=DISTRIBUTION_GROUPSgroups that work as mail addresses

All paths are relative to the base DN of the domain; the design writes them for site 1, DC=ad,DC=example,DC=net. A device account is named after its service with the suffix _mail: gitlab_mail is the design's example and sensu_mail appears in the exception lists of the encryption filter.

The design calls vmail_svc a special account "which will be used to query LDAP database". Every map binds with it, a simple bind with DN and password, and the password stands in clear text in each of the five Postfix files and in the two Dovecot files, on each of the three internal servers.

Groups

The design uses a two-level pattern: an account is put into an authorization group, and the authorization groups are members of a role group.

Authorization groupMembersRight
SMTP_ORG, SMTP_PARTNER1, SMTP_PARTNER2accounts of the organisation and of each of two partnerssend to addresses of the own domains
ESMTP_ORG, ESMTP_PARTNER1, ESMTP_PARTNER2the same splitsend to external domains
IMAP_ORG, IMAP_PARTNER1, IMAP_PARTNER2the same splituse webmail

The role groups collect them.

Role groupContainsAsked by
SMTP_ACCESSthe three SMTP_ groupsPostfix, ad_allow_internal_emails.cf
ESMTP_ACCESSthe three ESMTP_ groupsPostfix, ad_allow_external_emails.cf
IMAP_ACCESSthe three IMAP_ groupsDovecot

The operations chapter of the design says it in the form of a procedure: an address is activated by adding the account to one of the SMTP_ groups, and the right to send to external domains is one more membership, in an ESMTP_ group. As the restriction lists are built (Internal servers: Postfix), the second without the first is worth nothing: the sender check asks for SMTP_ACCESS before the recipient check asks for ESMTP_ACCESS.

One detail differs between the design and the files. The design places the role groups directly in OU=RBAC_ROLES,OU=RBAC; every filter, in the first concept and in the archived files alike, looks for them one level deeper, in OU=APPS,OU=RBAC_ROLES,OU=RBAC. The files are what ran.

The five maps

FileParameter in main.cfKeyAnswer
ad_allow_internal_emails.cfcheck_sender_access in the sender listenvelope senderOK if in SMTP_ACCESS
ad_allow_external_emails.cfcheck_sender_access in the recipient listenvelope senderOK if in ESMTP_ACCESS
ad_sender_login_maps.cfsmtpd_sender_login_mapsenvelope senderlogin that owns the address
ad_virtual_mailbox_maps.cfvirtual_mailbox_mapsrecipientmailbox path
ad_virtual_group_maps.cfvirtual_alias_mapsrecipientaddresses of the group members

All five begin with the same block: servers, base DN, scope = sub, the bind account. The last three are referenced as proxy:ldap:, which lets the proxymap service keep the connections open for all Postfix processes; the two access tables are plain ldap: and are opened by each smtpd process itself. The same five files lie on the mailbox server.

Allow internal and allow external differ in one word. The filter of the first:

output 1 line
(&(memberOf:1.2.840.113556.1.4.1941:=CN=SMTP_ACCESS,OU=APPS,OU=RBAC_ROLES,OU=RBAC,DC=ad-dc2,DC=example,DC=net)(userPrincipalName=%s))

Postfix replaces %s with the lookup key, the sender address, so the second half finds the account whose userPrincipalName is that address. The first half is the interesting one. A plain memberOf= matches direct membership only, and no account is a direct member of SMTP_ACCESS; the accounts are in SMTP_ORG or in a partner's group. The number between the colons is the OID of an Active Directory matching rule, LDAP_MATCHING_RULE_IN_CHAIN, which makes the domain controller walk the chain of nested groups. Without it the two-level group model could not be asked in one query. The map then needs to return something that an access table understands: result_attribute = userPrincipalName fetches a value and result_format = OK replaces it with the word OK. An address outside the group returns nothing, and the restriction list moves on. Whole files: allow internal emails, allow external emails.

Sender login maps answers the question to whom an address belongs:

output 1 line
(&(userPrincipalName=%s)(objectClass=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))

The second OID is the bitwise AND rule. userAccountControl is a bit field, the value 2 is the bit "account disabled", and the negated term removes disabled accounts from the result. The map returns the userPrincipalName itself: the owner of an address is the login of the same name. Whether Postfix ever asks this map in the configuration as archived is discussed in the previous Article. Whole file: sender login maps.

Virtual mailbox maps tells Postfix which recipients of the own domain exist. The filter is (&(objectclass=person)(userPrincipalName=%s)), and result_format = %d/%u/Maildir/ turns the returned address into a path made of its domain part and its local part, the mailbox below /data/vmail on the mailbox server (Mailbox server). On the internal pair the path is not used, only the fact that there is an answer. Two things are missing from this filter when it is compared with the others: it does not ask for a group and it does not exclude disabled accounts. Every person object of the domain can receive mail, activated or not. Whole file: virtual mailbox maps.

Virtual group maps is the distribution list:

ini
search_base     = OU=DISTRIBUTION_GROUPS,DC=ad-dc2,DC=example,DC=net
query_filter    = (&(objectClass=group)(sAMAccountName=%u))
special_result_attribute = member
result_attribute = mail

It is the only map with a narrower search base, and the only one that uses %u, the local part of the recipient address: mail to name@ad-dc2.example.net looks for a group with the sAMAccountName name in that OU. special_result_attribute names an attribute that contains DNs; Postfix reads each entry that member points to and collects its mail attribute, and it does so recursively, so a list may contain a list. A new distribution list is a new group in one OU and needs no change on a mail server. In the first concept this map returned userPrincipalName; as archived it returns mail, and a member without a filled mail attribute is silently not a recipient. Whole file: virtual group maps.

One authenticated submission

mermaid
sequenceDiagram
  participant c as Client
  participant p as Postfix smtpd
  participant d as Dovecot auth
  participant ad as Domain controller
  c->>p: EHLO, STARTTLS, AUTH with login and password
  p->>d: credentials over the socket private/auth
  d->>ad: LDAPS bind as vmail_svc, search userPrincipalName
  ad-->>d: DN of the account
  d->>ad: LDAPS bind as that DN with the password
  ad-->>d: bind accepted
  d-->>p: login accepted
  c->>p: MAIL FROM and RCPT TO
  p->>ad: is the sender in SMTP_ACCESS
  ad-->>p: userPrincipalName, read as OK
  p->>ad: is the sender in ESMTP_ACCESS
  ad-->>p: entry or nothing
  p->>ad: does the recipient exist, own domain only
  ad-->>p: mailbox or group members
  p-->>c: 250 or reject

Three things in the picture are easy to overlook. Postfix never sees the directory during authentication; it hands login and password to Dovecot and gets back yes or no. Dovecot does not compare passwords: it looks the account up as vmail_svc and then binds as the account itself, so the domain controller is the one that checks the password. And every message costs several LDAP queries on top, each check_sender_access asking more than once, because an access table is tried with the full address and then with its parts.

Dovecot uses two files of its own, one per OU, for user accounts and for device accounts, with the same bind account, the same servers and the same two matching rules. They are the subject of Dovecot authentication; the files are dovecot-ldap.conf.users and dovecot-ldap.conf.devices.

Testing

The install notes of the five servers contain no test of the maps: no postmap -q, no ldapsearch, no captured answer. The notes refer to the configuration files as attachments, and whatever was typed to prove the maps the first time was not kept. debuglevel = 0 at the end of every map is the one trace of that work: the switch to turn up while a filter does not match.

What I would do differently

← solutionz