1.0 Introduction

This is the second in a series of six posts explaining how I set up an email server on CentOS 7. It will assume you’ve read the first article in the series and taken the steps described there.

In this article, we are going to install and configure the beating heart of most mail servers these days: Postfix.

Postfix is a Mail Transport Agent, or MTA. Its job is to sit listening on the various SMTP-related ports and to receive email from them and send email out via them. Installing it is easy; configuring it is not such a piece of cake! In this article, too, I’m going to use a MySQL database to store various bits of Postfix configuration data. (Because we’re fans of opensource, it will actually be a ‘MariaDB’ database, but I’m going to call it MySQL throughout because, for all intents and purposes, they are the same thing).

2.0 Installation

The standard version of Postfix that is shipped with CentOS does not support the use of MySQL backends, so the first thing to do is to enable the CentosPlus repository which has a version which does. Additionally, we need to make sure that the base CentOS repository doesn’t try and conflict with CentosPlus’ version. So, having logged on to your server as yourself, you will now become root and issue all that follows as the root user:

sudo -i 
nano /etc/yum.repos.d/CentOS-Base.repo

The repository configuration file has a lot of stuff in it. Your job is to add the line exclude=postfix to both the [base] and [updates] sections, immediately after the name= parameter line.

For example, my file ended up looking in part as follows:

[base]
name=CentOS-$releasever - Base
exclude=postfix
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#released updates
[updates]
name=CentOS-$releasever - Updates
exclude=postfix
mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates&infra=$infra
#baseurl=http://mirror.centos.org/centos/$releasever/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

I’ve marked the lines I added in bold. Save the file when done.

Now enable the CentosPlus repository and download all the required software, not just for this article but for some to come, too:

yum --enablerepo=centosplus -y install postfix
yum -y install dovecot mariadb-server dovecot-mysql

Funnily enough, that’s all you need to do to install Postfix. As I said: pretty straightforward. On to the configuration stages…

3. Configure MySQL

First things first: we need to configure MySQL (or MariaDB if you insist!) to restart automatically at every server reboot and to have a database (including tables) which Postfix can use. As root, therefore, issue these commands in sequence:

systemctl enable mariadb.service
systemctl start mariadb.service

And now you can secure the default installation (which is pretty open to abuse otherwise):

mysql_secure_installation

Press [Enter] when first prompted to supply root’s password: there isn’t one to supply as yet! Next, you’ll be asked if you want to set a root password: press [Enter] there (or [y] and [Enter]… it amounts to the same thing). Now make your new root password long, complex and memorable -and confirm it when prompted.

After that, just press [Enter] each time to accept the default option. At the end, you’ll get a ‘success’ message, and your database setup is now a little more secure than before.

Now it’s time to create a new database for Postfix’s use. Again as root, type:

mysql -u root -p

That is how you log on to MySQL not as root the O/S user, but as root the ‘database superuser’. The ‘-p’ switch means you will be prompted for the long, complex and memorable password you just set for that user. Assuming you typed it all correctly, you’ll be connected and sitting at the MySQL prompt. Now issue these SQL commands:

create database mail;
use mail;

grant select, insert, update, delete on mail.* to 'mail_admin'@'localhost' identified by 'berrimer1';
grant select, insert, update, delete on mail.* to 'mail_admin'@'localhost.localdomain' identified by 'berrimer1';
flush privileges;

That creates a database called “mail” and then grants all rights on that database to a user called “mail_admin”, who is simultaneously created with password “berrimer1” (please use something longer and more complex than that example password!)

Now you can create some tables:

create table domains (domain varchar(50), primary key (domain));
create table forwardings (source varchar(80), destination text not null, primary key (source));
create table users (email varchar(80), password varchar(20) not null, primary key (email));
create table transport (domain varchar(128) not null default '', transport varchar(128) not null default '', unique key domain (domain));
quit

Finally, finish up with a change to the MySQL global configuration that ensures that MySQL binds to the 127.0.0.1 address:

nano /etc/my.cnf

Add a bind-address as the first line under the [mysqld] section:

[mysqld]
bind-address=127.0.0.1

If you alter the global configuration, you then need to restart the database service to make sure it is picked up and applied:

systemctl restart mariadb.service

So that’s the MySQL part of the configuration process out of the way. We now need to start configuring Postfix itself.

4.0 Configure Postfix to work with MySQL

To begin with, we have to tell Postfix what queries it can issue against which database (and logged on as what user). There are four files in all that we have to create to achieve this. You can do them one by one if you like, but I’ve found myself making silly mistakes when trying that (the file names are quite similar!) So instead, I’m going to suggest pasting this lot in at the command line and having all four created semi-automatically:

cat >> /etc/postfix/mysql-virtual_domains.cf << XXX
user = mail_admin
password = berrimer1
dbname = mail
query = SELECT domain AS virtual FROM domains WHERE domain='%s'
hosts = 127.0.0.1
XXX

cat >> /etc/postfix/mysql-virtual_forwardings.cf << XXX
user = mail_admin
password = berrimer1
dbname = mail
query = SELECT destination FROM forwardings WHERE source='%s'
hosts = 127.0.0.1
XXX

cat >> /etc/postfix/mysql-virtual_mailboxes.cf << XXX
user = mail_admin
password = berrimer1
dbname = mail
query = SELECT CONCAT(SUBSTRING_INDEX(email,'@',-1),'/',SUBSTRING_INDEX(email,'@',1),'/') FROM users WHERE email='%s'
hosts = 127.0.0.1
XXX

cat >> /etc/postfix/mysql-virtual_email2email.cf << XXX
user = mail_admin
password = berrimer1
dbname = mail
query = SELECT email FROM users WHERE email='%s'
hosts = 127.0.0.1
XXX

chmod o= /etc/postfix/mysql-virtual_*.cf
chgrp postfix /etc/postfix/mysql-virtual_*.cf

groupadd -g 5000 vmail
useradd -g vmail -u 5000 vmail -d /home/vmail -m

The only bit you have to edit is marked in bold: you supply the same password you specified for the administrator of the mail database when creating it earlier.

5.0 Advanced Postfix Configuration

You now need to finish things off by issuing a whole lot of commands whose purpose is not entirely obvious and which takes weeks of reading the Postfix doco to work out. You can do that if you like, or you can just cut-and-paste this lot:

postconf -e 'myhostname = mail.dizwell.com'
postconf -e 'mydestination = localhost, localhost.localdomain'
postconf -e 'mynetworks = 127.0.0.0/8'
postconf -e 'inet_interfaces = all'
postconf -e 'message_size_limit = 30720000'
postconf -e 'virtual_alias_domains ='
postconf -e 'virtual_alias_maps = proxy:mysql:/etc/postfix/mysql-virtual_forwardings.cf, mysql:/etc/postfix/mysql-virtual_email2email.cf'
postconf -e 'virtual_mailbox_domains = proxy:mysql:/etc/postfix/mysql-virtual_domains.cf'
postconf -e 'virtual_mailbox_maps = proxy:mysql:/etc/postfix/mysql-virtual_mailboxes.cf'
postconf -e 'virtual_mailbox_base = /home/vmail'
postconf -e 'virtual_uid_maps = static:5000'
postconf -e 'virtual_gid_maps = static:5000'
postconf -e 'smtpd_sasl_type = dovecot'
postconf -e 'smtpd_sasl_path = private/auth'
postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'broken_sasl_auth_clients = yes'
postconf -e 'smtpd_sasl_authenticated_header = yes'
postconf -e 'smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination'
postconf -e 'smtpd_use_tls = yes'
postconf -e 'smtpd_tls_cert_file = /etc/pki/dovecot/certs/dovecot.pem'
postconf -e 'smtpd_tls_key_file = /etc/pki/dovecot/private/dovecot.pem'
postconf -e 'virtual_create_maildirsize = yes'
postconf -e 'virtual_maildir_extended = yes'
postconf -e 'proxy_read_maps = $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $virtual_mailbox_limit_maps'
postconf -e 'virtual_transport = dovecot'
postconf -e 'dovecot_destination_recipient_limit = 1'

I’ve bolded the one line in that lot which requires a bit of editing to suit specific circumstances before it can be submitted. To be clear, each ‘postconf’ line is a separate command that edits the Postfix configuration files and sets the variables to the values shown.

Now for my least favourite part of Postfix:

nano /etc/postfix/master.cf

The file is a ghastly mess and looks extremely complicated (because it is). So let’s take it one step at a time. First, scroll to the very end of the file and, on a new line, paste this in:

dovecot   unix  -       n       n       -       -       pipe
  flags=DRhu user=vmail:vmail argv=/usr/libexec/dovecot/deliver -f ${sender} -d ${recipient}

That is basically a configuration item that tells Dovecot (which we haven’t otherwise configured yet) how to do the mail delivery (after Postfix has done the mail receiving).

Now back to the harder stuff at the top of the file. Your job is basically to uncomment the “submission” and “smtps” lines, and some of their associated option lines. This basically tells Postfix to handle receipt and delivery of email via secure ports, rather than the basic, unencrypted smtp one that is already uncommented.

So, in a virgin master.cf file, that section looks like this:

#submission inet n       -       n       -       -       smtpd
#  -o syslog_name=postfix/submission
#  -o smtpd_tls_security_level=encrypt
#  -o smtpd_sasl_auth_enable=yes
#  -o smtpd_reject_unlisted_recipient=no
#  -o smtpd_client_restrictions=$mua_client_restrictions
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
#  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
#  -o milter_macro_daemon_name=ORIGINATING
#smtps     inet  n       -       n       -       -       smtpd
#  -o syslog_name=postfix/smtps
#  -o smtpd_tls_wrappermode=yes
#  -o smtpd_sasl_auth_enable=yes
#  -o smtpd_reject_unlisted_recipient=no
#  -o smtpd_client_restrictions=$mua_client_restrictions
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
#  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
#  -o milter_macro_daemon_name=ORIGINATING

You need it to look like this instead:

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
#  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
#  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
smtps     inet  n       -       n       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
#  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
#  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

Notice that you leave some of the option lines commented out; but five of them in each section get uncommented; the submission and smtps lines themselves are uncommented, too.

Once the file is edited and saved, you can restart Postfix so that it picks up the new configuration:

systemctl enable postfix.service
systemctl start postfix.service

6.0 Conclusion

And once again, that is a convenient point at which to end the second installment of this mini-series of articles. At this point, you have a secure server running MySQL and Postfix, and Postfix has been configured to use MySQL for some of its configuration data and told to handle secure mail transports as well as insecure ones. It has also been told to expect to work with Dovecot for mail delivery purposes… and it’s the installation and configuration of Dovecot that will be the subject of the third part of this series.