Email Server With Postfix Dovecot and MailScanner (Part 1 - LEMP)

Email server is quite complicate. It is not only just for sending and receiving emails, but also need lots features and protections. Recently I built such an email server for my company. The whole process is worth to document.

I know there are some solutions exist for easy and quick setup a mail server, such as iRedMail. But I prefer to build the server step by step from scratch. This way I can understand how these all different components put together and how they work with each other. It’s kind of like driving an automobile, knowing a little bit better how the engine and transmission works will help me when there is a problem or need improvement down the road.

Features and Components

I begin with a list of some important features I needed for the mail system:

  • support virtual domain and mailboxes
  • support SMTP submission with STARTTLS
  • support IMAP/POP3 with SSL/TLS
  • have a secured web mail
  • have a management web interface for user accounts
  • have a management web interface for MailScanner

Here I list the major components I will build on the new email server:

  • LEMP server (CentOS 7 + Nginx + MariaDB + PHP)
  • Postfix
  • Dovecot (with Sieve filter)
  • MailScanner (which using ClamAV and Spamassassin)
  • MailWatch (Web UI for MailScanner)
  • RoundCube webmail
  • Postfix Admin (a web based interface used to manage mailboxes, virtual domains and aliases.)
  • Fail2ban and iptables (Firewall)
  • OpenSSL and Let’s Encripts SSL certificate
  • OpenDKIM and SPF

Domain name and DNS

Before I start to build the email server, I checked that I have these network related stuff ready:

  • Domain name: mydomain.com. I’ll use subdomain smtp.mydomain.com as the host name of the mail server.
  • A static IP from my ISP: 11.22.33.44
  • DNS records:

    mydomain.com.                  600     IN      A       11.22.33.44
    smtp.mydomain.com. 3600 IN A 11.22.33.44
    mydomain.com. 3599 IN MX 10 smtp.mydomain.com.
    postfixadmin.mydomain.com. 1697 IN CNAME smtp.mydomain.com.
    mailwatch.mydomain.com. 1697 IN CNAME smtp.mydomain.com.
    roundcube.mydomain.com. 1697 IN CNAME smtp.mydomain.com.
  • Reverse DNS PTR record: (Need ask my ISP to set this reverse DNS up for me)

    $ dig -x 11.22.33.44 +short
    smtp.mydomain.com.

CentOS installation

I installed CentOS 7.2 64bit which is my faverate OS for server platform. During the installation, I setup the static IP, time zone, and hard drive partitions.

I’ll save all the emails in /home/vmail later on, so make sure it has enough space for email storage.

Once I finished installation and reboot the server, I login in as root and start configration.

Disable SELinux

sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config

Install some packages

yum -y install net-tools nano wget man bind-utils git mailx telnet

Firewall

Centos 7 come with Firewalld as the firewall but I like to use iptables for firewall, so let’s disable the default installed firewalld.

systemctl stop firewalld
systemctl mask firewalld
systemctl disable firewalld

Install iptables:

yum -y install iptables-services
systemctl enable iptables
systemctl start iptables

Now install fail2ban:

yum install -y epel-release
yum install -y fail2ban jwhois

Create fail2ban local configration file:

cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edit this file to enable sshd jail. Set:

[sshd]
enabled = true
...

Now enable and start the fail2ban service:

systemctl start fail2ban.service
systemctl enable fail2ban.service

Now the ssh port 22 is protected by Fail2ban against brutal-force attack. In the future, anytime I need open up a new port for a service in iptables, I’ll also need to configure the corresponding fail2ban jail to have it protected.

There are lots scan activities on the well-known ssh port 22. So I changed the sshd to listen on a different port. Also configured iptables and fail2ban accordingly.

NTP

CentOS 7 use chronyd to keep the clock synchronized.
If timezone is not set, then:

timedatectl set-timezone America/Vancouver

Now install Chrony:

yum install -y chrony
systemctl enable chronyd
systemctl start chronyd

Use the following commands to check the status:

timedatectl
chronyc tracking
chronyc sources
chronyc sourcestats

Update system then reboot

Bring the system up to date:

yum -y update && reboot

After reboot, log in and check:

# sestatus
SELinux status: disabled

LEMP

MariaDB

Install MariaDB server:

yum install -y mariadb-server
systemctl enable mariadb.service
systemctl start mariadb.service

Secure MariaDB. Run this command, set the root password, and answer Y to all other questions:

mysql_secure_installation

Install Nginx

Add Nginx Repo by create a configration file /etc/yum.repos.d/nginx.repo with the following content:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=0
enabled=1

Install Nginx:

yum install nginx

Edit /etc/nginx/nginx.conf:

worker_processes 4;
...
gzip on;
server_tokens off;
...

Set worker_processes to the CPU core number.

Start nginx:

systemctl enable nginx.service
systemctl start nginx.service

Check to see Nginx is running on port 80:

# netstat -ntlp | grep :80
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 9901/nginx: master

Now open up port 80 and 443 on iptables. I can access http://smtp.mydomain.com for the nginx’s default page.

Install PHP5

PHP5 works in nginx through PHP-FPM. So install some php packages:

yum install php-fpm php-cli php-mysql php-gd php-mcrypt php-intl php-ldap php-odbc php-pdo php-pecl-memcache php-pear php-mbstring php-xml php-xmlrpc php-mbstring php-snmp php-soap php-imap

Edit /etc/php.ini , set

date.timezone = America/Vancouver
cgi.fix_pathinfo = 0

Edit /etc/php-fpm.d/www.conf, use socket instead of TCP port for better performance:

;listen = 127.0.0.1:9000
listen = /var/run/php-fpm/php-fpm.sock
user = nginx
group = nginx

Next enable and start php-fpm:

systemctl enable php-fpm.service
systemctl start php-fpm.service

Let’s Encrypt SSL Certificate

I use the free LE SSL Cert for all the virtual hosts and SMTP/IMAP/POP3

First make sure all DNS record are pointed to the correct IP. This includes all A records for @, www, and smtp.

yum install git bc
git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

Also make sure port 443 is open on iptables.

Obtain a certicate:

systemctl stop nginx
cd /opt/letsencrypt
./letsencrypt-auto certonly --standalone

Fill in my email address, then agree the agreement, then fill in all the domain names:

mydomain.com, www.mydomain.com,smtp.mydomain.com, mailwatch.mydomain.com, postfixadmin.mydomain.com roundcube.mydomain.com

output:

IMPORTANT NOTES:

  • Congratulations! Your certificate and chain have been saved at
    /etc/letsencrypt/live/mydomain.com/fullchain.pem. Your cert will
    expire on 2016-05-29. To obtain a new version of the certificate in
    the future, simply run Let’s Encrypt again.

Let’s Encript SSL Cert is only valid for 90 days. To renew it, set a cron job (or wrote a script). The command to renew is:

/root/.local/share/letsencrypt/bin/letsencrypt renew --agree-tos

To test this command, add --dry-run flag at the end.

Here is my simple bash script le_renew.sh:

#!/bin/bash

systemctl stop nginx
/root/.local/share/letsencrypt/bin/letsencrypt renew --agree-tos
systemctl start nginx

Tip:

To obtain LE SSL certificates in one liner command:

/opt/letsencrypt/letsencrypt-auto certonly --standalone --agree-tos --email myemail@domain.tld -d mydomain.com -d www.mydomain.com -d smtp.mydomain.com -d mailwatch.mydomain.com -d postfixadmin.mydomain.com -d smtp.mydomain.com -d roundcube.mydomain.com

Configure Virtual Host

I will have few web sites running on this server using named based virtual host. They are RoundCube webmail, Postfix Admin and MailWatch.

I need to create a 2048 bit DH params file(default is 1024 bit):

openssl dhparam -out /etc/nginx/dhparams.pem 2048

Now let’s configure the first virtual host postfixadmin.mydomain.com by create a new configration file /etc/nginx/conf.d/postfixadmin.conf with content:

server {

listen 80;
server_name postfixadmin.mydomain.com;
return 301 https://$server_name$request_uri; # enforce https

}

server {

listen 443 ssl;
server_name postfixadmin.mydomain.com;
root /var/www/html/postfixadmin;
index index.php;
charset utf-8;
access_log /var/log/nginx/pa-access.log;
error_log /var/log/nginx/pa-error.log;

## SSL settings
ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
ssl_dhparam /etc/nginx/dhparams.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ecdh_curve secp521r1;

add_header Strict-Transport-Security max-age=31536000;

location / {
try_files $uri $uri/ index.php;
}

location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
fastcgi_index index.php;
}

}

Let’s make a test page:

mkdir /var/www/html/postfixadmin
echo "<?php phpinfo(); ?>" > /var/www/html/postfixadmin/info.php

Restart Nginx and test by go to http://postfixadmin.mydomain.com/info.php, it should jump to https:// automatcally and show the PHP info page.

A good test for SSL can be done at SSL LABS. I got the highest score “A+“!

Repeat the above steps to create other virtual host sites(mailwatch, roundcube). Later we will build these sites in their doc root directory.

Now I have a basic LEMP server with SSL and firewall protections. I’m now ready to build the mail server on this system.


Quick links: