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:
- users are objects identified by an UID ;
- aliases are objects identified by an email address ;
- if an alias points to an external email, OpenSMTPD will run DNS queries and deliver the mail to the discovered MX ;
- if an alias points to an internal email, OpenSMTPD will loop and look for this address as another alias ;
- if an alias points to an UID reference, OpenSMTPD will look for such users in the directory and deliver the mail via LMTP.
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" \
http://www.openldap.org/devel/cvsweb.cgi/~checkout~/servers/slapd/schema/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
$2b$10$Kmj26pNwDu2Xi2K01KIhCeIGAUeXs/7AwPkyCLlSEXLVmTvfFGTSy
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
ldapd(ok)
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
$3c$88$td(...).
# 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, tumfatig.net
dn: ou=domains,dc=tumfatig,dc=net
objectClass: organizationalUnit
ou: domains
# tumfatig.net, domains, tumfatig.net
dn: dc=tumfatig.net,ou=domains,dc=tumfatig,dc=net
dc: tumfatig.net
objectClass: domain
Add as much as you like. They will be automatically checked for acceptance by smtpd(8).
A standart email alias goes:
# contact@tumfatig.net, aliases, tumfatig.net
dn: cn=contact@tumfatig.net,ou=aliases,dc=tumfatig,dc=net
rfc822MailMember: jca@tumfatig.net
cn: contact@tumfatig.net
objectClass: nisMailAlias
This means “email to contact@tumfatig.net will be delivered to email jca@tumfatig.net ”. This will also work if “rfc822MailMember” refers to external email address, like gmail or so.
A final email alias goes:
# jca@tumfatig.net, aliases, tumfatig.net
dn: cn=jca@tumfatig.net,ou=aliases,dc=tumfatig,dc=net
rfc822MailMember: jca
cn: jca@tumfatig.net
objectClass: nisMailAlias
This means “email to jca@tumfatig.net will be delivered to user jca”. AFAIK, OpenSMTPD requires a real user to deliver mails.
A multiple-recipient alias goes:
# contact, aliases, tumfatig.net
dn: cn=contact,ou=aliases,dc=tumfatig,dc=net
rfc822MailMember: jca@tumfatig.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 jca@tumfatig.net ”. This is quite useful to reference general pseudo accounts, RFC 2142 mailboxes, …
A virtual user is configured this way:
# jca, users, tumfatig.net
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 @@
unbound:\
:openfiles-cur=512:\
:tc=daemon:
+
+dovecot:\
+ :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 = 127.0.0.1
+ 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 = 127.0.0.1
+ 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/mkcert.sh 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://127.0.0.1
# 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
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 bagheera.tumfatig.net Dovecot ready.
LHLO localhost
250-bagheera.tumfatig.net
250-8BITMIME
250-ENHANCEDSTATUSCODES
250 PIPELINING
MAIL FROM:<joel.carnat@free.fr>
250 2.1.0 OK
RCPT TO:<jca>
250 2.1.5 OK
DATA
354 OK
From: joel.carnat@free.fr
To: jca@tumfatig.net
Subject: test
Delivery test
.
250 2.0.0 <jca> n5TaC1VxqVXXTgAAxLOgNw Saved
QUIT
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 www.tumfatig.net certificate "/etc/ssl/gandi.crt"
pki www.tumfatig.net key "/etc/ssl/private/gandi.key"
listen on lo0
listen on egress tls pki www.tumfatig.net auth-optional
listen on egress port submission tls-require pki www.tumfatig.net 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://127.0.0.1
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
cherie.tumfatig.net
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 cherie.tumfatig.net
# rcctl start spamd
spamd(ok)
# rcctl enable spamlogd
# rcctl start spamlogd
spamlogd(ok)
# cat > /etc/mail/nospamd
192.168.0.0/24
# 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 127.0.0.1 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 jca@tumfatig.net -T joel@carnat.net -S "Test smtpsend" 192.168.0.150
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 jca@tumfatig.net -T joel@carnat.net -S "Test smtpsend" 192.168.0.150
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.