Table of Contents
Was this article helpful?

7  out of  9 found this helpful

Try Vultr Today with

$50 Free on Us!

Want to contribute?

You could earn up to $600 by adding new articles.

Building Your Own Mail Server With FreeBSD 11

Last Updated: Fri, Nov 23, 2018
BSD Email
Archived content

This article is outdated and may not work correctly for current operating systems or software.

Running your own email server can be quite rewarding. You are in charge of your data. It also allows you more flexibility with your delivery options. However, there are a few challenges. You run the risk of opening your server up to vulnerabilities, as well as making your server a potential relay for spammers to use.

With that out of the way, let's get on to running our own mail server.



There are three required pieces of software to install that aren't included in the FreeBSD base system:

  • OpenSMTPd

  • Dovecot

  • spamd

OpenSMTPd is a mail transfer agent (MTA) and mail delivery agent (MDA). This means that it can communicate with other mail servers over the SMTP protocol, and it also handles delivering mail to the individual users' mailboxes. We'll be setting up OpenSMTPd so that it can communicate to external servers (filtered through spamd) and deliver mail to local users, as well as delivering local mail from user to user.

Dovecot is an MDA which reads local mailboxes and serves them up over IMAP or POP3 to the users. It will use the local users' mailboxes to serve this content.

Spamd is a mail filtering service. We can forward mail through spamd, and it will filter mail based on a variety of deny lists, allow lists, and a greylist.

The general idea for this mail server requires a few different paths:

Outside world -> Firewall -> spamd -> OpenSMTPD -> User mail boxes

Outside world -> Firewall (spamd-allow list) -> OpenSMTPD -> User mailboxes

Outside world -> Firewall (IMAP/POP3) -> Dovecot

Outside world -> Firewall (SMTPD submission)

For this tutorial, we will be using the FreeBSD version of OpenBSD's PF for our firewall. You can also use ipfw, where the configuration is very similar.

Note: Vultr, by default, blocks port 25, which is used by SMTP servers everywhere. If you want to run a fully functional email server, you will have to get that port opened up.

Initial Setup

First, we need to install the required programs.

Assuming you are running as a user with sudo access set up, we can run the following commands. They will vary depending on whether you are using ports or packages.

Packages (recommended)

Unless you need specific functionality built into these utilities, it is recommended to install via packages. It is easier, takes less server time and resources, and provides an intuitive, user friendly interface.

sudo pkg install opensmtpd dovecot spamd

The following make commands will give you lots of compile options, the defaults will work fine. Do not change these unless you know exactly what you're doing.

sudo portsnap fetch update   # or run portsnap fetch extract if using ports for the first time

cd /usr/ports/mail/opensmtpd  

make install  # Installs openSMTPd

make clean

cd /usr/ports/mail/dovecot

make install  # Installs dovecot

make clean

cd /usr/ports/mail/spamd

make install  # Installs spamd

make clean

We will need to add the following lines to /etc/rc.conf:









Firewall Setup

To configure PF, we can create our /usr/local/etc/pf.conf:

## Set public interface ##


## set and drop IP ranges on the public interface ##

martians = "{,,, \

,,, \

, }"

table <spamd> persist

table <spamd-allow> persist

# Allowed webmail services

table <webmail> persist file "/usr/local/etc/pf.webmail.ip.conf"

## Skip loop back interface - Skip all PF processing on interface ##

set skip on lo

## Sets the interface for which PF should gather statistics such as bytes in/out and packets passed/blocked ##

set loginterface $ext_if

# Deal with attacks based on incorrect handling of packet fragments 

scrub in all

# Pass spamd allow list

pass quick log on $ext_if inet proto tcp from <spamd-allow> to $ext_if port smtp \

    -> port 25

# Pass webmail servers

rdr pass quick log on $ext_if inet proto tcp from <gmail> to $ext_if port smtp \

    -> port 25

# pass submission messages.

pass quick log on $ext_if inet proto tcp from any to $ext_if port submission modulate state

# Pass unknown mail to spamd

rdr pass log on $ext_if inet proto tcp from {!<spamd-allow> <spamd>} to $ext_if port smtp \

    -> port 8025 

## Blocking spoofed packets

antispoof quick for $ext_if

## Set default policy ##

block return in log all

block out all

# Drop all Non-Routable Addresses 

block drop in quick on $ext_if from $martians to any

block drop out quick on $ext_if from any to $martians

pass in inet proto tcp to $ext_if port ssh

# Allow Ping-Pong stuff. Be a good sysadmin 

pass inet proto icmp icmp-type echoreq

# Open up imap/pop3 support

pass quick on $ext_if proto tcp from any to any port {imap, imaps, pop3, pop3s} modulate state

# Allow outgoing traffic

pass out on $ext_if proto tcp from any to any modulate state

pass out on $ext_if proto udp from any to any keep state

This is a working PF configuration. It is relatively simple, but there are a few quirks to be explained as well.

Firstly, we define our $ext_if variable for our vtnet0 device to use later on. We also define invalid IP addresses that should be dropped on the external interface.

We also define two tables, spamd and spamd-allow - these two tables are created by spamd in it's default configuration. As well, we define a table named webmail which we will use to allow some major webmail providers through.

To view a table, you can use the command pfctl -t tablename -T show to list the elements in a table.

We set a few PF rules: skip processing on the local interface, enable statistics on the external interface and scrub incoming packets.

Next is one of the more important parts, where we manage sending our traffic through to spamd or OpenSMTPd.

First up is a redirect rule (note the syntax here, FreeBSD 11 uses the older style PF syntax (pre-OpenBSD 4.6) so the syntax may seem odd. If we receive anything on smtp from a host listed in the spamd table or not listed in the spamd-allow table, we redirect the connection through to the spamd daemon, which deals with these connections. The next three rules are passthrough rules so that we can actually receive mail. We pass through messages from the IPs listed in the spamd-allow and the webmail tables straight through to OpenSMTPd. Also, we accept messages on the submission port (587).

Then there's a few housekeeping rules to set our default policy, and accept SSH and ICMP messages.

We then pass IMAP and POP3 on our external interface in order to access Dovecot.

Lastly we allow all outgoing traffic. If you wanted to add extra security, you could limit the ports you pass, but for a single-use server it's not a problem to pass everything.

Start PF:

sudo service pf start

Now that we have our firewall setup, we can move on to our mail server configuration.


OpenSMTPd has a very simple, and easy-to-read configuration syntax. An entire working configuration can fit into 14 lines, as you can see below:

#This is the smtpd server system-wide configuration file.

# See smtpd.conf(5) for more information.


# If you edit the file, you have to run "smtpctl update table aliases"

table aliases   file:/etc/mail/aliases

table domains   file:/etc/mail/domains

# Keys

pki key "/usr/local/etc/letsencrypt/live/"

pki certificate "/usr/local/etc/letsencrypt/live/"

# If you want to listen on multiple subdomains (e.g. mail.davidlenfesty) you have to add more lines

# of keys, and more lines of listeners

# Listen for local SMTP connections

listen on localhost hostname

# listen for filtered spamd connections

listen on lo0 port 10026

# Listen for submissions

listen on $ext_if port 587 tls-require auth pki tag SUBMITTED

# Accept mail from external sources.

accept from any for domain <domains> alias <aliases> deliver to maildir "~/mail"

accept for local alias <aliases> deliver to maildir "~/mail"

accept from local for any relay tls

accept tagged SUBMITTED for any relay tls

Firstly, we again define our external interface, as well as a few tables, aliases and domains. Then we move on to the SSL key and certificate for any domains we want to handle mail under.

In the next section, we define the interfaces and ports we want to listen on. Firstly, we listen on localhost for our domain, for any local connections. Then we listen for our spamd-filtered messages and submitted messages on the external interface. Lastly, we listen for submissions, these happen on port 587 and we are requiring them to authenticate, for security reasons.

Lastly are our accept settings. We accept any message for any of our domains defined in our domains table for aliases in our aliases table, to deliver to their home directory in the maildir format. Then we accept all local connections for local mailboxes and relay out our messages, so we can send email. Lastly, we accept our submitted messages to relay. If we didn't require authentication for our submissions port, this would be a big security hazard. This would let anyone use our server as a spam relay.


FreeBSD ships with a default alias file /etc/mail/aliases in the following format:

vuser1:  user1

vuser2:  user1

vuser3:  user1

vuser4:  user2

This defines the different mail boxes, and where we want to forward messages sent to these defined mailboxes. We can either define our users as local system users or external mailboxes to forward to. The default FreeBSD file is quite descriptive so you can refer to that for reference.


FreeBSD does not supply a default domains file, but this is incredibly simple:

# Domains

This is just a plain text file with each domain you want to listen to on a new line. You can make a comment using the # symbol. This file exists simply so that you can use fewer lines of configuration.

SSL Certificates

There are two ways to be able to secure your communications with your mail server, self-signed and signed certificates. It is certainly possible to self-sign your certificates, however services like Let's Encrypt provide free and incredibly easy to use signing.

First we have to install the certbot program.

sudo pkg install py-certbot

Alternatively, it can be installed with ports:

cd /usr/ports/security/py-certbot

make install

make clean

Then, to get your certificate, you need to make sure you have opened up port 80 on your external interface. Add the following lines somewhere in your filtering rules in /usr/local/etc/pf.conf:

pass quick on $ext_if from any to any port http

Then run pfctl -f /usr/local/etc/pf.conf to reload the ruleset.

Then you can run the command for any domains you want to get a certificate for:

certbot certonly --standalone -d

It is recommended to set up a crontab entry to run certbot renew once every 6 months to ensure your certificates don't expire.

Then for every relevant domain, you can modify the lines to point to the correct key file:

pki key "/usr/local/etc/letsencrypt/live/"

pki certificate "/usr/local/etc/letsencrypt/live/"

Edit the securities:

sudo chmod 700 /usr/local/etc/letsencrypt/archive/*

Note: You will have to do this for each original keyfile or else OpenSMTPd won't open them.

Now we can start the service:

sudo service smtpd start

Configuring spamd

Here we are using OpenBSD's spamd daemon to reduce the amount of spam we get from the internet. Essentially, this filters out messages from IPs that are known as bad from various spam sources, as well as (by default) "greylisting" incoming connections. Spamd also tries to waste spammer's time by "stuttering" denied and greylisted connections, which means it spreads out its response over several seconds which forces the client to stay open for longer than usual.

Greylisting a connection is done when any new IP address connects that isn't on any deny list or allow list. Once the new address connects, spamd drops the message with an inocuous error message, then it adds it to a temporary list. Because spammers get paid for delivered messages, they will not retry on an error, whereas a legitimate service will retry relatively soon.

You will have to run the following to mount fdescfs:

mount -t fdescfs null /dev/fd

Then you will have to add this line to /etc/fstab:

fdescfs     /dev/fd     fdescfs rw      0       0

The default config file (found in /usr/local/etc/spamd/spamd.conf.sample) will work fine. You can edit it to add new sources or change the sources you use:

sudo cp /usr/local/etc/spamd/spamd.conf.sample /usr/local/etc/spamd/spamd.conf

We can start the service with the following:

sudo service obspamd start

At this point spamd is set up.

Enabling Webmail Services

One problem with the greylisting approach is that large mail services will often send mail out through one of many different spools, and you aren't guaranteed to get the same server sending the message every time. One solution to this is to allow the IP ranges used by various webmail services. This is what the webmail table is used for in the PF configuration. This strategy can backfire if you include an IP address a spammer uses, but as long as you are careful with what ranges you put in the table you will be fine.

To add an email range to the webmail table, you can run the following command:

pfctl -t webmail -T add


If you want users to access their mail without logging in via SSH, you'll need an MDA that supports IMAP and/or POP3. A very popular program is Dovecot, with a fairly simple configuration and powerful features.

We can copy over the default configuration:

cd /usr/local/etc/dovecot

cp -R example-config/* ./

The configuration is made up of quite a few different files. To see the differences between your configuration and the dovecot defaults, run the command below:

sudo doveconf -n

The following is a simple, working configuration:

# (0719df592): /usr/local/etc/dovecot/dovecot.conf

# OS: FreeBSD 11.2-RELEASE amd64  

# Hostname:

hostname =

mail_location = maildir:~/mail

namespace inbox {

  inbox = yes

  location = 

  mailbox Archive {

    auto = create

    special_use = \Archive


  mailbox Archives {

    auto = create

    special_use = \Archive


  mailbox Drafts {

    auto = subscribe

    special_use = \Drafts


  mailbox Junk {

    auto = create

    autoexpunge = 60 days

    special_use = \Junk


  mailbox Sent {

    auto = subscribe

    special_use = \Sent


  mailbox "Sent Mail" {

    auto = no

    special_use = \Sent


  mailbox "Sent Messages" {

    auto = no

    special_use = \Sent


  mailbox Spam {

    auto = no

    special_use = \Junk


  mailbox Trash {

    auto = no

    autoexpunge = 90 days

    special_use = \Trash


  prefix = 

  separator = /


passdb {

  args = imap

  driver = pam


ssl = required

ssl_cert = </usr/local/etc/letsencrypt/live/

ssl_dh = </usr/local/etc/dovecot/dh.pem

ssl_key = </usr/local/etc/letsencrypt/live/

userdb {

  driver = passwd


Most config files will be in conf.d

The important ones are 10-auth.conf, 10-mail.conf, and 10-ssl.conf.

You can configure the different mailboxes you use in 15-mailboxes.conf. What you see above is a good configuration for many systems, but your mileage may vary. It's recommended you play around with this with as many different clients as you can.


Most default settings will be correct. If you want to use the system users to authenticate, you will have to edit 10-auth.conf.

Uncomment the following line:

!include auth-system.conf.ext


We have to generate Diffie-Hellman parameters:

sudo nohup openssl dhparam -out /usr/local/etc/dovecot/dh.pem

Note: This will take a long time to run. Much longer than you might expect.

We can now start Dovecot:

sudo service dovecot start


At this point, we have a functional, secure and relatively spam-free mail server.

Some more things to look into from here are using SpamAssassin to heuristically get rid of spam, as well as finding more spamd deny lists put out by sources you trust.

Want to contribute?

You could earn up to $600 by adding new articles.