/ SmartOS

Receiving Email on SmartOS

If sending email seems difficult, receiving email must seem nightmarishly complex. One of the biggest issues for senders: differentiating your legitimate email from the sheer volume of unsolicited messages, is nothing compared to being on the business end of that deluge. In addition to providing the counterparts of nearly everything mentioned in my first guide about email, receiving email hosts need to block messages coming from known black-listed MTAs, filter for and quarantine malicious programs and code, as well as store all of the collected messages, and finally provide an interface for legitimate users to access their email, an operation which introduces a completely new set of attack surfaces.

In short, hosting your own email is something you should never ever do.

But that wouldn't make for a very interesting guide now, would it?

This guide is intended as an immediate follow on from my last article, Sending Email from SmartOS. If you have not read that article yet, I recommend doing so as it will alleviate any possibility of starting out in the wrong place. In this article we will focus on extending a send-only Postfix installation as configured in that article to handle incoming email, as well as integrating industry standard practices into a first-class solution, using nothing but open-source software.

Warning: To reiterate my previous warning, you probably shouldn't be following this guide. There are plenty of companies providing very solid private email hosting services, and in most cases, they're a much better solution than rolling your own. The only time hosting your own email makes sense today is at very large scales (1000+ email accounts or 1000GB+ email volume). Anything smaller doesn't really justify the opportunity cost of learning how all of this stuff works and monitoring it to ensure it continues working. I have literally spent weeks of my free time learning this stuff and spending a few bucks with a third party service provider could have given me the same thing. In fact, the only reason that I AM actually using this is because I invested the time to learn it. That's time I could have spent furthering my plans for world domination: Misplaced priorities.

Notice: This guide focuses on how to set up small-scale email reception. If you are doing this for real, chances are you're going to want to go large scale, making a lot of this guide irrelevant.

Terminology

While I have been trying to avoid this completely in my first article, we will need to address some basic email terminology before moving forward, specifically the roles that different agents have while handling email.

  • Message User Agent (MUA): A program that interfaces with a user to send and receive email. Historically, MUAs have been run both on workstations and servers (in the form of terminal applications and web-applications). These programs typically speak both (E)SMTP to send email, as well as POP3 or IMAP to receive email, but can also use command-line sendmail and access local mailboxes, depending on the environment in which they are running.
  • Message Submission Agent (MSA): A service that receives messages from an MUA and forwards them on to either an intermediate MTA or the terminal MTA. More of a comparison below.
  • Message Transfer Agent (MTA): A service that receives messages from MSAs or intermediate MTAs and forwards them either to additional intermediate MTAs, or MDAs. The biggest distinction between MSAs and MTAs is what port they listen on (MTA is officially tcp/25, MSA is officially tcp/587). Functionally, MTAs accept incoming messages (messages for domestic delivery) while MSAs accept outgoing messages (messages for foreign delivery).
  • Message Delivery Agent (MDA): A service that delivers emails to local recipients' mailboxes. Also known as a Local Delivery Agent (LDA), this service is a component of the Postfix suite, and is specifically referred to as local, but other MDAs can be used with Postfix if messages need to be delivered in a methodology beyond the capabilities of local.
  • Remote Mailbox Services: Remote Mailbox services allow MUAs to access messages stored in remote mailboxes. They are a necessity for MUAs that are designed to run on the user's desktop, and for any MUA that's unable to access a local mailbox. Standard protocols include IMAP and POP3.

Postfix is capable of fulfilling the roles of an MSA, MTA, and MDA all at once. If we want to provide remote mailbox services or a web-based MUA, we will need to install and configure additional software.

IMAP vs POP3

The Internet Message Access Protocol (IMAP) and Post Office Protocol version 3 (POP3) are two popular protocols for allowing MUAs to access messages stored in a remote mailbox.

IMAP is a stateful protocol, which allows for mailbox contents to be locally mirrored, which can be useful when simultaneously using multiple MUAs (ie: a smart-phone, a desktop computer, and a laptop) and wanting to ensure that all devices see all email at all time. It is also able to notify connected clients of new messages as they arrive. IMAP, unfortunately, requires a constant network connection, making it ill-suited for off-line use. This can be overcome by message synchronization features in modern MUAs, but often at a performance hit to the end user.

POP3 is a much simpler protocol, which simply transfers messages to the MUA, deleting them from the MDA mailbox after the transfer is complete by default. Unlike IMAP, POP3 sessions are opened periodically, meaning it only requires network access when transferring emails between the mailbox and the MUA. While this behavior is ideal if transferring messages to another service to view email, it's much less convenient for multiple devices than IMAP is. As well it is only able to notify users of new emails every time it polls the server.

Personally and despite its weaknesses, I always recommend the use of IMAP when dealing directly with end-user devices. POP3 is much better suited for aggregating messages into another mail service, such as Gmail, namely due to the convenience of not having to worry about your email storage being in two places. For the rest of this guide, we will be deploying both POP3 and IMAP services, as different users may have different requirements.

Which is a great segue into...

Courier vs Cyrus vs Dovecot

At the time of this writing, there are basically three open-source remote mailbox solutions out there, each of which provides both POP3 and IMAP connectivity.

Courier

Courier Mail Server (GPL license) is a software suite which provides MUA, MSA, MTA and MDA functionality, providing ESMTP, IMAP, POP3 and SMAP server implementations, as well as a webmail and mailing list implementation. While I appreciate the effort, much of the functionality has already been provided by Postfix, which as it turns out is how Joyent sees it too (as of this writing, Joyent only offers the IMAP component of the Courier suite).

Cyrus

Cyrus IMAP (BSD license) is an email, contacts and calendar server. It offers IMAP, POP3 and KPOP connectivity in a secure, scalable deployment. While this actually seems quite reasonable to me (the feature creep of CalDAV and CardDAV isn't too bad), I've actually already used previous versions of this software, and would actually like to learn something else for this guide (maybe I'll compare all three sometime.)

Dovecot

Dovecot (MIT license) is an open source IMAP and POP3 email server, written with security primarily in mind. It's fast, simple to set up, requires no special administration and uses very little memory, making it an excellent choice for any sized installation.

Which is why we'll be using it.

MDA vs LDA vs LMTP

With our choices so far, local email could be delivered through a variety of methods. While we could configure Postfix to deliver the email directly (through either the local or virtual interface), a Postfix managed delivery precludes storing our email in the higher performance Dovecot mailbox formats of single-dbox or multi-dbox, making either LDA or LMTP a preferred method for local delivery.

The Dovecot LDA process is run by Postfix each time it needs to deliver email. In contrast, the Dovecot LMTP (Local Message Transfer Protocol, a variant of ESMTP, or Extended Simple Message Transfer Protocol) service is a long-running process that listens on a named UNIX socket for incoming messages. According to the official Dovecot LDA documentation, LMTP is the preferred method for local delivery as it is "somewhat easier to configure and gives better performance."

In short: MDA < LDA < LMTP.

Testing

While you could configure a Gmail or another webmail account to access your email service for testing, I recommend using those accounts as external addresses and instead set up a test environment that utilizes a traditional end-user email client (such as Thunderbird). It may be wise to additionally test with any specific email software you were intending on using as well.

Finally, I recommend using a service such as MXToolbox to do a thorough check of your configuration after you've finished with this guide. Their reports can help you to identify configuration and security issues before someone or something else identifies them for you.

SmartOS Zone Configuration

To briefly refresh, our zone configuration needs enough bandwidth to handle incoming and outgoing email, enough CPU cycles and memory to run any additional services and scheduled tasks we may have in the zone, and enough storage space to store the volume of messages we're going to ask it manage.

ZFS Datasets

This article assumes that you have followed the previous guide, and have a zones/.../data/mail dataset containing all mail related data.

We're going to add another child dataset for the local delivery spool:

# UUID=`sysinfo | json UUID`
# zfs create -o quota=470G zones/$UUID/data/mail/virtual

This is the final ZFS Dataset we're planning on adding, making this a perfect time to review relevant datasets and their quotas:

# zfs list -o name,quota,mountpoint
NAME                         QUOTA  MOUNTPOINT
...
zones/.../data/mail           980G  /var/mail
zones/.../data/mail/archive   500G  /var/mail/archive
zones/.../data/mail/postfix    10G  /var/spool/postfix
zones/.../data/mail/virtual   470G  /var/mail/virtual
...

As I've mentioned in the previous article, these numbers are just course recommendations

If you decide to over-provision your datasets, it's also wise to set a reservation on the Postfix spool dataset, as this will prevent encroachment from the other datasets:

# zfs set reservation=10G zones/.../data/mail/postfix

User Accounts

The Dovecot installation process will create the requisite dovecot and dovenull users, however, it doesn't create any additional system users for actually holding incoming email. The current recommendation is to either use a separate user for each mailbox or a dedicated virtual user for mailboxes. It has been heavily recommended not to use the dovecot or dovenull users for the purpose of holding incoming email messages.

We will set up and use a single dedicated virtual user, as well as grant write permission to the /var/mail/virtual path to that user/group pair.

# groupadd -g 962 vmail
# useradd -u 962 -g 962 -c "Virtual Email User" -d /nonexistent -s /usr/bin/false vmail
# chown vmail:vmail /var/mail/virtual

TLS Certificates

Because we're still using common passwords, allowing unencrypted communication between our MUAs and MSA/IMAP/POP3 services would be reckless, therefore we will only be allowing plain-text authentication over encrypted protocols. This fits in well with our already established policy of transmitting authenticated emails over opportunistically encrypted channels.

To facilitate this we should have a minimum of two TLS key-pairs: one for Postfix to do authenticate to MUAs via submission, and to MTAs via opportunistic DANE and one for Dovecot to authenticate itself to clients via IMAPS/POP3S. While we could just as well use a single certificate for this purpose, using two different certificates gives us a little bit more protection if one of our certificates is compromised.

First, we're going to make our secure private directory for Dovecot. Since the base configuration directory doesn't exist yet, we'll have to make that too.

# DIR=/opt/local/etc/dovecot/private
# mkdir -p $DIR
# cd $DIR
# chmod 700 ./

Generate a 2048-bit self-signed RSA certificate for Dovecot:

# openssl req -x509 -nodes -newkey rsa:2048 -days 365 -keyout key.pem -out cert.pem \
-subj "/C=US/O=Example Networks/CN=mail.example.net"

And we might as well secure the files too, just to be extra sure.

# chmod 600 *.pem

Note: The Dovecot certificate should ideally name the hostname used by clients to access the IMAP/POP3 services, which in our case is 'mail.example.net'.

Since the keys are read before the Dovecot processes drop their root permissions, the key files can have all non-root access revoked.

Next, we're going to generate a self-signed RSA key-pair, a self-signed ECDSA key-pair, and generate some DH parameters for Postfix. Before we get started though, we're going to need to know which curve type to use.

# postconf tls_eecdh_strong_curve
tls_eecdh_strong_curve = prime256v1

This is the curve name we should use when generating our ECDSA keys. Let's make our secure private directory for Postfix.

# DIR=/opt/local/etc/postfix/private
# mkdir $DIR
# cd $DIR
# chmod 700 ./

Generate our RSA and ECC key-pairs.

# openssl genrsa -out rsa-key.pem 2048
# openssl ecparam -name prime256v1 -genkey -out ecc-key.pem

While the common name can refer to the MTA hostname, we can also specify an alternative subject name for MSA connections. Unfortunately, OpenSSL doesn't deal with Subject Alternative Names well on the command-line (understatement of the year), so we'll use sub-shells to create a temporary configuration file that describes the extensions we need to create our Certificate Signing Request. Apologies for the mess.

Generating self-signed RSA and ECC certificates.

# openssl req -new -x509 -days 365 -key rsa-key.pem -out rsa-cert.pem \
-subj "/C=US/O=Example Networks/CN=mx.example.net" -reqexts tmp -config \
<(cat /opt/local/etc/openssl/openssl.cnf \
<(printf "[tmp]\nsubjectAltName=DNS:mx.example.net,DNS:mail.example.net"))
# openssl req -new -x509 -days 365 -key ecc-key.pem -out ecc-cert.pem \
-subj "/C=US/O=Example Networks/CN=mx.example.net" -reqexts tmp -config \
<(cat /opt/local/etc/openssl/openssl.cnf \
<(printf "[tmp]\nsubjectAltName=DNS:mx.example.net,DNS:mail.example.net"))

Alternatively, you can generate CSRs to pass off to a Certificate Authority.

# openssl req -new -key rsa-key.pem -out rsa-csr.pem \
-subj "/C=US/O=Example Networks/CN=mx.example.net" -reqexts tmp -config \
<(cat /opt/local/etc/openssl/openssl.cnf \
<(printf "[tmp]\nsubjectAltName=DNS:mx.example.net,DNS:mail.example.net"))
# openssl req -new -key ecc-key.pem -out ecc-csr.pem \
-subj "/C=US/O=Example Networks/CN=mx.example.net" -reqexts tmp -config \
<(cat /opt/local/etc/openssl/openssl.cnf \
<(printf "[tmp]\nsubjectAltName=DNS:mx.example.net,DNS:mail.example.net"))

Generate our DH parameters and lock all files down.

# openssl dhparam -out dh.pem 2048
# chmod 600 *.pem

Note: Ideally, DH parameters and TLS keys should be of the same size. Because generating 4096-bit DH parameters takes so long, and 2048-bit RSA keys should be secure for a while still, we'll opt for using 2048-bit RSA keys and 2048-bit DH parameters.

As with Dovecot, Postfix reads the certificate files into memory before dropping root permissions (or entering its chroot jail).

Since we will be using DANE to opportunistically secure communication between MTAs, we will be able to get away with using either self-signed or certificates signed by our own Certificate Authority, as covered by my previous article on that topic.

This can be done at almost any time after this guide, and I will leave any further handling of certificates as an exercise for the reader.

Configuring DNS

There's a lot of interplay between Email and DNS. Mail eXchanger (MX) records can be designated within DNS, allowing any domain name to delegate it's email off to a specific host, be it in the same domain or not. The most popular method of authenticating public keys, DNS-based Authentication of Named Entities (DANE) makes use of DNSSEC to authenticate TLS certificates without a Certificate Authority. It's also a good idea to set up some records for clients to access your email server as well.

You may or may not want to set up your DNS records before you begin. If you're deploying a new email service on a fresh domain, I recommend doing it as early as possible, as it won't generate any disruption, and will save you time and effort in identifying reconfiguration when you do get DNS setup. On the other hand, if you're working with an already established domain, the potential disruption to service would require that you get a fully functional mail service up before you change anything in DNS.

It's your discretion.

Mail Exchanger

While some Internet hosts still handle their own email, the vast majority of them transfer that responsibility off to dedicated hosts (like the one we're about to build). This transfer of responsibility is facilitated by DNS and is specifically implemented by the Mail eXchanger record or MX record.

MX records can be set for any domain in DNS, but are most often used on user sub-domains of the top-level domains. Additionally, multiple MX records can be set for a single domain, allowing for robust failover or load-balancing, depending on the preference values of the records.

For our simple case, we're going to need a single MX record for our domain that points to our designated primary mail exchanger with a preference value of one. If you have the resources or an agreement with an entity that you trust with your emails, you can also specify a backup or secondary mail exchanger by creating an additional MX record that refers to that host and has a larger preference value.

Smaller preference numbers are attempted delivery to first.

Since MX records refer to domains and not addresses, you will need to specify a recipient domain as well. In our case, we will use a sub-domain named 'mx' under our primary domain, but any other naming scheme should work just as well.

Note: These names should match the name of the mail server as previously configured.

When you're done, your domain should look something like this:

# dig mx example.net
...
;; QUESTION SECTION:
;example.net.                        IN      MX

;; ANSWER SECTION:
example.net.         86400   IN      MX      1 mx.example.net.
example.net.         86400   IN      MX      5 mx.notexample.net.
...

If you only set up a single mail exchanger, you'll only have one record.

DANE

DNS-based Authentication of Named Entities, or DANE for short, is a DNS based protocol that allows for the binding of X.509 certificates to domain names, as well as the authentication of said X.509 certificates via the DNSSEC trust chain.

We will be using DANE today to authenticate the public certificate we're using for our mail exchanger, allowing clients to trust that they are connecting to whom they believe they are.

There are some fantastic websites out there that will generate TLSA records for you, but in the spirit of this entire guide, why ask someone else to do something for you that you're fully capable of doing yourself?

First, lets use OpenSSL to extract the public key data from our certificate, convert it to DER format and digest it in SHA256 (with hex output):

# openssl x509 -in /opt/local/etc/postfix/private/rsa-cert.pem -noout \
-pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256 -hex
(stdin)= e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Do the same with your ECC certificate as well (if you're using both).

# openssl x509 -in /opt/local/etc/postfix/private/ecc-cert.pem -noout \
-pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256 -hex
(stdin)= 91b7852b855e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca4959

We will then use this as the payload of a TLSA record for _25._tcp under our mail exchanger's domain name. You will want to specify that the usage is of type 3 (Domain Issued Certificate), the selector is of type 1 (Subject Public Key) and the Matching Type is of type 1 (SHA-256 Hash). Add the record and confirm that it's correct with dig:

# dig tlsa _25._tcp.mx.example.net
...
;; QUESTION SECTION:
;_25._tcp.mx.example.net.  IN      TLSA

;; ANSWER SECTION:
_25._tcp.mx.example.net. 599 IN    TLSA    3 1 1 E3B0C44298fC1C149AFBF4C8996FB92427AE41E4649b934CA495991b 7852B855
_25._tcp.mx.example.net. 599 IN    TLSA    3 1 1 
91b7852b855e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b9 34ca4959
...

You should be able to confirm it on incoming connections as well if you also enable the smtpd_tls_recieved_header parameter in the Postfix TLS instruction sub-section in this guide.

User Access

While we could simply configure our users' MUAs to access our email host using the previously established MX record host, that's quite inflexible in terms of expansion: specifically, if our expansion strategy might involve separating Email Exchangers and client-facing services onto different hosts.

A better strategy would be to set some CNAME records that refer to the same host that our MX record refers to. If we need to expand down the road, we can adjust that CNAME to point to a new host without having to reconfigure hundreds or potentially even thousands of end-user MUAs.

When you're done, you should have a CNAME record that looks like this.

# dig mail.example.net
...
;; QUESTION SECTION:
;mail.example.net.         IN      A

;; ANSWER SECTION:
mail.example.net.       86400   IN      CNAME   mx.example.net.
mx.example.net.         86400   IN      A       1.2.3.4
...

If you need additional flexibility, set up two independent CNAME records: one for incoming and one for outgoing email.

Configuring Dovecot

While this may seem like it's being done backward, Postfix requires a functioning SASL framework before it can even begin authentication. Additionally, since we're going to be using LMTP instead of LDA or Postfix MDA, those services act as prerequisites to configuring Postfix.

This also lets us later segue easily into the more complicated bits of processing incoming email which will be handled between Postfix and other subsystems.

First, install the dovecot package.

# pkgin in dovecot

By default, Dovecot configuration is split up into multiple files using a conf.d layout located under the /opt/local/etc/dovecot/conf.d directory. While I appreciate the gesture, I much prefer concise, singular configuration files. So we'll be replacing the existing configuration directory tree with something much simpler.

Fortunately, Dovecot ships with the doveconf command (similar to postconf) which allows us to easily generate a terse version of the configuration file (doveconf -n) which will only contain directives which deviate from the default values.

Our goals for this configuration are as follows:

  • Provide a UNIX Domain Socket for Postfix to interface with Dovecot for SASL and LMTP services. These sockets should be provided under /var/spool/postfix/private and be named dovecot-sasl and dovecot-lmtp respectively.
  • Establish Multi-dbox email spools for each user under /var/mail/virtual/%d/%n/mail.
  • Use a passwd like file for authentication at /opt/local/etc/dovecot/private/passwd.
  • Allow both plain and login authentication mechanisms.
  • Set the auth-worker service user to be dovecot instead of root.
  • Set the lmtp service user to be vmail instead of root (this works because all email is delivered as vmail).
  • Only provide encrypted services to clients (disallow imap/pop3 in favor of imaps/pop3s). Keys will be located under /opt/local/etc/dovecot/private/.
  • Ensure that we're not using known insecure versions of SSL/TLS and that we're not using insecure ciphers.

Note: While it would be nice if we could also specify a file containing our DH parameters, the current version of dovecot (2.2.x) does not support that. The next version (2.3.x) does.

This is what our minimal configuration file looks like.

/opt/local/etc/dovecot/dovecot.conf

service auth-worker {
  user = $default_internal_user
}

service auth {
  unix_listener /var/spool/postfix/private/dovecot-auth {
    group = postfix
    user = postfix
    mode = 0660
  }
  user = $default_internal_user
}

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    group = postfix
    user = postfix
    mode = 0660
  }
  user = vmail
}

passdb {
  args = /opt/local/etc/dovecot/private/passwd
  driver = passwd-file
}

userdb {
  driver = passwd-file
  args = /opt/local/etc/dovecot/private/passwd
  override_fields = uid=vmail gid=vmail home=/var/mail/virtual/%d/%n mail=mdbox:~/mail
}

log_path = /var/log/dovecot.log
auth_mechanisms = plain login
auth_default_realm = unknown
mail_location = mdbox:~/mail:UTF-8
pop3_uidl_format = %m

ssl = required
ssl_cert = </opt/local/etc/dovecot/private/cert.pem
ssl_dh_parameters_length = 2048
ssl_key = </opt/local/etc/dovecot/private/key.pem
ssl_cipher_list = ALL:!LOW:!EXP:!aNULL
ssl_protocols = !SSLv3 !SSLv2

Once you've set this configuration file, you can enable Dovecot and verify that it's running.

# svcadm enable dovecot
# svcs dovecot
STATE          STIME    FMRI
online          4:58:03 svc:/pkgsrc/dovecot:default

Setting User Passwords

Finally, a password file will need to be populated at /opt/local/etc/dovecot/private/passwd containing a colon-separated list of usernames & passwords (and everything else you'd normally find in a /etc/passwd file, except without shadow separation). Since we're using plaintext logins, and SmartOS, we are able to store passwords as Blowfish password hashes, which is what we're going to do next.

Note: Password hashes such as Blowfish support "rounds", or multiple iterations of hashing the output of the hash function. This improves password cracking resistance at the cost of performance by increasing the computational cost of calculating the password hash output. By default, Blowfish performs five rounds of blowfish when used with Dovecot, but can be tuned up to a maximum of 31. The number of rounds has an exponential effect on the difficulty of calculating the password. While it may be tempting to turn this number up as high as possible, consider that a higher round count not only will slow down the login process but also present a possible surface for a denial of service attack.

A quick way to approximate the appropriate number of rounds for your system is to measure how long it takes to calculate a Blowfish password hash using time and doveadm:

# time doveadm pw -p insecure -s BLF-CRYPT -r 8
{BLF-CRYPT}$2a$08$6U9XVhAdBjylRGg96P9i.esHQYhrGHHv.OpCpqqAPYtAYFOePAe9O

real    0m0.057s
user    0m0.047s
sys     0m0.009s

I'm very comfortable with a round count of eight, as it's eight times more difficult than the default of five (2^(8-5) = 8) and still under 1/20 of a second. This is acceptable to me in my case as I never expect to be fulfilling anywhere close to 20 legitimate login requests per second with this hardware. Once you've decided on a reasonable round count, you can hash a password for real this time.

# echo "user@example.net:`doveadm pw -s BLF-CRYPT -r 8`::::::" >> /opt/local/etc/dovecot/private/passwd
Enter new password:
Retype new password:

Note: The six colons after the password are required for the userdb side of Dovecot to make sense of this password file.

Editing a password can be done by overwriting the contents of a user's password with the output from doveadm.

# doveadm pw -s BLF-CRYPT -r 8
Enter new password:
Retype new password:
{BLF-CRYPT}$2a$08$6U9XVhAdBjylRGg96P9i.esHQYhrGHHv.OpCpqqAPYtAYFOePAe9O

(Optional) Configuring Quotas

Dovecot comes with a robust collection of plug-ins to enable additional functionality. One major function provided by a Dovecot plug-in, email quotas, keep users from exceeding a set volume of email. We'll walk through configuring that now.

Our requirements:

  • Enable Quota service using the count method (high-performance method that's compatible with multi-dbox)
  • Set the default quota rule to 1GB of space used in storage. This can be increased globally or on a per-user basis.
  • Enable the IMAP Quota plugin for the IMAP protocol. This allows quota information to be passed to an IMAP client.
  • Provide a quota status service to Postfix (via UNIX socket) so that quota information can be communicated from Dovecot to Postfix.

Add the following to Dovecot's configuration file:

/opt/local/etc/dovecot/dovecot.conf

service quota-status {
  executable = quota-status -p postfix
  unix_listener /var/spool/postfix/private/dovecot-quota {
    mode = 0660
    user = postfix
    group = postfix
  }
}

mail_plugins = quota
protocol imap {
  mail_plugins = $mail_plugins imap_quota
}

mailbox_list_index = yes
plugin {
  quota = count:User Quota
  quota_rule = *:storage=1G
  quota_vsizes = yes

  quota_grace = 10%%
  quota_status_success = DUNNO
  quota_status_nouser = DUNNO
  quota_status_overquota = "522 5.2.2 Mailbox is full"
}

Note: The first configuration block in the above snippet labeled service quota-status is only required if you want Postfix to check the quota status of the destination mailbox before relaying the message on to dovecot via LMTP. It requires additional configuration in Postfix, namely appending a check_policy_service value into the Postfix smtpd_recipient_restrictions parameter. We will cover that in the next section.

Restart Dovecot to enable your changes.

# svcadm restart dovecot

While this shouldn't be necessary, you can use Doveadm to recalculate the quotas for either a given user or all of your users.

# doveadm quota recalc -u user@example.net
# doveadm quota recalc -A

If you would like additional information on configuring Dovecot quotas, I recommend reading their fantastic documentation on the topic.

Configuring Postfix

We're going to be making quite a few changes to the general behavior of Postfix. These changes will be split up into sub-sections briefly describing the impact of each change, as well as instructions on how to implement the change. Some changes will require a restart of Postfix rather than just a re-reading of the configuration file. Each section will make reference to the proper command to use. Alternatively, you could just wait until the end of all sub-sections and issue the following command:

# svcadm restart postfix

To save us from unnecessary refreshing and reloading.

Enabling Virtual Email

While Postfix has several available methods for receiving incoming email, we would like to collect email for a variable number of email addresses attached to a variable number of domains, all of which are completely isolated from any system user accounts and from each other. The Postfix virtual domain delivery method is going to yield the best possible results for us here.

We need to configure Postfix to transport email for virtual destinations via LMTP, specifically routed through the dovecot-lmtp UNIX Domain Socket we configured in the Dovecot section above. Set the following Postfix directives:

# postconf -e virtual_transport=lmtp:unix:private/dovecot-lmtp \
virtual_mailbox_domains=/opt/local/etc/postfix/virtual_mailbox_domains \
virtual_mailbox_maps=hash:/opt/local/etc/postfix/virtual_mailbox_maps \
virtual_alias_maps=hash:/opt/local/etc/postfix/virtual_alias_maps

Now is a good time to ensure that these lookup files are populated:

/opt/local/etc/postfix/virtual_mailbox_domains

example.net
another-example.com

The virtual_mailbox_domains lookup provides a list of valid recipient domains to Postfix. These domains should be documented nowhere else (not in mydestination, virtual_alias_domains, or relay_domains). Since this is just a list, no additional handling is required.

/opt/local/etc/postfix/virtual_mailbox_maps

brian@example.net true
notbrian@example.net true

Since Postfix is expecting a hash under virtual_mailbox_maps, we need to provide a key-value pair, despite the fact that the email will be routed over LMTP for local delivery by Dovecot instead of local delivery by Postfix. Anything works here as a value, I just used "true" because why not?

/opt/local/etc/postfix/virtual_alias_maps

info@example.net         brian@example.net,notbrian@example.net
info@another-example.com notbrian@example.net

abuse@example.net        brian@example.net
webmaster@example.net    brian@example.net
postmaster@example.net   brian@example.net

*@example.net            notbrian@example.net

While alias maps can be handy for redirecting email for business purposes, there are several standard email addresses which should be specified here as well, such as abuse, webmaster, and postmaster. I recommend reading RFC 2142 to get a fuller sense for any additional email aliases you may want to specify. We can also specify catch-all aliases here, redirecting email from any otherwise undeclared username on the domain to one or more recipients.

Next, we can generate our hash databases from the above files that need them and restart Postfix to enact your changes.

# postmap /opt/local/etc/postfix/virtual_mailbox_maps
# postmap /opt/local/etc/postfix/virtual_alias_maps
# svcadm restart postfix

At this stage, Postfix should accept inbound email and relay it to Dovecot via LMTP. Dovecot should accept inbound email from Postfix and store it locally, and you should be able to access that email using an MUA configured to connect via IMAP or POP3 to your server's IP address. Now would be a good time to test (via socat) and verify this before continuing on:

# socat - TCP:localhost:smtp
220 mx.example.net ESMTP
EHLO mx.example.net
250-mx.example.net
...
MAIL FROM: <root@example.net>
250 2.1.0 Ok
RCPT TO: <brian@example.net>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: Test Message

This is a manually submitted test message.
.
250 2.0.0 Ok: queued as 305B030F
QUIT
221 2.0.0 Bye

If you'd like to do any additional reading on Postfix Virtual Domain Hosting, I recommend the official Postfix guide on the topic.

Enabling TLS

While encryption is in no way a hard technical requirement to get Postfix operational, it is most certainly a policy requirement, and I prefer to address it sooner rather than later.

Why re-configure Postfix to be secure when we can just configure Postfix to be secure the first time?

We should tell Postfix about our keys, certificates and DH parameters we generated in the TLS section above. Set the following parameters:

# postconf -e smtpd_tls_key_file=/opt/local/etc/postfix/private/rsa-key.pem \
smtpd_tls_cert_file=/opt/local/etc/postfix/private/rsa-cert.pem \
smtpd_tls_eccert_file=/opt/local/etc/postfix/private/ecc-cert.pem \
smtpd_tls_eckey_file=/opt/local/etc/postfix/private/ecc-key.pem \
smtpd_tls_dh1024_param_file=/opt/local/etc/postfix/private/dh.pem

Next, we must set the default TLS policy for incoming SMTP connections. Since it breaks RFC to force TLS over publicly accessible email servers, the most aggressive we should set this parameter to is may:

# postconf -e smtpd_tls_security_level=may

We will additionally want Postfix to not to use insecure ciphers or versions of SSL/TLS. While this may seem pointless with our above setting of optionally encrypting communication, consider that our submission port will enforce the use of TLS and that changes in this file will affect behavior on that service.

Set the following parameters:

# postconf -e smtpd_tls_mandatory_ciphers=high \
smtpd_tls_mandatory_exclude_ciphers=aNULL,MD5 \
smtpd_tls_mandatory_protocols=\!SSLv2,\!SSLv3

If you would like to see what protocol version and cipher your incoming connections are using, set the following. This is especially useful if you set up a DANE record for your host and need to verify if encryption is taking place.

# postconf -e smtpd_tls_received_header=yes

Once you're done, you may restart postfix.

# svcadm restart postfix

At this point, Postfix should now announce STARTTLS under its list of extended capabilities, as well as offer to start TLS when requested to:

# socat - TCP:localhost:smtp
220 mx.example.net ESMTP
EHLO mx.example.net
250-mx.example.net
...
250-STARTTLS
...
STARTTLS
220 2.0.0 Ready to start TLS
...
QUIT
221 2.0.0 Bye

If you'd like to do any additional reading on Postfix TLS, I recommend the official Postfix guide on the topic.

Enabling SASL & Submission

While Postfix configuration is robust enough to allow us to require an encrypted channel to do authentication over a channel which is only optionally encrypted (think about that for a moment), it is much cleaner to use two different ports with clearly defined roles: The standard SMTP port (tcp/25) for accepting inbound email from other MTAs, and the standard mail submission port (tcp/587) for accepting outbound email from authenticated MUAs.

Lets start by setting some default behaviors for Postfix. Set the following parameters:

# postconf -e smtpd_sasl_type=dovecot \
smtpd_sasl_path=private/dovecot-auth \
smtpd_sasl_tls_security_options=noanonymous \
smtpd_tls_auth_only=yes

This will tell Postfix that it's going to be interfacing with Dovecot, and also how to interface with Dovecot (that fancy UNIX domain socket we specified back in the Dovecot configuration). We also specify that any non-anonymous authentication is acceptable and that all authentication must take place over an encrypted channel. This has the effect of enabling plain-text authentication over an encrypted channel, which is exactly what we want.

We don't need to worry about Postfix attempting to fulfill SASL authentication requests on tcp/25 since we didn't globally enable SASL. Let's turn on the submission port with required encryption and SASL. Edit the Postfix master process file and uncomment the following sections:

/opt/local/etc/postfix/master.cf

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes

Once you're done, you may restart postfix.

# svcadm restart postfix

At this point, Postfix should only announce AUTH under its submission port, and only if TLS has already been negotiated. Let's verify that this is the case:

Unencrypted SMTP

# socat - TCP:localhost:smtp
220 mx.example.net ESMTP
EHLO mx.example.net
250-mx.example.net
250-PIPELINING
250-SIZE 51200000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN

Unencrypted Submission

# socat - TCP:localhost:submission
220 mx.example.net ESMTP
EHLO mx.example.net
250-mx.example.net
250-PIPELINING
250-SIZE 51200000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN

Encrypted SMTP

# openssl s_client -connect localhost:smtp -starttls smtp
...
250 DSN
EHLO mx.example.net
250-mx.example.net
250-PIPELINING
250-SIZE 51200000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN

Encrypted Submission

# openssl s_client -connect localhost:submission -starttls smtp
250 DSN
EHLO mx.example.net
250-mx.example.net
250-PIPELINING
250-SIZE 51200000
250-VRFY
250-ETRN
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN

Note how AUTH was only announced on the submission port and only after TLS negotiation had taken place. This is exactly what we want.

At this stage, Postfix has been configured to authenticate incoming connections on the encrypted submission port, and should technically be fully capable of sending outbound messages and receiving inbound messages with an MUA.

If you'd like to do any additional reading on Postfix SASL, I recommend the official Postfix guide on the topic. Some of the optional sections below also draw from this guide.

Notice: Since you have a fully functional installation at this point, everything after this sub-section is technically optional, but I highly recommend at least reviewing all of the following sub-sections, as they really aren't that difficult to understand, and you might find something worth implementing.

Sender Address Authorization

By default, an SMTP client can specify any sender address in the MAIL FROM command. This is because, with unauthenticated connections, Postfix has the remote SMTP client's hostname and IP address, but not any user credentials. This changes as soon as SASL is introduced, as now Postfix also has an authenticated username.

By virtue of a lookup table, Postfix can now determine if a connection is authorized to send mail from a particular sender address. This prevents authenticated users from using sender addresses that they are not authorized for.

Set the following parameters:

# postconf -e smtpd_sender_login_maps=hash:/opt/local/etc/postfix/sender_login_maps

Additionally, insert reject_sender_login_mismatch to the smtpd_recipient_restrictions parameter before the permit_sasl_authenticated restriction. While it is possible to manipulate a large multi-line parameter using postconf, I prefer to do it directly in the config file for improved readability and to ensure that the entry is done in the proper order.

/opt/local/etc/postfix/main.cf

...
smtpd_recipient_restrictions =
  permit_mynetworks,
  reject_sender_login_mismatch,       # <-- This line is new and
  permit_sasl_authenticated,          # <-- is before this line
  reject_unlisted_recipient,
  reject_non_fqdn_recipient,
  reject_unauth_destination,
  reject_unknown_recipient_domain,
  check_recipient_access hash:/opt/local/etc/postfix/filtered_domains
...

Next, we will need to populate our controlled envelope sender table. This hash is expecting sender addresses as keys and names of SASL users (optionally a comma-separated list of SASL usernames) as values.

/opt/local/etc/postfix/sender_login_maps

brian@example.net     brian@example.net
notbrian@example.net  notbrian@example.net
info@example.net      brian@example.net, notbrian@example.net

Notice: Each SASL user doesn't automatically have permission to send through the sending address that matches its username. Due to the naming strategy we've adopted, it's easy to confuse the keys and values of this hash table as the same things, and it's important to remember that they are not.

Next, you can generate your hash databases from the above file and restart Postfix to enact your changes.

# postmap /opt/local/etc/postfix/sender_login_maps
# svcadm restart postfix

I found testing this from within Thunderbird by changing the account name easier than playing around with base64 encoding my username and password.

SASL Access Control

Postfix can also implement reception policies that are as fine-grained as the SASL username. Typically this is used to hold or reject email that has been sent from compromised accounts. While this step may seem like a waste of time, it will be nice for later on if we decide to integrate your Dovecot and Postfix databases under PostgreSQL or OpenLDAP. If you don't plan on doing that, you may just prefer to skip this step.

To do so, add the check_sasl_access restriction to smtpd_recipient_restrictions as depicted:

/opt/local/etc/postfix/main.cf

...
smtpd_recipient_restrictions =
  permit_mynetworks,
  check_sasl_access hash:/opt/local/etc/postfix/sasl_access,
  permit_sasl_authenticated, # <-- Note: it's before this line
  reject_unlisted_recipient,
  reject_non_fqdn_recipient,
  reject_unauth_destination,
  reject_unknown_recipient_domain,
  check_recipient_access hash:/opt/local/etc/postfix/filtered_domains
...

Next, populate the sasl_access hash table:

/opt/local/etc/postfix/sasl_access

brian@example.net    HOLD
notbrian@example.net REJECT

A value of HOLD will accept messages into the Postfix queue, but never pass them along. A value of REJECT will simply refuse to accept messages.

Generate our hash databases from the above file and restart Postfix to enact your changes.

# postmap /opt/local/etc/postfix/sasl_access
# svcadm restart postfix

Relaying Incoming Messages

While not always practical, it's a good idea to have at least one backup mail exchanger in place to receive messages if your primary mail exchanger goes off-line. Often times, this can be implemented by clustering two or more independent mail exchangers together so that for each primary mail exchanger in the set, all other mail exchangers will act as a backup.

Add the following parameters to your configuration:

# postconf -e relay_domains=/opt/local/etc/postfix/relay_domains

We can now list domains we wish to relay messages for under that file.

/opt/local/etc/postfix/relay_domains

notexample.net
example.com
example.org

Note that we should never put our primary destination domains in this file.

Note: While Postfix can be configured to relay only messages to certain addresses (with the relay_recipient_maps parameter), this practice tends to become unmanageable after a certain scale, and has fallen out of common use.

Restart Postfix to enact your changes.

# svcadm restart postfix

Scrubbing Sensitive Information

If you've ever read through the SMTP envelope headers of an email, you know just how much sensitive information about the originating MUA is present. If you want a submission service that scrubs all identifiable information from incoming messages before relaying them along, then this sub-section is for you.

Since we don't want to scrub any information from domain-inbound emails, we'll only be working on the submission service. And since the configuration directive applies to the cleanup service (and not submission) we will need to define a new service (within master.cf) called cleanup-submission and modify the configuration of submission use it (also within master.cf).

/opt/local/etc/postfix/master.cf

Our new submission cleanup service configuration:

cleanup-submission unix n -    n       -       0       cleanup
  -o syslog_name=postfix/cleanup-submission
  -o header_checks=regexp:/opt/local/etc/postfix/submission_header_checks

And our reference to our submission cleanup service in submission

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o cleanup_service_name=cleanup-submission  # Add this part to submission
...

Note: I am aware that the regexp is lower performance than the pcre table type, but unfortunately, the SmartOS distributed Postfix doesn't support pcre at this time. If you're using a different operating system, you can check to see what table types are supported by using postconf. Note in the below example the lack of pcre and the existence of regexp:

# postconf -m
...
nis
pipemap
proxy
randmap
regexp
socketmap
...

Next, we should describe the headers we want to ignore using regular expressions. Normally this would be constrained to headers which may contain MUA IP addresses or system configuration data, but you're free to expand this list if you like.

/opt/local/etc/postfix/submission_header_checks

/^Received:.*with ESMTPSA/ IGNORE
/^User-Agent:/             IGNORE
/^X-Mailer:/               IGNORE
/^X-Originating-IP:/       IGNORE

The IGNORE directive will completely remove the header from the message.

Generate our hash databases from the above file and restart Postfix to enact your changes.

# postmap /opt/local/etc/postfix/submission_header_checks
# svcadm restart postfix

Quota Policy

If you configured Dovecot to enforce a user quota and also provided the quota-status service for Postfix, you will need to configure Postfix to listen to and make decisions based on that service.

To do so, add the check_policy_service restriction to smtpd_recipient_restrictions as depicted:

/opt/local/etc/postfix/main.cf

...
smtpd_recipient_restrictions =
  permit_mynetworks,
  permit_sasl_authenticated,
  reject_unlisted_recipient,
  reject_non_fqdn_recipient,
  reject_unauth_destination,
  reject_unknown_recipient_domain,
  check_recipient_access hash:/opt/local/etc/postfix/filtered_domains,
  check_policy_service unix:private/dovecot-quota   # This is the new line this time!
...

Note: check_policy_service should always be specified AFTER reject_unauth_destination, otherwise you risk turning your installation into an open relay.

Restart Postfix to enact your changes.

# svcadm restart postfix

There will be several more sub-sections in this guide that make use of policy delegation. For more information on Postfix policy delegation, I recommend reading the official documentation on the topic. While it reads more like it's directed towards developers rather than system administrators,

Verifying DKIM Signatures

OpenDKIM is configured by default to verify signatures on incoming messages that have them, so if you followed my guide on sending email, chances are that no additional configuration is required.

Verifying SPF Compliance

As you may remember from the last guide, Sender Policy Framework is a DNS framework which allows domain administrators to specify where legitimate emails should originate from. This sub-section will focus on configuring Postfix to verify that every inbound connection complies with the SPF records for the domains which they're sending email from.

Notice: This sub-section has been completely rewritten to make use of the 2.0 version of python policyd-spf.

Since SPF came about after Postfix's move of delegating policy decisions to external services, we will need to install an SPF policy daemon. Fortunately, there's one written in Python that's exactly what we're looking for. Unfortunately, it's not currently available via pkgsrc so we'll have to manually install it and it's dependencies. Despite their best efforts (due to the horrible naming of some python libraries), Joyent has the wrong dependencies for the python spf package (dnspython is not a dependency of Python-SPF, PyDNS on the other hand, is). Since we're going to all of this work, we should probably be doing this with Python 3.5.

# pkgin in python35

Download all of the latest libraries from their various sources:

  • py3dns: The right DNS library for py-spf
  • pyspf: An SPF library written in Python
  • authentication-results: An Authentication-Results style header format (optional)
  • pypolicyd-spf: A Postfix policy engine for verifying SPF compliance

Notice: The below example was valid as of December 2016. It may not still be valid.

# cd
# wget https://launchpad.net/py3dns/trunk/3.1.1/+download/py3dns-3.1.1.tar.gz
# tar zxf py3dns-3.1.1.tar.gz
# cd py3dns-3.1.1
# python3.5 setup.py build
# python3.5 setup.py install
# cd
# wget https://pypi.python.org/packages/88/4d/440c273b6a136b58fad9f779847cc90179d627f8a2f2cd8b36313664cf1b/pyspf-2.0.12t.tar.gz
# tar zxf pyspf-2.0.12t.tar.gz
# cd pyspf-2.0.12
# python3.5 setup.py build
# python3.5 setup.py install
# cd
# wget https://launchpadlibrarian.net/297310837/pypolicyd-spf-2.0.1.tar.gz
# tar zxf pypolicyd-spf-2.0.1.tar.gz
# cd pypolicyd-spf-2.0.1
# python3.5 setup.py build
# python3.5 setup.py install
# cd
# rm -r py3dns-3.1.1 pyspf-2.0.12 pypolicyd-spf-2.0.1
# rm py3dns-3.1.1.tar.gz pyspf-2.0.12t.tar.gz pypolicyd-spf-2.0.1.tar.gz

And if you would like to have SPF Authentication-Results style headers instead, you will need to install the authentication-results library as well.

# cd
# wget https://launchpadlibrarian.net/199794213/authres-0.800.tar.gz
# tar zxf authres-0.800.tar.gz
# cd authres-0.800
# python3.5 setup.py build
# python3.5 setup.py install
# cd
# rm -r authres-0.800 authres-0.800.tar.gz

Review the installation output and ensure that all data ends up going into /opt/local, in my case, I needed to manually move the configuration directory.

# mv /etc/python-policyd-spf/ /opt/local/etc/

And since this is written in Python, I also went ahead and modified the default configuration location within policyd-spf to reflect SmartOS standards:

/opt/local/bin/policyd-spf

...
configFile = '/opt/local/etc/python-policyd-spf/policyd-spf.conf'
...

Editing that configuration, we may want to ensure that policy-spf only adds headers instead of outright rejects emails (since we might make use of those headers later on).

/opt/local/etc/python-policyd-spf/policyd-spf.conf

debugLevel = 1
TestOnly = 1

HELO_reject = False
Mail_From_reject = False

PermError_reject = False
TempError_Defer = False

Header_Type = AR  # This will only work if you installed authres

skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1

While we could set up an SMF manifest and let the operating system handle running this daemon, it's been designed to be managed directly by Postfix.

Add the following service description:

/opt/local/etc/postfix/master.cf

policyd-spf unix -       n       n       -       0       spawn
  user=nobody argv=/opt/local/bin/policyd-spf

And then add another check_policy_service restriction to smtpd_recipient_restrictions as depicted:

/opt/local/etc/postfix/main.cf

...
smtpd_recipient_restrictions =
  permit_mynetworks,
  permit_sasl_authenticated,
  reject_unlisted_recipient,
  reject_non_fqdn_recipient,
  reject_unauth_destination,
  reject_unknown_recipient_domain,
  check_recipient_access hash:/opt/local/etc/postfix/filtered_domains,
  check_policy_service unix:private/dovecot-quota,
  check_policy_service unix:private/policyd-spf   # I'm the latest one
...

Note: check_policy_service should always be specified AFTER reject_unauth_destination, otherwise you risk turning your installation into an open relay.

Additionally, set the timeout limit for policy-spf as 3600 seconds, and then as always, restart Postfix to enact your changes.

# postconf -e policyd-spf_time_limit=3600
# svcadm restart postfix

Virus Detection

Note: While I strongly recommend the use of virus detection on your mail server, ClamAV is incredibly taxing on system resources. I recommend increasing your memory allocation by 1GB (to a minimum of 2GB) if you intend on following this section.

While ClamAV is pretty much the de facto Open-Source virus detection software, there are many different ways to implement scanning email using ClamAV. The main difference that I found between all methods comes down to where the actual scan takes place: namely during, or after Postfix accepts email from a remote source.

After-queue scanning may initially seem more attractive (it was to me), but it quickly runs into problems. While it's true that it can perform the virus scan off-line (without delaying the incoming email) what should Postfix do if the message is infected? Quarantining an email message can be messy, and will likely run afoul of DKIM and even the law in certain jurisdictions. Returning the message to the sender can be problematic as well, as you're effectively sending them a virus, and the sender address may have been forged, which opens a whole new can of worms.

Scanning a message as it's being sent to Postfix (before-queue) is really our best option. If a message fails it's scan, we reject it immediately letting the actual real sender know (since we're still in communication with them, as they're still connected to our SMTP port). As an added bonus, we can scan our outgoing email just as easily, ensuring that our organization isn't inadvertently sending virus-laden emails to unsuspecting recipients. Indeed this appears to be how Google deals with scanning viruses as well (google additionally scans attachments a second time at the MUA level). Yes, it limits our maximum incoming message rate, but we can always compensate by scaling horizontally if need be.

Since ClamAV ships with clamav-milter, which will give us a before-queue like service, we're going to use that here. Let's start by installing ClamAV via pkgsrc and immediately running freshclam to pull down a current copy of the virus definition/signature database.

# pkgin in clamav
# freshclam

You may have noticed in the install notes that there are several SMF enabled services that came with this package:

  • clamd: Virus-scanning daemon. Listens for incoming connections on UNIX and/or TCP socket(s) and scans files or directories on demand.
  • freshclamd: Definition updating daemon. Periodically connects to clamav.net to pull new versions of virus signature databases.
  • clamav-milter: Milter-compatible email scanner. This allows Postfix and ClamAV to communicate with each other.

Since I prefer to avoid unnecessary indirection. I've modified my service manifests to call ClamAV directly instead of going through the service method scripts. Those modifications are as follows:

The freshclamd service is already perfectly configured for our needs, you can enable it immediately.

# svcadm enable clamav:freshclamd

Your clamd configuration file should be configured as follows:

/opt/local/etc/clamd.conf

PidFile /var/clamav/clamd.pid   # Can be removed if you modified your SMF manifest
LocalSocket /var/clamav/clamd.sock

All of the other defaults are fine. When you're ready, enable the clamd service.

# svcadm enable clamav:clamd

Note: due to the complexity of this service, it may take a moment to fully load.

Next, your clamav-milter configuration file should be configured as follows. As always, if you're familiar with these settings, or are feeling adventurous and don't mind reading the notes in the example configuration file, feel free to adjust and test.

/opt/local/etc/clamav-milter.conf

MilterSocket unix:/var/clamav/mail-scan
#MilterSocketGroup postfix
MilterSocketMode 0666

ClamdSocket unix:/var/clamav/clamd.sock
OnClean Accept
OnInfected Reject
OnFail Defer
RejectMsg Virus Detected!
SupportMultipleRecipients yes

Once you're happy with your configuration, enable the clamav-milter service:

# svcadm enable clamav:clamav-milter

Finally, we need to integrate this with Postfix. Note that we run this milter first, before our DKIM milter, as we want to identify and reject virus-laden email before we do any additional processing.

# postconf -e \
smtpd_milters=unix:/var/clamav/mail-scan,unix:/var/db/opendkim/opendkim.sock \
non_smtpd_milters=unix:/var/clamav/mail-scan,unix:/var/db/opendkim/opendkim.sock

Restart Postfix to enact your changes.

# svcadm restart postfix

Testing this functionality will involve ensuring that virus-free messages can pass, and that virus-laden messages are rejected.

# socat - TCP:localhost:smtp
220 mx.example.net ESMTP
EHLO mx.example.net
250-mx.example.net
250-PIPELINING
250-SIZE 51200000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
MAIL FROM: <root@example.net>
250 2.1.0 Ok
RCPT TO: <brian@example.net>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: Testing Viruses
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
.
550 5.7.1 Virus Detected!
MAIL FROM: <root@example.net>
250 2.1.0 Ok
RCPT TO: <brian@example.net>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: Virus Free
No Viruses here!
.
250 2.0.0 Ok: queued as 69ED4133
QUIT
221 2.0.0 Bye

That string we included is called an EICAR standard antivirus test file. It's a completely harmless way to verify that virus scanning software is working, and an entertaining way to scare your friends.

Be creative with it.

Handling Spam

What sort of comprehensive email guide would be complete without mentioning SpamAssassin?

And ... done.

This is as far as I'll go on the subject for right now. As I mentioned at the beginning of this post, filtering unsolicited emails is one of the most difficult things to configure in a modern email implementation, and while it's one could just slap a solution like SpamAssassin in place and call it done, proper filtering involves employing the right combination of filters in the right order and at the right stages of your email stack, and is honestly beyond the scope of a few footnotes in this guide.

If there is an interest, I'll collect my notes, do some research, and compile up another post in this email series. But for now, we're done.

Appendix: Adding additional users

As this guide has gotten quite long, this section outlines how to add additional users. We're assuming you've turned everything on.

  • Add the email address (user@domain) to Dovecot (/opt/local/etc/dovecot/private/passwd in a passwd-file like format) to allow the user to receive email.*
  • Add the email address (user@domain) to Postfix (/opt/local/etc/postfix/virtual_mailbox_maps) followed by true to allow Postfix to forward email for the email address to Dovecot.
  • Add the email address (user@domain) twice to Postfix (/opt/local/etc/postfix/sender_login_maps) to allow the user to send using their own email address.

*The following is the command-line used above to create additional users.

echo "user@example.net:`doveadm pw -s BLF-CRYPT -r 8`::::::" >> /opt/local/etc/dovecot/private/passwd

Conclusion

And there you have it. Nearly all of the features of large-scale email services all packed into a single SmartOS instance.

Besides the before mentioned spam filtering, additional ways to develop this would be to switch from flat files to either MySQL/PostgreSQL or better yet, OpenLDAP to furnish domain and user information to both Postfix and Dovecot. That would allow you to easily segue into setting up multiple SmartOS instances to provide redundancy, as well as scalability.

One of the first things that are likely to become an issue is ClamAV, which fortunately is perfectly suited to be configured as a virus scanning farm (several ClamAV instances could run across multiple pieces of hardware, all connecting back to a single, or better yet, 2-3 clamav-milter instances running on your gateways).

As I've mentioned, if there's interest, I enjoy the challenge and am more than happy to put in the research and see what I can come up with.