Gerardo Zamudio about Linux, open source, and Internet

Slackware Mail Server with MySQL, Postfix, and Dovecot

Introduction

This post contains some suggestions for a mildly secure mail server running on a Slackware Linux host. The guide assumes a default, fresh installation of Slackware64 14.1 that includes at least the A/, AP/, D/, L/, and N/ package series. By the end of the tutorial, you will have:

  • Postfix for encrypted connections over SMTP and removing identifying information from the email headers
  • Dovecot for local mail directories and encrypted connections over POP and IMAP
  • MariaDB a drop-in replacement for MySQL to store mailbox information
  • Postgrey which will require unknown senders to resend their mail, eliminating most spam
  • SpamAssassin for e-mail spam filtering based on content-matching rules
  • ClamAV to detect trojans, viruses, malware and other malicious threats in your email
  • amavisd-new to manage ClamAV and SpamAssasin
  • OpenDKIM a DomainKeys Identified Mail (DKIM) milter to sign and/or verify mail
  • nginx as a webserver (optional)
  • Postfix Admin to manage mailboxes and domains using a TLS secured web user interface (optional)
  • Roundcube as a TLS secured webmail client (optional)

I based some of the Postfix and Dovecot configuration options on an existing mailserver guide by Reason.

Why?

There are a number of reasons you may want to host your own email. The Internet of today is not the Internet of yesterday. There is a movement to decentralize the internet and regain control of one’s data. You and others may want to distance yourself from “the stacks”: the internet companies that control a large portion of it. Think of the chaos that ensues whenever CloudFlare’s hardware fucks up and how much of the internet relies on providers like them and Akamai. Think of the companies that have presence in every aspect of your digital life. Today, you can be woken up by the alarm on your Google Nexus phone, running Google’s version of Android. You’ll check your email on Gmail and plan your day using Google Calendar on your Google Chromebook running Chrome OS, through your Google Fiber internet connection. They may even be your domain registrar. Watch your Google TV while you eat breakfast. You can put on Google Glass and get a quick bike route to work on Google Maps. Listen to Google Music while you drive. Go over your presentation and spreadsheets on Google Docs and save them on Google Drive. What if Google disappeared tomorrow? The infrastructure of the internet is just as fragile and people want to keep control of their data.

We also have hackers breaking into crypto currency exchanges and vendors bundling spyware with their PCs. Single points of failure are common, with the organizations in charge not even being able to trust each other:

Both the US commerce department and the Department of Homeland Security take a close interest, to differing degrees, in ICANN’s operations. In the wake of the ongoing revelations of NSA spying, and of undermined internet security, this does not sit well with many of ICANN’s overseas partners. Some, including Russia and Brazil – whose president has made such demands very public – are calling for a complete overhaul of how the internet is run, suggesting it should be put under UN auspices.

Read Ken Thompson’s Reflections on Trusting Trust and see why you can’t even trust your compiler. Trust is difficult. Email is horribly insecure.

A note about hosting providers

There are a number of hosting providers that offer Slackware images, mostly based in the U.K. Linode offers some cheap VPS plans and is based in the US. I got the folks at CloudSigma to build a Slackware image which I’m sure they’ll happily provide if you ask. The guys at LQ also offer some suggestions. In general, you want to keep these things in mind:

  • Where is the company based? If it’s a publicly traded company, who is involved?
  • Does the company own their data centers or are they colocated?
  • Where are the data centers located? Does the country have strong privacy laws?

You’ll also want to pay attention to any modifications they may make to the images they provide. Linode, for example, provide their own custom kernel and have the -current branch of Slackware selected in /etc/slackpkg/mirrors. They also have a very minimal system with only the absolute necessary packages necessary to run the OS installed. You’ll want to skip installing glibc-zoneinfo. It turns out if you set the date on the server and then proceed to install that package, your /etc/localtime file will become corrupted. I notified Linode about this issue but they didn’t seem to care too much. On the CloudSigma platform watch out for a script they add to /etc/rc.d/rc.local that creates a shell user for them and checks your host keys. There may be other caveats on other providers. Make sure you check the OS thoroughly. You’ll also have to make some changes if you roll your own image. Most of the time, you will have to chroot and create an initrd right after installation, before rebooting:

chroot /mnt
/usr/share/mkinitrd/mkinitrd_command_generator.sh | sh

You’ll need to edit /etc/lilo.conf and add some parameters to your global and partition sections. These were taken from a CloudSigma image. They should work on other KVM based cloud platforms that allow you to upload your own ISO, such as Vultr:

# Start LILO global section
lba32
append=" vt.default_utf8=0 elevator=deadline"
boot=/dev/vda
disk=/dev/vda bios=0x80 max-partitions=7
bitmap = /boot/slack.bmp
bmp-colors = 255,0,255,0,255,0
bmp-table = 60,6,1,16
bmp-timer = 65,27,0,255
prompt
timeout = 100
reset
vga = normal
# End LILO global section
# Linux bootable partition config begins
image = /boot/vmlinuz
initrd = /boot/initrd.gz
root = /dev/vda2
label = Slackware64
read-only
# Linux bootable partition config ends

Adjust your root device accordingly or contact your provider for assistance. Don’t forget to run lilo -C /mnt/etc/lilo.conf before rebooting.

Self host

Another option you have is setting this up in a physical server in your home. Former secretary of state Hillary Clinton certainly thought it was a good idea:

[She] used a private email account rather than her official State.gov email address while serving in the State Department. And this was no Gmail or Yahoo! Mail account: On Wednesday the AP reported that Clinton actually ran a private mail server in her home during her entire tenure leading the State Department, hosting her email at the domain Clintonemail.com.

The article questions her use of a private domain registrar and a self-signed certificate. These are real concerns and unfortunate tradeoffs that have to be made for those of us who are unable to register a .gov domain or purchase a certificate from a “trusted” authority. Some argue a self-signed certificate is more secure than one purchased from a certificate authority (only trust yourself), but if you want to get rid of those pesky browser warnings you’ll need to had over some money to a certificate authority.

This setup will most likely involve some port forwarding being done on your modem or router. Preferably, you should also have a static IP. Most ISPs don’t allow residential customers to host mail servers using their service. Contact them if you have any doubts.

Now we’re ready to get started.

Preparation

Hostname

Set up an FQDN as your hostname. We’ll use mail.example.org:

echo "mail.example.org" > /etc/HOSTNAME 
hostname -F /etc/HOSTNAME

PHP + nginx

Install nginx

I recently switched my site to nginx so I’ll be sticking with that for Postfix Admin and Roundcube. You’ll need to compile nginx with support for FastCGI and memcached to better support Roundcube so you’re probably better off using the build from SlackBuilds than the configure options I gave in the linked post. Create your webserver user:

useradd -r -M -U -c "User for nginx" -d /srv/httpd -s /bin/false nginx

If you choose to install from source, there is a handy modification you can make to the code before compiling. As of this writing, the most recent stable release of nginx is 1.8.0. Extract the source archive and change into the new directory:

tar -xvzf nginx-1.8.0.tar.gz
cd nginx-1.8.0

Next, we’ll edit two files to remove some identifying information. The first of these is the src/http/ngx_http_header_filter_module.c file in the extracted source. We are going to remove the Server: string of the host. You don’t really need to advertise this especially if you’ll be the only user on the server (or just a few friends). The Server: string is found in lines 49-50. You will also need to change lines 281-282:

--- a/src/http/ngx_http_header_filter_module.c    2015-04-10 08:33:35.000000000 -0600
+++ b/src/http/ngx_http_header_filter_module.c    2015-04-10 09:33:35.000000000 -0600
@@ -46,8 +46,8 @@
 };
 
 
-static char ngx_http_server_string[] = "Server: nginx" CRLF;
-static char ngx_http_server_full_string[] = "Server: " NGINX_VER CRLF;
+static char ngx_http_server_string[] = "";
+static char ngx_http_server_full_string[] = "";
 
 
 static ngx_str_t ngx_http_status_lines[] = {
@@ -278,8 +278,8 @@
     clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
 
     if (r->headers_out.server == NULL) {
- len += clcf->server_tokens ? sizeof(ngx_http_server_full_string) - 1:
- sizeof(ngx_http_server_string) - 1;
+ len += clcf->server_tokens ? sizeof(ngx_http_server_full_string) - 0:
+ sizeof(ngx_http_server_string) - 0;
     }
 
     if (r->headers_out.date == NULL) {

The next edit will remove it from the auto generated error pages as well. This is done in the src/http/ngx_http_special_response.c file in lines 21 and 28. In this same file you can also change the HTML for each of the server error pages such as 301, 404, etc:

--- a/src/http/ngx_http_special_response.c        2015-04-10 08:33:35.000000000 -0600
+++ b/src/http/ngx_http_special_response.c        2015-04-10 09:33:35.000000000 -0600
@@ -18,18 +18,8 @@
 static ngx_int_t ngx_http_send_refresh(ngx_http_request_t *r);
 
 
-static u_char ngx_http_error_full_tail[] =
-"<hr><center>" NGINX_VER "</center>" CRLF
-"</body>" CRLF
-"</html>" CRLF
-;
-
-
-static u_char ngx_http_error_tail[] =
-"<hr><center>nginx</center>" CRLF
-"</body>" CRLF
-"</html>" CRLF
-;
+static u_char ngx_http_error_full_tail[] = "" CRLF;
+static u_char ngx_http_error_tail[] = "" CRLF;
 
 
 static u_char ngx_http_msie_padding[] =

Once you’ve made these changes, go ahead and compile. You can still use the SlackBuild if you tar up the sources, otherwise, just ./configure && make && make install. You’ll want to add the --with-http_spdy_module to add support for SPDY, the starting point for HTTP 2.0. If you use the SlackBuild don’t forget to change the VERSION, USER and GROUP variables in the script.

Unfortunately there is no init script included with the source, but you can use rc.nginx from the SlackBuild. If you want to enable syntax highlighlting for the nginx.conf file in vim, copy the contrib/vim directory from the extracted source to ~/.vim.

SSL Certificate

This is a tough one. The SHA-1 hash algorithm has been “sunsetting” since back in 2014 thanks to Google so stay away from that, not just because Google says so but because they have good reason to. You’ll hear everyone urging to move from RSA to ECDSA but then it turns out NIST curves are suspicious.. You can feel free to browse a list of Safe Curves but you’ll be hard pressed to find any Certificate Authority that offers any certificate using non-NIST curves. Browser vendors also tend to only support these as well. To that end, you can then refer to things like Mozilla’s Modern Compatibility ciphersuite selection for your nginx SSL configuration. The linked example will require modern browsers and mobile devices like Android 4.4+ in order to work properly. Let’s say you go with a relatively strong NIST curve like secp384r1 for compatibility (or the stronger secp521r1 if not). You’ll need to find a Certificate Authority that provides ECC SSL certificates such as Comodo or DigiCert. The suggested configuration below will include support for HSTS and PFS to completely get rid of plain HTTP access to the server. It will also provide support for SPDY.

Configure nginx

Let’s set up some Server Blocks (VirtualHosts if you’re coming from Apache) to serve content. Your nginx.conf file will need an events section which can be left blank to activate the defaults or modified depending on the load you’re expecting on the server.

events {
}

http {
	charset                   utf-8;
	default_type              application/octet-stream;
	ignore_invalid_headers    on;
	include                   /etc/nginx/mime.types;
	server_tokens             off;
	server_name_in_redirect   off;
	source_charset            utf-8;
	ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA;
	ssl_ecdh_curve secp384r1;
	ssl_prefer_server_ciphers on;
	ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
	
	server {
		access_log  /var/log/nginx/access.log main buffer=32k;
		error_log   /var/log/nginx/error.log error;
		listen      80;
		server_name mail.example.org;
		root        /var/empty;
		return 301 https://mail.example.org$uri;
	}

	server {
		add_header  Strict-Transport-Security "max-age=315360000; includeSubdomains; preload";
		access_log  /var/log/nginx/access.log main;
		error_log   /var/log/nginx/error.log info;
		index       index.php;
		listen      443 ssl spdy;
		root        /var/www/html/mail.example.org;
		server_name mail.example.org;

		ssl on;
		ssl_session_cache shared:SSL:1m;
		ssl_certificate /ssl_keys/crt/mail.example.org.crt;
		ssl_certificate_key /ssl_keys/key/mail.example.org.key;
		
	}
}

You will notice I am only allowing TLS and disabling SSL. I used /var/www/html/mail.example.org as the site’s root for easier management, but you can use whatever you feel comfortable with. Make sure that whatever directory structure you choose allows the nginx user and group to write to it.

Configure PHP

Slackware’s default PHP installation is already compiled with support for GD, curl, mcrypt, and XML parsing. As mentioned before, you’ll probably want to install some extensions to increase performance. I’m going with memcached, which you can also obtain from SlackBuilds.

If you choose to install memcached, don’t forget to install the PHP extension as well. You can grab the older memcache extension from SlackBuilds, but it’s probably easier to install the newer (confusingly named) memcached extension through PECL:

pecl install memcached

This will require that you have libmemcached (SlackBuild) installed. Remember to add extension=memcached.so to your php.ini file or simply create a new /etc/php/memcached.ini file with that content. You’ll also want to change the user memcached runs as in the /etc/rc.d/rc.memcached init script to nginx if you went the SlackBuilds route. I also recommend giving it its own run directory, assigning it to the nginx user and group, and modifying the value of PID to /var/run/memcached/memcached.pid in /etc/rc.d/rc.memcached. In addition, add -s /var/run/memcached/memcached.sock to the memcached_start() function in the same file to use a socket instead of TCP:

mkdir /var/run/memcached
chown nginx:nginx /var/run/memcached

While we’re changing permissions, you’ll also need to change the permissions of /var/lib/php since we’re using nginx instead of Apache to run PHP:

chown root:nginx /var/lib/php/

Note that in order to make proper use of GD, you’ll need some X11 libraries. These are most likely not installed if you’re working on a headless server. Get the needed dependencies from a Slackware mirror:

slackpkg install libX11 libXpm libxcb libXau libXdmcp

PHP-FPM with FastCGI in nginx

The default Slackware install includes PHP-FPM so you’ll need to make sure the startup script is executable. Start PHP-FPM at least once to let it create its default configuration files:

chmod +x /etc/rc.d/rc.php-fpm
/etc/rc.d/rc.php-fpm start
/etc/rc.d/rc.php-fpm stop

We’ll be making some changes to the www pool in /etc/php-fpm/php-fpm.conf. First, change the user and group lines to nginx, with listen mode set to 0666. We’re going to be using Unix sockets throughout this guide so change listen to /var/run/php-fpm.sock . We also need to add .htm and .html files to the scripts FPM will allow to pass. The relevant lines should look like this:

[www]
user = nginx
group = nginx
listen = /var/run/php-fpm.sock
listen.owner = nginx
listen.group = nginx
listen.mode = 0666
security.limit_extensions = .php .php3 .php4 .php5 .html .htm

We’ll need to add a new location block inside the HTTPS server block in /etc/nginx/nginx.conf telling it to use the FastCGI server we set up previously for PHP files:

location ~ ^(.+?\.php)(/.*)?$ {
fastcgi_param HTTPS on;
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_buffer_size 128k;
fastcgi_buffers 256 4k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
}

Feel free to adjust those values to your needs. Pay attention to fastcgi_pass, which connects to the Unix socket.

Dovecot

We are going to be using Dovecot to handle secure IMAP and POP3 connections and for SMTP authentication. Postfix Admin will store the user information in this setup instead of creating a Unix account for each one. The email will be stored in /var/vmail organized by domain and user, so the email for admin@example.org would be stored in /var/vmail/example.org/admin. We’ll create a single user to own the mailboxes on the system and let Dovecot manage them. Dovecot will also need its own user and group:

useradd -r -d /var/vmail -s /bin/false -u 150 -g 12 vmail
mkdir /var/vmail
chmod 770 /var/vmail
chown vmail:mail /var/vmail
groupadd -g 202 dovecot
useradd -d /dev/null -s /bin/false -u 202 -g 202 dovecot
groupadd -g 248 dovenull
useradd -d /dev/null -s /bin/false -u 248 -g 248 dovenull

Make sure the GIDs and UIDs used above do not interfere with any of your current users and groups. You can compile Dovecot from source with a simple ./configure && make && make install or grab the SlackBuild. Edit the VERSION variable in the SlackBuild to build the latest version, which is 2.2.16 as of this writing. If you build from source on your own, make sure you compile with the --with-mysql flag to enable MariaDB support. You are also responsible for creating an init script to manage the service. There is one provided in the source in doc/dovecot-initd.sh which should work with Slackware after some minor adjustments. You can also use Alan Hick’s simplied version from the SlackBuild if you like. Make sure it’s executable as well.

Once you have Dovecot installed, copy over the example configuration files from /usr/doc/dovecot-2.2.16/example-config into /etc/dovecot and change its permissions while you’re there:

cp -r /usr/doc/dovecot-2.2.16/example-config/* /etc/dovecot/
chown -R vmail:dovecot /etc/dovecot
chmod -R o-rw /etc/dovecot

We are going to set up the database connection between Dovecot and MariaDB in the /etc/dovecot/dovecot-sql.conf.ext file. Edit that file and make sure you set the following options:

driver = mysql
connect = host=/var/run/mysql/mysql.sock dbname=mail user=mail password=password-here
default_pass_scheme = SHA512-CRYPT

You will also need to add two queries, password_query to retrieve passwords and user_query to get user information. This information will be obtained from the schema Postfix Admin will create. If you didn’t want to install the Postfix Admin web interface, you can still use it to generate schema for you so you can add users manually and still have Dovecot find them. Of course if you’re good with databases you can make up your own and tell Dovecot here how to find your users. See Dovecot’s SQL documentation for more. Either way, here’s what you need to add for Postfix Admin:

	
password_query = \
  SELECT username as user, password, '/var/vmail/%d/%n' as \
  userdb_home, 'maildir:/var/vmail/%d/%n' as userdb_mail, \
  150 as userdb_uid, 12 as userdb_gid \
  FROM mailbox WHERE username = '%u' AND active = '1'

user_query = \
  SELECT '/var/vmail/%d/%n' as home, 'maildir:/var/vmail/%d/%n' \
  as mail, 150 AS uid, 12 AS gid, \
  concat('dirsize:storage=', quota) AS quota \
  FROM mailbox WHERE username = '%u' AND active = '1'

The next file to edit is /etc/dovecot/conf.d/10-auth.conf. We will enable the SQL configuration file we just modified and disable plaintext authentication unless it’s already encrypted through TLS. We’ll also disable the auth-system.conf.ext file that’s loaded by default. Don’t worry about authenticating with plain text; your connection will be secured with TLS so this is safe:

	
disable_plaintext_auth = yes
auth_mechanisms = plain login
#!include auth-system.conf.ext
!include auth-sql.conf.ext

Now we’re going to tell Dovecot where the email is stored in the filesystem. Change the UID in the following lines in /etc/dovecot/conf.d/10-mail.conf to whatever you set it to when you created the vmail user:

	
mail_location = maildir:/var/vmail/%d/%n
mail_uid = vmail
mail_gid = mail
first_valid_uid = 150
last_valid_uid = 150

You need to edit /etc/dovecot/conf.d/10-ssl.conf and set the path to the SSL certificate you obtained earlier for nginx. We are going to disable SSLv3 as we did with the web server. We can use the same SSL ciphers here

ssl = yes
ssl_cert = </ssl_keys/crt/mail.example.org.crt
ssl_key = </ssl_keys/key/mail.example.org.key
ssl_protocols = !SSLv2 !SSLv3
ssl_cipher_list =  ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA
ssl_prefer_server_ciphers = yes

Dovecot needs to know how to authenticate to the userdb so uncomment the user, group, and mode lines in the unix_listener auth-userdb section of the service auth block in /etc/dovecot/conf.d/10-master.conf. You also need to set up a unix listener for Postfix. Uncomment that section as well and add postfix as the user and group. We’ll create those later when we’re setting up Postfix.

service auth {
  unix_listener auth-userdb {
    mode = 0666
    user = vmail 
    group = mail
  }
}

  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix
  }

Postfix

Installing Postfix is simple if you use Alan Hick’s excellent SlackBuild script. Set the DATABASE variable to mysql before running the script to enable MariaDB support. You have the option, of course, to compile from source yourself. In that case you will need to pay attention to the Postfix configuration parameters outlined in section 4.6.2 of the INSTALL file in Postfix’s official documentation . Whichever you choose, you will need to create a user and group for Postfix. Wietse suggests in the INSTALL file to create a postfix user with no login shell or home directory and a postdrop group with a group ID that is not used by any other account, even the postfix user. The SlackBuild script has the following, which is what I went with:

groupadd -g 200 postfix
groupadd -g 201 postdrop
useradd -u 200 -d /dev/null -s /bin/false -g postfix postfix

You’re also going to need a startup script. Alan’s rc.postfix script is good enough. We’re going to need the following configuration files to tell Postfix how to find your users in the database. These are lifted straight from Reason’s guide. Note in our setup, we’re using Unix sockets so that has been changed to localhost. This will cause Postfix to connect to the default domain socket. The username and password used here are the same we’ll use when we set up Postfix Admin.

/etc/postfix/mysql_virtual_alias_domainaliases_maps.cf

user = mail
password = mailpassword
hosts = localhost
dbname = mail
query = SELECT goto FROM alias,alias_domain
  WHERE alias_domain.alias_domain = '%d'
  AND alias.address=concat('%u', '@', alias_domain.target_domain)
  AND alias.active = 1

/etc/postfix/mysql_virtual_alias_maps.cf

user = mail
password = mailpassword
hosts = localhost
dbname = mail
table = alias
select_field = goto
where_field = address
additional_conditions = and active = '1'

/etc/postfix/mysql_virtual_domains_maps.cf

user = mail
password = mailpassword
hosts = localhost
dbname = mail
table = domain
select_field = domain
where_field = domain
additional_conditions = and backupmx = '0' and active = '1'

/etc/postfix/mysql_virtual_mailbox_domainaliases_maps.cf

user = mail
password = mailpassword
hosts = localhost
dbname = mail
query = SELECT maildir FROM mailbox, alias_domain
  WHERE alias_domain.alias_domain = '%d'
  AND mailbox.username=concat('%u', '@', alias_domain.target_domain )
  AND mailbox.active = 1

/etc/postfix/mysql_virtual_mailbox_maps.cf

user = mail
password = mailpassword
hosts = localhost
dbname = mail
table = mailbox
select_field = CONCAT(domain, '/', local_part)
where_field = username
additional_conditions = and active = '1'

We’re going to strip the email client and IP address of each mail you send. Remember this is just security through obscurity and you’ll be violating RFC 2045 if you remove the MIME-Version string. It’s still useful to prevent uninitiated recipients from figuring out where you are sending your mail from. Add these to the file /etc/postfix/header_checks for now. We’ll enable it in the Postfix configuration later.

	
/^Received:/                 IGNORE
/^User-Agent:/               IGNORE
/^X-Mailer:/                 IGNORE
/^X-Enigmail:/               IGNORE
/^X-Originating-IP:/         IGNORE
/^x-cr-[a-z]*:/              IGNORE
/^Thread-Index:/             IGNORE
/^\s*Mime-Version: 1.0.*/    REPLACE Mime-Version: 1.0

Postfix Main Configuration

There are way too many Postfix configuration options to be able to go through them all here. The default configuration file at /etc/postfix/main.cf should have some good defaults which you can make minor adjustments to. We are specifically adding header_checks and mime_header_checks to enable the /etc/postfix/header_checks file we created earlier.

myhostname = mail.example.org
myorigin = /etc/hostname
inet_interfaces = all
mynetworks = 127.0.0.0/24 [::ffff:127.0.0.0]/104 [::1]/128
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
header_checks = regexp:/etc/postfix/header_checks
mime_header_checks = regexp:/etc/postfix/header_checks
smtpd_banner = $myhostname ESMTP $mail_name
biff = no
append_dot_mydomain = no

The next section is to use Dovecot for authentication. You’ll need to add these

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
broken_sasl_auth_clients = no
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain =
smtpd_sasl_authenticated_header = yes

We’ll set up TLS next. We’re disabling the use of any SSL versions. The last two options are to force incoming and outgoing SMTP connections to use TLS. This may have some unwanted consquences. If a mail server you are trying to reach does not support this, you will not be able to communicate with it. You can change these options to may instead of encrypt to enable TLS but not enforce it. Check your logs frequently and if you see any incoming connections without TLS, you can suggest that server’s admin to enable this feature.

smtpd_tls_cert_file=/ssl_keys/crt/mail.example.org.crt
smtpd_tls_key_file=/ssl_keys/key/mail.example.org.key
smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3
smtp_tls_note_starttls_offer = yes
smtpd_tls_loglevel = 1
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
tls_random_source = dev:/dev/urandom
smtpd_tls_security_level = encrypt
smtp_tls_security_level = encrypt

The SMTP parameters are below. You can tweak these to your tastes. These have to do with the amount of times server connections are retried if failed, how long you want to keep mail in your queue (in case the remote server is not reachable for some time), recipient limits, and so on.

delay_warning_time = 4h
maximal_queue_lifetime = 4d
minimal_backoff_time = 1000s
maximal_backoff_time = 8000s
smtp_helo_timeout = 60s
smtpd_recipient_limit = 16
smtpd_soft_error_limit = 3
smtpd_hard_error_limit = 12

We need to add some restrictions regarding who can send and receive mail on this server. This also allows for checks against various DNSBLs to block spam. We’ll also need check_policy_service for Postgrey later on and we’ll set up the milters to use OpenDKIM. Use Unix sockets here.

smtpd_helo_restrictions = permit_mynetworks, warn_if_reject reject_non_fqdn_hostname, reject_invalid_hostname, permit
smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks, warn_if_reject reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unauth_pipelining, permit
smtpd_client_restrictions = reject_rbl_client sbl.spamhaus.org, reject_rbl_client blackholes.easynet.nl, reject_rbl_client b.barracudacentral.org
smtpd_recipient_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:/var/run/postgrey/postgrey.sock, permit
smtpd_data_restrictions = reject_unauth_pipelining
smtpd_relay_restrictions = reject_unauth_pipelining, permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:/var/run/postgrey/postgrey.sock, permit
smtpd_helo_required = yes
smtpd_delay_reject = yes
disable_vrfy_command = yes
mailbox_size_limit = 0
recipient_delimiter = +
non_smtpd_milters=unix:/var/run/opendkim/opendkim.sock
smtpd_milters=unix:/var/run/opendkim/opendkim.sock

The next section will use the MySQL configuration files we set up earlier and tell Postfix were the mail folders are located. Pay attention to the UID and GID used.

virtual_mailbox_base = /var/vmail
virtual_mailbox_maps = mysql:/etc/postfix/mysql_virtual_mailbox_maps.cf, mysql:/etc/postfix/mysql_virtual_mailbox_domainaliases_maps.cf
virtual_uid_maps = static:150
virtual_gid_maps = static:12
virtual_alias_maps = mysql:/etc/postfix/mysql_virtual_alias_maps.cf, mysql:/etc/postfix/mysql_virtual_alias_domainaliases_maps.cf
virtual_mailbox_domains = mysql:/etc/postfix/mysql_virtual_domains_maps.cf

Finally, we’re going to integrate Postfix with Amavis and Dovecot.

virtual_transport = dovecot
dovecot_destination_recipient_limit = 1
content_filter = amavis-forward:unix:amavis/amavisd.sock

Postfix Master Configuration

That should take care of the /etc/postfix/main.cf file. Now let’s edit /etc/postfix/master.cf. Check the file out before editing to get a feel for the format and read the comments describing what the options do. Some of them you can edit and others you’ll have to add. First we’ll set up SMTP with TLS on port 587 and SMTPS on port 465

submission inet n       -       -       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_enforce_tls=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject_unauth_destination,reject
  -o smtpd_sasl_tls_security_options=noanonymous
smtps     inet  n       -       -       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject_unauth_destination,reject
  -o smtpd_sasl_security_options=noanonymous,noplaintext
  -o smtpd_sasl_tls_security_options=noanonymous

We’ll set up Amavis integration. Note the number 4 in the first line. This is the number of processes Amavis is allowed to run. Incrase this if you need more.

amavis-forward      unix    -       -       -       -       4       lmtp
  -o lmtp_data_done_timeout=1200
  -o lmtp_send_xforward_command=yes
  -o disable_dns_lookups=yes
  -o max_use=20
amavis/amavis-accept unix    n       -       -       -       -       smtpd
  -o content_filter=
  -o local_recipient_maps=
  -o relay_recipient_maps=
  -o cleanup_service_name=cleanup
  -o smtpd_restriction_classes=
  -o smtpd_delay_reject=no
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o smtpd_data_restrictions=reject_unauth_pipelining
  -o smtpd_end_of_data_restrictions=
  -o mynetworks=127.0.0.0/8
  -o smtpd_error_sleep_time=0
  -o smtpd_soft_error_limit=1001
  -o smtpd_hard_error_limit=1000
  -o smtpd_client_connection_count_limit=0
  -o smtpd_client_connection_rate_limit=0
  -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks

Uncomment the other external delivery methods if you need them. Let’s not forget to set up the Dovecot for local delivery

dovecot      unix   -        n      n       -       -   pipe
  flags=DRhu user=vmail:mail argv=/usr/libexec/dovecot/dovecot-lda -d $(recipient)

MariaDB and Postfix Admin

A note about web based interfaces

Installing a web based user interface for any of these services is entirely optional. This makes some tasks easier, such as adding a new domain and mailbox to your Postfix database. A web based email client can be beneficial if you are in an unfamiliar environment and only have access to a browser. However, you are increasing the attack vectors to your server. Web based control panels have long been a favorite gateway for script kiddies to gain unauthorized access to servers due to their popularity. You can protect yourself against common attacks (bots looking for /roundcube, install.php, etc) by renaming the directory where the website files reside so they’re no longer accessible or changing the permissions. You can restrict access only to your home IP using your server’s firewall or through some file access rules in nginx.conf. You can also simply not install them at all and work through the command line. For the sake of completeness, this guide will assume you want them.

MariaDB Initial Setup

Slackware officially switched to MariaDB starting with 14.1 but the init scripts are still named rc.mysqld. It should be included in a default Slackware install as part of the AP/ series, but if you don’t have it just type slackpkg install mysql as root. Make sure the program starts at boot:

chmod +x /etc/rc.d/rc.mysqld

Now we need to perform the first time setup:

/usr/bin/mysql_install_db --user=mysql

Start MariaDB then perform the secure installation. Make sure to set up a secure password for MariaDB’s root user.

/etc/rc.d/rc.mysqld start
/usr/bin/mysql_secure_installation

Once that’s set up, log in as root using mysql -u root -p and create the user and database that we’ll use for Postfix Admin. Choose a secure password here.

CREATE DATABASE mail;
GRANT ALL PRIVILEGES ON mail.* TO "mail"@"localhost" \
IDENTIFIED BY "password-here";
FLUSH PRIVILEGES;

Postfix Admin

This one is a breeze to set up. You can create a new server block in nginx.conf to set it up as a subdomain, but I chose to install it as a subdirectory. Change into your main site’s root and download the latest Postfix Admin tarball. As of this writing, that’s 2.92:

cd /var/www/html/mail.example.org/
wget http://softlayer-dal.dl.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-2.92/postfixadmin-2.92.tar.gz
tar -xzf postfixadmin-2.92.tar.gz 
mv postfixadmin-2.92 postfixadmin     
chown -R nginx:nginx postfixadmin
cd postfixadmin

You can modify config.inc.php, but I recommend you create a new config.local.php and place your settings there. This has the advantage that whatever you don’t explicitly set is left at the default value in config.inc.php and whatever you change takes precedence. This way your changes won’t be overwritten if you decide to upgrade Postfix Admin. Now we’re going to add some lines to tell Postfix Admin how to connect to MariaDB. We are also going to set configured to true and tell it how to store its data. The passwords will be stored using Dovecot’s crypt scheme:

$CONF['configured'] = true;
$CONF['database_type'] = 'mysql';
$CONF['database_host'] = 'localhost:/var/run/mysql/mysql.sock';
$CONF['database_user'] = 'mail';
$CONF['database_password'] = 'password-here';
$CONF['database_name'] = 'mail';
$CONF['domain_path'] = 'NO';
$CONF['domain_in_mailbox'] = 'YES';
$CONF['encrypt'] = 'dovecot:SHA512-CRYPT';
$CONF['dovecotpw'] = "/usr/bin/doveadm pw";

Again, note that we are connecting using the MariaDB Unix socket. Next, visit https://mail.example.org/postfixadmin/setup.php to generate a setup password to use in config.local.php. If you have been following this guide carefully, you should see everything OK. If you don’t see the setup page, check your nginx log files for any PHP errors or permission errors. I’ve found from experience that these PHP applications sometimes have difficulty parsing complex passwords in configuration files so if your database connection is failing, try removing some special characters from your password. Take it up with the developers, not me.

Before you continue, we’ll need to make a few changes to the Dovecot configuration files. We’ve made all the configuration changes in the /etc/dovecot/conf.d directory so far. It turns out Postfix Admin has some difficulty parsing the !include conf.d/*.conf line in /etc/dovecot/dovecot.conf, so we’ll need to move all our configuration to that file. This is simple enough with the doveconf utility. We’ll need to change the permissions of that file and add nginx to the dovecot group to give Postfix Admin access.

cp /etc/dovecot/dovecot.conf{,.bk}
doveconf -n > /etc/dovecot/dovecot.conf
chmod 644 /etc/dovecot/dovecot.conf
usermod -G dovecot -a nginx

Once you’ve got that sorted out and placed your setup password in config.local.php, the setup will ask you to create an admin user for Postfix Admin. You will need to provide the setup password you selected earlier. Use an email address for the admin user such as admin@example.org and choose a password for it. Please make sure it’s secure.

The installer will claim you don’t need to remove setup.php and it will recommend you block off access to it. You can do that or just delete the file. You can always get it back by downloading the source tarball again. Once you’re done, you can log in to Postfix Admin at https://mail.example.org/postfixadmin using the admin user you just created.

You should take this time to go back and read through config.inc.php. It is very well commented. There are some options you may want to set such as your default admin email and footer links. Remember to add your changes to config.local.php.

Add Email Domains and Mailboxes

Log in to https://mail.example.org/postfixadmin and head over to Domain List > New Domain. Fill in whatever works for you here to add a new domain, then head to Virtual List > Add Mailbox and create your first user. I set up example.org as an email domain and admin@example.org as my first user. Doing this will generate the needed database schema that Postfix will use. Go ahead and play around with this and make sure your aliases are set up

Amavis with ClamAV and SpamAssassin

Now that we’ve installed the major components, it’s time to add some virus and spam checking. We’ll be using Amavis for this. As is the case with several of the older open source projects, Amavis has gone through a few name and code base changes. The most recent, still maintained version is amavisd-new, with the name of the program being amavisd and the name of the project being Amavis. Most people use these interchangeably. Amavis will be the interface bewtween Postfix, ClamAV and SpamAssassin.

The Amavis and SpamAssassin packages are a pain to install manually mostly due to their long list of dependencies. They’re both written in Perl and need quite a few modules. These can be installed using CPAN but I would recommend you stick to the scripts provided by Nishant Limbachia at SlackBuilds.org. This makes it much easier to keep track of what you have installed with sbopkg, slackpkg or whatever Slackware “package manager” you use. Before getting started you may want to install the arj, unrar, cabextract, lzop, nomarch, and p7zip packages to allow SpamAssassin and ClamAV to handle different compressed files. These are all available on SlackBuilds.

amavisd-new

Stick with the SlackBuild for this one. The dependencies are listed thoroughly in Nishant’s README.SBo file, including the ones for SpamAssassin. Create a user and group before you run the script.

groupadd -g 225 amavis
useradd -m -d /var/lib/amavis -s /bin/bash -u 225 -g 225 amavis

Uncomment the lines @bypass_virus_checks_maps and @bypass_spam_checks_maps at the top of /etc/amavisd.conf and add the following

@bypass_virus_checks_maps = (
   %bypass_virus_checks, @bypass_virus_checks_acl, $bypass_virus_checks_re);
 
@bypass_spam_checks_maps = (
   %bypass_spam_checks, @bypass_spam_checks_acl, $bypass_spam_checks_re);

Go down a bit further and uncomment @lookup_sql_dsn, then modify it to connect to your database using the Unix socket and the proper credentials. Amavis uses the DBD::mysql Perl module. The documentation states setting the host value to localhost will use the socket. This configuration will enable spam checking for the domains you’ve added to your database either manually or through Postfix Admin

@lookup_sql_dsn = (
    ['DBI:mysql:database=mail;host=localhost',
     'mail',
     'mailpassword']);
$sql_select_policy = 'SELECT domain from domain WHERE CONCAT("@",domain) IN (%k)';

There are a couple of other settings we can change. For instance, make sure you also set $max_servers to the same number of processes you allowed Amavis to use in /etc/postfix/master.cf. Setting the $sa_tag_level_deflt opton to a large negative number will ensure that spam headers are added to every single email. Change the user and group to the ones you created earlier, set up a home directory for configuraiton files and quarantine emails, and set your domain name (not the same as your hostname). Make sure you also uncomment the amavis section in @av_scanners. We are also setting the Unix socket for Amavis here too

$max_servers  = 4;
$sa_tag_level_deflt  = -9999;
$daemon_user  = 'amavis';
$daemon_group = 'amavis';
$mydomain = 'example.org';
$myhostname = 'mail.example.org';
$MYHOME = '/var/lib/amavis';
$QUARANTINEDIR = "$MYHOME/virusmails";
$unix_socketname = "/var/spool/postfix/amavis/amavisd.sock"; 
$unix_socket_mode = 0660;
$interface_policy{'SOCK'} = 'mysock';
$policy_bank{'mysock'} = {
   protocol => 'LMTP',
   auth_required_release => 0,
}

Comment out $inet_socket_port and $interface_policy('10026'). There may already be an $interface_policy set up for AM.PDP-SOCK. Comment it out or modify it to what I have above. Notice we’re placing the socket in the Postfix queue directory, /var/spool/postfix. We’ll need to create the directory to hold the socket and assign some permissions as well.

mkdir /var/spool/postfix/amavis/
chmod 770 /var/spool/postfix/amavis/
chown amavis:postfix /var/spool/postfix/amavis/
usermod -G amavis -a postfix

Use the following commands to let Postfix create the amavis-accept PID:

cd /var/spool/postfix/public
ln -s ../amavis amavis
cd /var/spool/postfix/pid
mkdir unix.amavis
chown root:root unix.amavis
chmod 700 unix.amavis

In order for the above to work, we need to set a $forward_method in /etc/amavisd.conf. In my setup, amavisd-new would ignore that configuration option and insist on using TCP port 10025. I could not for the life of me figure out how to get it to use a socket no matter what value I tried. I ended up having to modify the /usr/sbin/amavisd Perl script directly. Look around line 926 or search for 10025. Comment out the existing $forward_method and replace it with this:

$forward_method = $have_inet6 && !$have_inet4 ? 'smtp:[::1]:10025' : 'smtp:/var/spool/postfix/amavis/amavis-accept';

Go through the rest of /etc/amavisd.conf file and modify any settings you might want changed.

SpamAssassin

Get the SpamAssassin SlackBuild from here, or get the source files. You’ll notice one of the optional dependencies of SpamAssassin is perl-Geo-IP, which requires GeoIP (SlackBuild). If you decide to install that, I found that SpamAssassin’s sa-update script expects the IPv6 database to be in /usr/share/GeoIP/. You can get that file from MaxMind’s servers and place it where needed (as root):

wget -O - \
http://geolite.maxmind.com/download/geoip/database/GeoIPv6.dat.gz \
| gunzip -c > /usr/share/GeoIP/GeoIPv6.dat

If you chose to install from source, you can grab a startup script from the extracted tarball. The guys at Apache were nice enough to include a Slackware-ready script in the spamd directory of the extracted source, named slackware-rc-script.sh. Once you have SpamAssassin installed, edit the /etc/spamassassin and set the following options. You only really need ENABLED but the rest are a good idea.

ENABLED=1
OPTIONS="--create-prefs --max-children 5 --helper-home-dir"
PIDFILE="/var/run/spamd.pid"
CRON=1

The SpamAssassin source no longer includes rules, so you’ll have to download them. Run sa-update to do this.

ClamAV

This one is pretty straightforward as well. Download the SlackBuild and set the COUNTRY variable to your country’s two letter ISO code. Run the script to build automatically. You can also get the source directly and ./configure && make && make install. It doesn’t need anything outside a base Slackware installation. Grab the rc.clamav init script from the SlackBuild for easier management. Wether you’re using the SlackBuild or installing manually from source, you’ll need a user and group created first.

groupadd -g 210 clamav
useradd -u 210 -d /dev/null -s /bin/false -g clamav clamav

While we’re at it, go ahead and add the amavis user to the clamav group and vice versa

usermod -G clamav -a amavis
usermod -G amavis -a clamav

Edit the /etc/clamd.conf file and change the LocalSocket and LocalSocketGroup options

LocalSocket /var/run/clamav/clamd.sock
LocalSocketGroup amavis

Now go ahead and update your virus database by running freshclam as root. Don’t worry if you get a message from freshclam saying clamd was not updated. This is because we have not started clamd yet. We also need to fix some permissions to get all three to play nicely.

chmod 775 /var/lib/spamassassin/
chown amavis:amavis /var/lib/spamassassin/
chown -R amavis:amavis /var/lib/spamassassin/
chown -R amavis:amavis /var/lib/amavis
chown -R clamav:amavis /var/lib/clamav

Postgrey

This one is simple so we might as well get it out of the way. As usual, create your user and group first

groupadd -g 301 postgrey
useradd -u 301 -d /var/lib/postgrey -s /bin/false -g postgrey postgrey

Since Postgrey is just a Perl script, feel free to copy over the needed files from the source tarball to /usr/bin, notably postgrey, policy-test, and contrib/postgrereport. Check the requirements to make sure you have the needed Perl modules. You can of course use the SlackBuild, too. If you install from source, grab the init script from contrib/postgrey.init and place it in /etc/rc.d/rc.postgrey. Make sure you also get a copy of postgrey_whitelist_recipients and postgrey_whitelist_clients and place them in /etc/postfix. The SlackBuild already does this for you. If you do choose the SlackBuild, you’ll need to set POSTGREYUSR, POSTGREYGRP, POSTGREYUID and POSTGREYGID in the script to the values you set earlier when you created them.

Whichever you choose, you’ll need to edit the init script and set USER, GROUP, and HOST to their proper values. Feel free to get rid of PORT. Find the postgrey_start() function and edit the postgrey flags to make sure it uses a socket instead of TCP.

postgrey_start() {
  echo "Starting postgrey milter:  /usr/bin/postgrey -d --unix=/var/run/postgrey/postgrey.sock --pidfile=$PIDFILE --user=$USER --group=$GROUP --dbdir=/var/lib/postgrey --hostname=$HOST"
  mkdir -p /var/run/postgrey
  /usr/bin/postgrey -d \
                    --unix=/var/run/postgrey/postgrey.sock \
                    --pidfile=/var/run/postgrey/postgrey.pid \
                    --user=$USER --group=$GROUP \
                    --dbdir=/var/lib/postgrey \
                    --hostname=$HOST
}

Roundcube

Setting up Roundcube is just like setting up any other PHP site. We’ve already configured nginx to use FastCGI so it’s a matter of downloading the files and placing them in a directory. As with Postfix Admin, feel free to create a new server block for this or just stick with using a subdirectory of your existing site. Since I placed Postfix Admin in /var/www/html/mail.example.org/postfixadmin I’ll place my Roundcube installation at the root, in /var/www/html/mail.example.org/. Get the sources (I recommend the “complete” version) and extract. As of this writing, that’s 1.1.1

cd /var/www/html/mail.example.org
wget http://downloads.sourceforge.net/project/roundcubemail/roundcubemail/1.1.1/roundcubemail-1.1.1-complete.tar.gz
tar --strip-components=1 -xvzf roundcubemail-1.1.1-complete.tar.gz
chown -R nginx:nginx .

We’ll need to create a database for Roundcube to use. Log in to MariaDB with mysql -u root -p and create it. Make sure you use a strong password.

CREATE DATABASE roundcubemail /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
GRANT ALL PRIVILEGES ON roundcubemail.* TO "roundcube"@"localhost" \
IDENTIFIED BY "password-here";
FLUSH PRIVILEGES;

Roundcube includes an SQL file that can create the necessary database structure for you

mysql -u roundcube roundcubemail -p < SQL/mysql.initial.sql 

I recommend you check out the INSTALL file included in the source for a more complete guide on the installation. Now head over to https://mail.example.org/installer and make sure everything is OK in the Check environment section. My installation complained about the date.timezone setting in php.ini so I had to set that. Get a list of supported time zones from here. You may need to restart nginx and php-fpm for the changes to take effect.

echo date.timezone = "America/Mexico_City" >> /etc/httpd/php.ini

Click Next when you’re done to move on to the Create config section. You can leave most of these settings alone. If you want to know what a specific setting does, check out Roundcube’s wiki. Fill in the Database setup section with the user and database you created earlier.

Make sure you do not enable spellchecking support. If you do, Roundcube will connect to external services to check your spelling. Why would we go through all this trouble to have a third party see every word we type?

IMAP and SMTP Settings

We’re going to set default_host to tls://localhost and the default_port to 993 in the IMAP Settings section. In the SMTP Settings section, set smtp_server to tls://localhost , smtp_port to 587 and check the Use the current IMAP username and password for SMTP authentication option. Click on Create config.

After it’s done writing your configuraiton file, we’ll add support for memcached and make sure we only use HTTPS in config/config.inc.php :

$config['imap_cache'] = 'memcache';
$config['memcache_hosts'] = array( 'unix:/var/run/memcached/memcached.sock' );
$config['force_https]' = true;

Click on Continue in the browser to go to Step 3

OpenDKIM, DNS and Building Trust

The setup up to this point should be pretty much complete and meet most people’s needs. Some mail servers are quite picky when it comes to receiving email. Gmail particularly doesn’t like when an email is not signed. The next section will walk you through signing your email with DomainKeys Identified Mail and setting up Sender Policy Framework. If you are using a hosting provider for your server, you will need to contact them and have them set up a PTR record for your IP address. This is also known as rDNS. Some mail servers will reject your email if the IP you are sending from does not point back to your domain name. In general, it should look something like this:

34.216.184.93.in-addr.arpa PTR 600 example.org

If you’re hosting at home, you can try asking your ISP to set this up for you but it is unlikely they’ll want to. They may be willing if you purchase a static IP. You’ll also need to add MX records to your domain’s DNS records. You can add something like this

example.org MX 600 10 mail.example.org

That’s assuming mail.example.org points to your mail sever’s IP and you want a priority of 10. You can ask your DNS provider to add these for you. Check that the record has propagated with host:

gzamudio@hades:~ $ host -t MX example.org
example.org mail is handled by 10 mail.example.org.

OpenDKIM

You’ll want to grab the latest sources, which correspond to version 2.10.1 as of this writing. I also wrote a handy SlackBuild which you can use. Note that you will need libbsd(SlackBuild) installed in order to be able to compile. Let’s make the user and group first:

groupadd -g 305 opendkim
useradd -r -u 305 -g opendkim -d /var/run/opendkim/ -s /sbin/nologin -c "OpenDKIM Milter" opendkim

We’ll need to create the run directory as well:

mkdir -p /var/run/opendkim
chown opendkim:opendkim /var/run/opendkim

If you’re building manually, you may want to add --prefix=/usr to the configure script or else it’ll place everything in /usr/local. We’ll need support for MariaDB, so pass --with-sql-backend to the configure script as well. If you’re using the SlackBuild set the USE_MYSQL variable to yes and run the script. I used a modified version of CentOS’s init script for mine, but feel free to grab the one included in the source in the contrib/init/generic/ directory.

Once it’s installed, we’ll need to set up a basic configuration file. You can copy the sample one from opendkim/opendkim.conf.simple to /etc/opendkim and add the user and group we created earlier. Note that the SlackBuild already does this for you:

UserID opendkim:opendkim
KeyFile /etc/opendkim/keys/default.private

You’ll notice my init script will automatically create some default keys for you in /etc/opendkim/keys and create the directory if it doesn’t exist. We’re using Unix sockets in this guide, so let’s change a line in /etc/opendkim.conf:

Socket local:/var/run/opendkim/opendkim.sock

We’ll generate a 2048 bit key with the opendkim-genkey command. You can try something stronger like 4096 bits, but RFC 6376 suggests it might not fit in a 512 byte DNS UDP response. See section 3.3.3 Key Sizes for more information.

opendkim-genkey -b 2048 -D /etc/opendkim/keys -s mailsvr -d example.org

The selector is simply something to tell your key apart once you add it to your DNS records. You’ll end up with two files, mailsvr.private, which is your key, and mailsvr.txt which has a nicely formatted record you’ll need to add to your DNS zone. If you don’t manage your own DNS or have access to your zone file, simply copy the text starting with v=DKIM1 as a TXT record in whatever control panel your DNS provider uses. For the example above, this is what I got (truncated for demonstration):

"v=DKIM1; k=rsa; 
p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m8F6p1AD"

You’ll need to wait a while before the DNS record propagates but once it does you can check it with dig

dig mailsvr._domainkey.example.org TXT

Sender Policy Framework

This basically consists of adding another TXT record to your DNS zone. There used to be an SPF type record but this was removed in RFC 7208. You’ll want to add something like this:

v=spf1 a mx ~all 

Testing

We’ll need to start all the components we’ve installed and check their logs to make sure everything is running smoothly. You can place your startup commands in /etc/rc.d/rc.local and the stop commands in /etc/rc.d/rc.local_shutdown. Make sure both are executable and add this content

/etc/rc.d/rc.local

if [ -x /etc/rc.d/rc.php-fpm ]; then
        /etc/rc.d/rc.php-fpm start
fi

if [ -x /etc/rc.d/rc.memcached ]; then
        /etc/rc.d/rc.memcached start
fi

if [ -x /etc/rc.d/rc.nginx ]; then
        /etc/rc.d/rc.nginx start
fi

if [ -x /etc/rc.d/rc.postgrey ]; then
        /etc/rc.d/rc.postgrey start
fi

if [ -x /etc/rc.d/rc.clamav ]; then
        /etc/rc.d/rc.clamav start
fi

if [ -x /etc/rc.d/rc.spamd ]; then
        /etc/rc.d/rc.spamd start
fi

if [ -x /etc/rc.d/rc.amavisd-new ]; then
        /etc/rc.d/rc.amavisd-new start
fi

if [ -x /etc/rc.d/rc.postfix ]; then
        /etc/rc.d/rc.postfix start
fi

if [ -x /etc/rc.d/rc.dovecot ]; then
        /etc/rc.d/rc.dovecot start
fi

if [ -x /etc/rc.d/rc.opendkim ]; then
        /etc/rc.d/rc.opendkim start
fi

/etc/rc.d/rc.local_shutdown

if [ -x /etc/rc.d/rc.nginx ]; then
        /etc/rc.d/rc.nginx stop
fi

if [ -x /etc/rc.d/rc.php-fpm ]; then
        /etc/rc.d/rc.php-fpm stop
fi

if [ -x /etc/rc.d/rc.memcached ]; then
        /etc/rc.d/rc.memcached stop
fi

if [ -x /etc/rc.d/rc.opendkim ]; then
        /etc/rc.d/rc.opendkim stop
fi

if [ -x /etc/rc.d/rc.postfix ]; then
        /etc/rc.d/rc.postfix stop
fi

if [ -x /etc/rc.d/rc.dovecot ]; then
        /etc/rc.d/rc.dovecot stop
fi

if [ -x /etc/rc.d/rc.postgrey ]; then
        /etc/rc.d/rc.postgrey stop
fi

if [ -x /etc/rc.d/rc.amavisd-new ]; then
        /etc/rc.d/rc.amavisd-new stop
fi

if [ -x /etc/rc.d/rc.clamav ]; then
        /etc/rc.d/rc.clamav stop
fi

if [ -x /etc/rc.d/rc.spamd ]; then
        /etc/rc.d/rc.spamd stop
fi

Log in to your Roundcube webmail at https://mail.example.org and start sending email. Try sending to big providers like Gmail and Yahoo. You’ll note the first time you receive an email, it’ll be greylisted thanks to Postgrey. Check /var/log/maillog:

Apr 24 22:38:56 mail postgrey[945]: action=greylist, reason=new, client_name=mail.example.net, client_address=61.4.1.30, sender=testuser@example.net, recipient=testuser@example.org
Apr 24 22:38:56 mail postfix/smtpd[19527]: NOQUEUE: reject: RCPT from mail.example.net[63.4.1.30]: 450 4.2.0 <testuser@example.org>: Recipient address rejected: Greylisted, see http://postgrey.schweikert.ch/help/example.org.html; from=<testuser@example.net> to=<testuser@example.org> proto=ESMTP helo=<mail.example.net>

Most mail servers will try once more after some time and will be allowed the second time they send. Spammers generally only try once so this should stop some most of the common spam you could receive. Once the mail passes through, Amavis will check for viruses and other malware.

Apr 24 22:48:23 mail amavis[1078]: (01078-02) Passed CLEAN {RelayedInbound}, [63.4.1.30]:43126 [63.4.1.30] <testuser@example.net> -> <testuser@example.org>

You can also test your anti-virus using the EICAR test file. Send yourself an email from another mail server with only the following string in the body:

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

It will be detected as a virus and you’ll see this in the log as well:

Apr 24 23:05:25 mail postfix/qmgr[1060]: BF9D558771: from=<virusalert@example.org>, size=2092, nrcpt=1 (queue active)
Apr 24 23:05:25 mail amavis[21120]: (21120-01) Blocked INFECTED (Eicar-Test-Signature) {DiscardedInbound,Quarantined}, [63.4.1.30]:36539 [63.4.1.30] <testuser@example.net> -> <testuser@example.org>, quarantine: /var/lib/amavis/virusmails

In a similar fashion, you can test your spam filter using the GTUBE. Send a message from another mail server to yourself with only the following string in the body:

XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

It will be detected as spam and you’ll see this in the log as well:

Apr 24 23:13:44 mail amavis[21119]: (21119-01) Passed SPAM {RelayedTaggedInternal,Quarantined}, [63.4.1.30]:36539 [63.4.1.30] <testuser@example.net> -> <testuser@example.org>, quarantine: /var/lib/amavis/virusmails

You’ll probably want to set up an email client such as Thunderbird or Claws Mail. They should automatically detect your sever’s open ports but you can use the following settings to set it up manually. You can substitute the hostname for example.org if that points to your server’s IP as well.

Username: testuser@example.org
Incoming (IMAP) server: mail.example.org
Port: 993
SSL/TLS Enabled
Authentication: Plain (Normal Password)

Username: testuser@example.org
Outgoing (SMTP) server: mail.example.org
Port: 587
STARTTLS Enabled
Authentication: Plain (Normal Password)

Other Considerations

You’ll notice in your /var/log/maillog file that there will be a ton of bots and compromised third party servers trying to relay using your server, trying to log in using common usernames (admin, support, test, etc) and generally just trying to wreak havoc on your mail server. The setup suggestions I described in this article should prevent most of those attacks. You may consider trying something like fail2ban to automatically ban these IPs. Get yourself a nice firewall using AlienBob’s Easy Firewall Generator and block anything you don’t need.

I hope you found this guide useful. I tested this setup to the best of my ability but you may find certain things didn’t work for you. If that’s the case or you have any suggestions, comments, or just want to say hi, feel free to contact me. Encrypted email is strongly encouraged and preferred :)