OpenSMTPD, Dovecot and ldapd on OpenBSD 5.7

       2535 words, 12 minutes

Looking to replace my old Postfix/Dovecot configuration with more native OpenBSD stuff, I finally ended with a configuration than seems suitable to me. I’ll be hosting virtual users and mail aliases in ldapd(8), smtpd(8) will deal with email receiving/sending and dovecot(1) will be in charge of email delivery using LMTP and email reading using IMAP. Of course, spamd(8) will do a bit of work in front of OpenSMTPD. All of those will run on OpenBSD 5.7.

How OpenSMTPD works with LDAP data

Using LDAP to host aliases and accounts for OpenSMTPD requires a bit of organization. It’s not exactly configured as I used to do and used to see in most Enterprise directory. Usually, aliases are objects containing the alias reference and a set of destination emails. Then there are users account containing “human information” and the user principal email address.

In my OpenSMTPD case, I had to apply those rules:

The directory

I’ve used as much stock stuff as possible. Unfortunately there are missing parts to store mail aliases. So you need to get an extra schema file:

# ftp -o "/etc/ldap/misc.schema" \

I’ve installed LDAP client tools so that I can manage things locally. It’s not required but quite useful:

# pkg_add openldap-client

The LDAP admin password can be store in clear or encrypted. I’d rather have it stored encrypted. So I have to generate the encrypted string first:

# encrypt -b a RootLDAPsecret

Finally, the default ldapd.conf file can be used and modify to configure the LDAP base:

# cp /etc/examples/ldapd.conf /etc/                                                                                                
# diff -u /etc/examples/ldapd.conf /etc/ldapd.conf
--- /etc/examples/ldapd.conf    Sun Mar  8 17:51:15 2015
+++ /etc/ldapd.conf     Tue Jul 14 23:33:35 2015
@@ -3,16 +3,20 @@
 schema "/etc/ldap/core.schema"
 schema "/etc/ldap/inetorgperson.schema"
 schema "/etc/ldap/nis.schema"
+schema "/etc/ldap/misc.schema"
-listen on lo0
+listen on lo0 secure
+listen on vmx0 ldaps
 listen on "/var/run/ldapi"
-#namespace "dc=example,dc=com" {
-#      rootdn          "cn=admin,dc=example,dc=com"
-#      rootpw          "secret"
-#      index           sn
-#      index           givenName
-#      index           cn
-#      index           mail
+namespace "dc=tumfatig,dc=net" {
+       rootdn          "cn=admin,dc=tumfatig,dc=net"
+       rootpw          "{CRYPT}$2b$10$Kmj26pNwDu2Xi2K01KIhCeIGAUeXs/7AwPkyCLlSEXLVmTvfFGTSy"
+       index           sn
+       index           givenName
+       index           cn
+       index           mail
+       index           objectClass
+       index           uid

Time to enable and start the daemon:

# rcctl enable ldapd
# rcctl start ldapd       

In my case, I use a service account that has read access on the whole LDAP tree. That enables various daemons, like opensmtpd, not to use the admin account to do the first bindings. The service account is build over the “person” LDAP object. Once again, the password is prepared using the command line:

# encrypt -b a NotSoSimplePass
# ldapadd -H ldap://localhost -D "cn=admin,dc=tumfatig,dc=net" -W
dn: cn=service,dc=tumfatig,dc=net
objectClass: top
objectClass: person
sn: Service account
cn: service
userPassword: {CRYPT}$3c$88$td(...).

adding new entry "cn=service,dc=tumfatig,dc=net"

The directory tree and objects

I use a dedicated OU for storing aliases, one for email accounts and one for mail domains.

The email domains look like those:

# domains,
dn: ou=domains,dc=tumfatig,dc=net
objectClass: organizationalUnit
ou: domains

#, domains,
objectClass: domain

Add as much as you like. They will be automatically checked for acceptance by smtpd(8).

A standart email alias goes:

#, aliases,
objectClass: nisMailAlias

This means “email to will be delivered to email ”. This will also work if “rfc822MailMember” refers to external email address, like gmail or so.

A final email alias goes:

#, aliases,
rfc822MailMember: jca
objectClass: nisMailAlias

This means “email to will be delivered to user jca”. AFAIK, OpenSMTPD requires a real user to deliver mails.

A multiple-recipient alias goes:

# contact, aliases,
dn: cn=contact,ou=aliases,dc=tumfatig,dc=net
cn: daemon
cn: www
cn: abuse
cn: security
cn: root
cn: postmaster
cn: contact
objectClass: nisMailAlias

This means “for each configured domain, daemon@, www@, … will be delivered to ”. This is quite useful to reference general pseudo accounts, RFC 2142 mailboxes, …

A virtual user is configured this way:

# jca, users,
dn: uid=jca,ou=users,dc=tumfatig,dc=net
uid: jca
userPassword:: e1(...)=
cn: Joel
gidNumber: 2000
homeDirectory: /home/vmail
objectClass: posixAccount
objectClass: inetOrgPerson
sn: Carnat
uidNumber: 2000

This will be matched by aliases pointing rfc822MailMember to “jca”. OpenSMTPD will look for uid, uidNumber, gidNumber and homeDirectory. If you let OpenSMTPD store mails, it’ll try putting the mail in “/home/vmail”. In my case, dovecot will be in charge of writing the mail ; so those field values must exists only to prevent smtpd(8) to issue errors.

In my actual configuration, the users will need to exist in /etc/passwd. So don’t forget to issue commands like:

# useradd -d /var/empty -u 2001 -g nogroup -s /sbin/nologin jca

For every LDAP inetOrgPerson.

The IMAP and LTMP daemon

I will use Dovecot for two things: delivering the email on disk and enable mail reading with IMAPS.

Using Dovecot/LMTP rather than OpenSMTPD enables storing using a single directory and user. This is the standard way for virtual users. So first of all, create the virtual user mail account and setup a few systems things:

# groupadd -g 2000 vmail
# useradd -m -u 2000 -g vmail -s /sbin/nologin -d /home/vmail vmail
# mkdir /home/vmail
# chown _dovecot:_dovecot /home/vmail

Install and configure Dovecot:

# pkg_add dovecot-ldap

# diff -u /tmp/login.conf.orig /etc/login.conf 
--- /tmp/login.conf.orig        Sun Mar  8 17:51:15 2015
+++ /etc/login.conf     Wed Jul 15 22:12:14 2015
@@ -96,3 +96,8 @@
+       :openfiles-cur=512:\
+       :openfiles-max=2048:\
+       :tc=daemon:

# [ -f /etc/login.conf.db ] && cap_mkdb /etc/login.conf

# usermod -L dovecot _dovecot

The various configuration files are modified this way:

--- /usr/local/share/examples/dovecot/example-config/dovecot.conf       Sat Mar  7 22:05:14 2015
+++ /etc/dovecot/dovecot.conf   Wed Jul 15 22:56:14 2015
@@ -21,7 +21,7 @@
 # --sysconfdir=/etc --localstatedir=/var
 # Protocols we want to be serving.
-#protocols = imap pop3 lmtp
+protocols = imap lmtp
 # A comma separated list of IPs or hosts where to listen in for connections. 
 # "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces.

--- /usr/local/share/examples/dovecot/example-config/conf.d/10-auth.conf        Sat Mar  7 22:05:13 2015
+++ /etc/dovecot/conf.d/10-auth.conf    Wed Jul 15 22:57:16 2015
@@ -119,9 +119,9 @@
 #!include auth-deny.conf.ext
 #!include auth-master.conf.ext
-!include auth-system.conf.ext
+#!include auth-system.conf.ext
 #!include auth-sql.conf.ext
-#!include auth-ldap.conf.ext
+!include auth-ldap.conf.ext
 #!include auth-passwdfile.conf.ext
 #!include auth-checkpassword.conf.ext
 #!include auth-vpopmail.conf.ext

--- /usr/local/share/examples/dovecot/example-config/conf.d/10-mail.conf        Sat Mar  7 22:05:13 2015
+++ /etc/dovecot/conf.d/10-mail.conf    Thu Jul 16 00:17:27 2015
@@ -27,7 +27,7 @@
 # <doc/wiki/MailLocation.txt>
-#mail_location = 
+mail_location = maildir:/home/vmail/%u
 # If you need to set multiple mailbox locations or want to change default
 # namespace settings, you can do it by defining namespace sections.
@@ -103,8 +103,8 @@
 # System user and group used to access mails. If you use multiple, userdb
 # can override these by returning uid or gid fields. You can use either numbers
 # or names. <doc/wiki/UserIds.txt>
-#mail_uid =
-#mail_gid =
+mail_uid = vmail
+mail_gid = vmail
 # Group to enable temporarily for privileged operations. Currently this is
 # used only with INBOX when either its initial creation or dotlocking fails.

--- /usr/local/share/examples/dovecot/example-config/conf.d/10-master.conf      Sat Mar  7 22:05:13 2015
+++ /etc/dovecot/conf.d/10-master.conf  Wed Jul 15 23:43:08 2015
@@ -16,7 +16,8 @@
 service imap-login {
   inet_listener imap {
-    #port = 143
+    address =
+    port = 143
   inet_listener imaps {
     #port = 993
@@ -51,11 +52,11 @@
   # Create inet listener only if you can't use the above UNIX socket
-  #inet_listener lmtp {
+  inet_listener lmtp {
     # Avoid making LMTP visible for the entire internet
-    #address =
-    #port = 
-  #}
+    address =
+    port = 24
+  }
 service imap {

--- /usr/local/share/examples/dovecot/example-config/conf.d/10-ssl.conf Sat Mar  7 22:05:13 2015
+++ /etc/dovecot/conf.d/10-ssl.conf     Wed Jul 15 22:39:11 2015
@@ -3,14 +3,14 @@
 # SSL/TLS support: yes, no, required. <doc/wiki/SSL.txt>
-#ssl = yes
+ssl = yes
 # PEM encoded X.509 SSL/TLS certificate and private key. They're opened before
 # dropping root privileges, so keep the key file unreadable by anyone but
 # root. Included doc/ can be used to easily generate self-signed
 # certificate, just make sure to update the domains in dovecot-openssl.cnf
-ssl_cert = </etc/ssl/dovecotcert.pem
-ssl_key = </etc/ssl/private/dovecot.pem
+ssl_cert = </etc/ssl/gandi.crt
+ssl_key = </etc/ssl/private/gandi.key
 # If key file is password protected, give the password here. Alternatively
 # give it when starting dovecot with -p parameter. Since this file is often
@@ -46,7 +46,7 @@
 #ssl_dh_parameters_length = 1024
 # SSL protocols to use
-#ssl_protocols = !SSLv2
+ssl_protocols = !SSLv2 !SSLv3
 # SSL ciphers to use
 #ssl_cipher_list = ALL:!LOW:!SSLv2:!EXP:!aNULL

--- /usr/local/share/examples/dovecot/example-config/conf.d/20-lmtp.conf        Sat Mar  7 22:05:13 2015
+++ /etc/dovecot/conf.d/20-lmtp.conf    Wed Jul 15 23:36:56 2015
@@ -15,6 +15,6 @@
 protocol lmtp {
   # Space separated list of plugins to load (default is global mail_plugins).
-  #mail_plugins = $mail_plugins
+  mail_plugins = $mail_plugins
\ No newline at end of file

--- /usr/local/share/examples/dovecot/example-config/dovecot-ldap.conf.ext      Sat Mar  7 22:05:14 2015
+++ /etc/dovecot/dovecot-ldap.conf.ext  Fri Jul 17 22:52:41 2015
@@ -21,14 +21,14 @@
 # LDAP URIs to use. You can use this instead of hosts list. Note that this
 # setting isn't supported by all LDAP libraries.
-#uris = 
+uris = ldap://
 # Distinguished Name - the username used to login to the LDAP server.
 # Leave it commented out to bind anonymously (useful with auth_bind=yes).
-#dn = 
+dn = cn=service,dc=tumfatig,dc=net
 # Password for LDAP server, if dn is specified.
-#dnpass = 
+dnpass = TheClearComplexPassword
 # Use SASL binding instead of the simple binding. Note that this changes
 # ldap_version automatically to be 3 if it's lower. Also note that SASL binds
@@ -67,7 +67,7 @@
 # The pass_filter is used to find the DN for the user. Note that the pass_attrs
 # is still used, only the password field is ignored in it. Before doing any
 # search, the binding is switched back to the default DN.
-#auth_bind = no
+auth_bind = yes
 # If authentication binding is used, you can save one LDAP request per login
 # if users' DN can be specified with a common template. The template can use
@@ -83,14 +83,14 @@
 # For example:
 #   auth_bind_userdn = cn=%u,ou=people,o=org
-#auth_bind_userdn =
+auth_bind_userdn = uid=%u,ou=users,dc=tumfatig,dc=net
 # LDAP protocol version to use. Likely 2 or 3.
 #ldap_version = 3
 # LDAP base. %variables can be used here.
 # For example: dc=mail, dc=example, dc=org
-base =
+base = ou=users,dc=tumfatig,dc=net
 # Dereference: never, searching, finding, always
 #deref = never
@@ -114,7 +114,7 @@
 #   %u - username
 #   %n - user part in user@domain, same as %u if there's no domain
 #   %d - domain part in user@domain, empty if user there's no domain
-#user_filter = (&(objectClass=posixAccount)(uid=%u))
+user_filter = (&(objectClass=posixAccount)(uid=%u))
 # Password checking attributes:
 #  user: Virtual user name (user@domain), if you wish to change the
@@ -132,7 +132,7 @@
 #  homeDirectory=userdb_home,uidNumber=userdb_uid,gidNumber=userdb_gid
 # Filter for password lookups
-#pass_filter = (&(objectClass=posixAccount)(uid=%u))
+pass_filter = (&(objectClass=posixAccount)(uid=%u))
 # Attributes and filter to get a list of all users
 #iterate_attrs = uid=user

Time to start the daemon:

# rcctl enable dovecot
# rcctl start dovecot

OpenSMTPD will use LMTP socket. But my configuration allows testing the daemon on localhost. Here’s an example:

# telnet localhost 24
Connected to localhost.
Escape character is '^]'.
220 Dovecot ready.
LHLO localhost
250 2.1.0 OK
RCPT TO:<jca>
250 2.1.5 OK
354 OK
Subject: test

Delivery test
250 2.0.0 <jca> n5TaC1VxqVXXTgAAxLOgNw Saved
221 2.0.0 OK
Connection closed by foreign host.

The SMTP daemon

OpenSMTPD 5.4.4 is shipping with OpenBSD 5.7. smtpd(8) is simple and it’s configuration is clean:

# cat /etc/mail/smtpd.conf                                                                                                        
# OpenSMTPD configuration

pki certificate "/etc/ssl/gandi.crt"
pki key "/etc/ssl/private/gandi.key"

listen on lo0
listen on egress tls pki auth-optional
listen on egress port submission tls-require pki auth

table vusers ldap:/etc/mail/ldap.conf
table vdomains ldap:/etc/mail/ldap.conf

accept from any for domain <vdomains> virtual <vusers> deliver to lmtp "/var/dovecot/lmtp"

accept from local for any relay

# cat /etc/mail/ldap.conf
# LDAP server
url                     ldap://
basedn                  dc=tumfatig,dc=net
username                cn=service,dc=tumfatig,dc=net
password                TheClearComplexPasswd

# Mail domains
domain_filter           (&(objectClass=domain)(dc=%s))
domain_attributes       dc

# SMTP submission / authentication
credentials_filter      (&(objectClass=posixAccount)(uid=%s))
credentials_attributes  uid,userPassword

# SMTP delivery / IMAP authentication
userinfo_filter         (&(objectClass=posixAccount)(uid=%s))
userinfo_attributes     uid,uidNumber,gidNumber,homeDirectory

# SMTP aliases
alias_filter            (&(objectClass=nisMailAlias)(cn=%s))
alias_attributes        rfc822MailMember

OpenSMTPD will use the FQDN as a default. In my case, the public MX name is different from the server name. To match the external name, I use an extra configuration file:

# cat /etc/mail/mailname

Time to run the daemon with the new configuration:

# rcctl restart smtpd

From now, I can receive emails for every domains and aliases referenced in LDAP. VoilĂ !

The anti spam system

spamd(8) is the default smtp protection system. Let’s have it working too.

# rcctl enable spamd
# rcctl set spamd flags -G 10:4:864 -h 
# rcctl start spamd

# rcctl enable spamlogd
# rcctl start spamlogd  

# cat > /etc/mail/nospamd                                                                                                 

# diff -u /tmp/pf.conf.orig /etc/pf.conf                                                                                                
--- /tmp/pf.conf.orig   Sun Mar  8 17:51:15 2015
+++ /etc/pf.conf        Thu Jul 16 23:42:51 2015
@@ -7,5 +7,14 @@
 block return   # block stateless traffic
 pass           # establish keep-state
+# rules for spamd(8)
+table <spamd-white> persist
+table <nospamd> persist file "/etc/mail/nospamd"
+pass in on egress proto tcp from any to any port smtp \
+    rdr-to port spamd
+pass in on egress proto tcp from <nospamd> to any port smtp
+pass in log on egress proto tcp from <spamd-white> to any port smtp
+pass out log on egress proto tcp to any port smtp
 # By default, do not permit remote connections to X11
 block return in on ! lo0 proto tcp to port 6000:6010

# pfctl -f /etc/pf.conf

# crontab -e
0 * * * * sleep $((RANDOM \% 1800)) && /usr/libexec/spamd-setup

# /usr/libexec/spamd-setup

Final thoughts

Finally, I can replace my old Postfix configuration with native smtpd(8) ; which is great new :)

The LDAP organization works but it’s not really standart. In most case, users object contains the user’s email and aliases objects points to email addresses, not UID. I’ll dig a bit later on to see if I can reproduce such configuration when using directory like MS Active Directory. Next challenge :p

I found that if ldapd(8) dies, smtpd(8) won’t reconnect automatically and has to be restarted to get back in business. Not really nice but it’s not a big deal.

To close this article, let’s have an idea regarding performance:

# smtpsend -s 10 -m 10 -t 60 -F -T -S "Test smtpsend"
Sent 9084 messages in 61 seconds
Sending rate:  8935.08 messages/minute,   148.92 messages/second
Average delivery time:     0.01 seconds/message

# smtpsend -s 10 -m 10 -b 5242880 -t 60 -F -T -S "Test smtpsend"
Sent 65 messages in 68 seconds
Sending rate:    57.35 messages/minute,     0.96 messages/second
Average delivery time:     1.05 seconds/message

Not bad at all for a (2 vCPU / 512MB) virtual machine (ESXi 5.5) running on Intel I5 2500T @2.3GHZ.