Mail Server with Postfix, Dovecot

Building a Postfix mail server on CentOS 7.

Steps

01. Server Configuration and design
02. Getting certificates for mai mail servers from Let's Encrypt
03. Installing MariaDB and creating the database
04. Installing Postfix 3.2
05. Configuring Postfix
      main.cf
      master.cf
      virtual directories
      virtual maps
06. Installing Dovecot
07. PostfixAdmin on srv4 (management server)
08. Roundcube on the management server (srv4)
09. clamav, amavisd-new, spamassassin
10. HAProxy in front for IMAP
11. spf, DKIM, DMARC
12. Fail2ban

01. Configuration and design

The entire system has one external gateway (srv33), three HAProxy servers (srv10, srv11, srv27), two Postfix relays (srv14 and srv15) and a mail server (srv31) where the emails are stored and from where they are accessed by the users.

Grafana Diagram SMTP

  • Only one HAProxy server is active (green), the other two in stand-by (red - no transaction flowing through them).
  • Mail server (for now only one) - srv31 (192.168.122.123) with postfix (MTA), dovecot (MDA, IMAP) and database
  • For managing postfix accounts - PostfixAdmin on srv4 (management server)
  • Webmail with Roundcube (also on srv4)

The mail server

CentOS 7 minimal
A dedicated drive /data for mail storage

Adding A and PTR records to DNS

srv31.mydomain.dom - 192.168.122.123

/etc/sysconfig/selinux - permissive

systemctl disable NetworkManager
systemctl disable firewalld
systemctl enable network.service
yum install iptables-services
yum install net-tools
yum install epel-release
yum update
yum install psmisc net-tools bind-utils

Configuring chrony

Configuring chrony and set it to start at boot

vi /etc/chrony.conf
server 192.168.122.151 iburst
systemctl enable chronyd
systemctl list-unit-files | grep chronyd
chronyd.service                               enabled 

02. Getting certificates for mail servers from Let's Encrypt

Will be using the DNS challenge with certbot.
Installing certbot on the mail server (srv31). Will get all the certificate here, then will distribute them to the other servers.

yum install certbot

Domains and names I need on the certificate

  • mail.mydomain.dom
  • mail1.mydomain.dom
  • mail2.mydomain.dom
  • mail3.mydomain.dom
  • mail4.mydomain.dom
  • mail5.mydomain.dom
certbot --cert-name mail.mydomain.dom \
    -d mail.mydomain.dom \
    -d mail1.mydomain.dom \
    -d mail2.mydomain.dom \
    -d mail3.mydomain.dom \
    -d mail4.mydomain.dom \
    -d mail5.mydomain.dom -\
    -agree-tos \
    --email me@mydomain.dom \
    --manual \
    --preferred-challenges dns certonly

Added the required TXT records to DNS then continued. In the end:

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/mail.mydomain.dom/fullchain.pem. Your cert
   will expire ...

Checking

cd /etc/letsencrypt/
ls -l
accounts
archive
csr
keys
live
renewal
cd live
ls -l
mail.mydomain.dom

cd mail.mydomain.dom/
mail.mydomain.dom]# ls -l
cert.pem -> ../../archive/mail.mydomain.dom/cert1.pem
chain.pem -> ../../archive/mail.mydomain.dom/chain1.pem
fullchain.pem -> ../../archive/mail.mydomain.dom/fullchain1.pem
privkey.pem -> ../../archive/mail.mydomain.dom/privkey1.pem
README

03. Installing MariaDB and creating the database

The database will be hosted on the mail server (srv31).
Adding the repo:

vi /etc/yum.repos.d/mariadb.repo
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.3.0/centos73-amd64/
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

Installing:

yum install MariaDB-server MariaDB-client

Then:

systemctl start mariadb && systemctl status mariadb
/usr/bin/mysql_secure_installation

Set root password? [Y/n] Y
New password: 
Re-enter new password: 
Password updated successfully!
Reloading privilege tables..
 ... Success!

Remove anonymous users? [Y/n] Y
 ... Success!

Disallow root login remotely? [Y/n] Y
 ... Success!

Remove test database and access to it? [Y/n] Y
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reload privilege tables now? [Y/n] Y
 ... Success!

Cleaning up...

Thanks for using MariaDB!

Creating database mail_db:

CREATE DATABASE mail_db;
CREATE USER "mail_user"@"localhost" IDENTIFIED BY "userpass";
GRANT ALL PRIVILEGES ON mail_db.* TO "mail_user"@"localhost";
FLUSH PRIVILEGES;
exit

Enabling remote access

mysql -u root -p
Enter password: 
...

MariaDB [(none)]> GRANT ALL ON *.* TO 'root'@'%' IDENTIFIED BY 'root_pass';
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> exit
Bye

04. Installing Postfix 3.2

Creating yum repo for Postfix 3.2 on the mail server (srv31)

cd /etc/yum.repos.d/
vi postfix.repo
[postfixrepo]
name=EL-$releasever Postfix repo
baseurl=http://repos.oostergo.net/7/postfix-3.2/
enabled=1

Excluding postfix from centos base repo

vi /etc/yum.repos.d/CentOS-Base.repo
[base]
name=CentOS-$releasever - Base
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
exclude=postfix-* 

#released updates 
[updates]
name=CentOS-$releasever - Updates
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
exclude=postfix-* 

Importing the repo key

rpm --import http://repos.oostergo.net/RPM-GPG-KEY

Then installing:

yum update
Dependency Installed:
libicu.x86_64 0:50.1.2-15.el7
Updated:
postfix.x86_64 2:3.2.2-1.el7.centos    
cd /etc/postfix/
ls -l

access
canonical
generic
header_checks
main.cf
master.cf
relocated
transport
virtual

cp main.cf main_cf_orig
cp master.cf master_cf_orig

05. Configuring Postfix

Adjusting postfix configuration files on the mail server (srv31)

main.cf

Path: /etc/postfix/main.cf

My custom configs:

##### My Configs #####

myhostname = mail.mydomain.dom
inet_interfaces = all
inet_protocols = IPv4
mynetworks_style = subnet
mynetworks = 192.168.121.0/24 192.168.122.0/24 192.168.123.0/24 127.0.0.0/8
relayhost = [provider.relay.host]:25

dovecot_destination_recipient_limit = 1
mailbox_size_limit = 0
message_size_limit = 10485760
biff = no
masquerade_domains = $mydomain

recipient_delimiter = +

# virtual maps and mail directories
virtual_alias_maps = mysql:/etc/postfix/mysql_virtual_alias_maps.cf
virtual_gid_maps = static:102
virtual_mailbox_base = /data/mail/
virtual_mailbox_domains = mysql:/etc/postfix/mysql_virtual_domains_maps.cf
virtual_mailbox_limit = 51200000
virtual_mailbox_maps = mysql:/etc/postfix/mysql_virtual_mailbox_maps.cf
virtual_minimum_uid = 102
virtual_transport = dovecot
virtual_uid_maps = static:102

smtpd_helo_restrictions = permit_mynetworks,
 reject_non_fqdn_hostname,
 reject_invalid_hostname,
 permit

smtpd_recipient_restrictions =
  permit_mynetworks,
  permit_sasl_authenticated,
  reject_non_fqdn_hostname,
  reject_non_fqdn_sender,
  reject_non_fqdn_recipient,
  reject_unauth_destination,
  reject_unauth_pipelining,
  reject_invalid_hostname,
  reject_rbl_client zen.spamhaus.org,
  permit

smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous

smtpd_sasl_local_domain = $myhostname
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
broken_sasl_auth_clients = yes

# TLS
smtpd_tls_exclude_ciphers = SSLv2, aNULL, ADH, eNULL
smtpd_tls_security_level = may
smtp_use_tls = yes 
smtpd_enforce_tls = no
smtpd_tls_loglevel = 1
smtpd_use_tls = yes
smtpd_tls_key_file = /etc/letsencrypt/live/mail.mydomain.dom/privkey.pem
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.mydomain.dom/fullchain.pem
smtpd_tls_received_header = yes
smtp_tls_note_starttls_offer = yes

#
smtpd_helo_required = yes
disable_vrfy_command = yes
smtpd_data_restrictions = reject_unauth_pipelining
smtpd_etrn_restrictions = reject
tls_random_source = dev:/dev/urandom
data_directory = /var/lib/postfix

# for amavisd-new
content_filter = amavis:[127.0.0.1]:10024
strict_rfc821_envelopes = yes

# DKIM 
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891

Info:
The DROWN Attack

Added the ciphers exclusion:

smtp_tls_exclude_ciphers =
        EXPORT, LOW, MD5,
        aDSS, kECDHe, kECDHr, kDHd, kDHr,
        SEED, IDEA, RC2
smtpd_tls_exclude_ciphers = SSLv2, SSLv3, aNULL, ADH, eNULL, EXPORT, LOW, MD5, SEED, IDEA, RC2

master.cf

In /etc/postfix/master.cf adding:

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

And adding the password for sasl:

vi /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd

Configuring Virtual Directories for Postfix

All mail will be stored in /data.

Creating the user:

Creating user vmail with group vmail:

groupadd -g 102 vmail
useradd -u 102 -g 102 -s /bin/false -d /var/empty vmail

Creating a directory for storing the email messages

mkdir /data/mail
chown -R vmail:postfix /data/mail
chown -R vmail:postfix /data/mail/

Virtual maps for Postfix

In /etc/postfix:

mysql_virtual_alias_maps.cf

user = mail_user
password = userpass
hosts = 127.0.0.1
dbname = mail_db
query = SELECT goto FROM alias WHERE address='%s' AND active = 1

mysql_virtual_domains_maps.cf

user = mail_user
password = userpass
hosts = 127.0.0.1
dbname = mail_db
query = SELECT domain FROM domain WHERE domain='%s'

mysql_virtual_mailbox_maps.cf

user = mail_user
password = userpass
hosts = 127.0.0.1
dbname = mail_db
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = 1

06. Installing Dovecot

The IMAP flow:

Grafana Diagram IMAP

srv27 is the master in keepalived for IMAP.

Installing

A very good mirror, with updated versions for centos, here: http://au1.mirror.crc.id.au/repo/el7-extra/x86_64/

Creating a repo using this link:

/etc/yum.repos.d/dovecot_crc_id_au.repo

[dovecot_crc_id_au]
name=EL-$releasever Dovecot repo
baseurl=http://au1.mirror.crc.id.au/repo/el7-extra/x86_64/
enabled=0
gpgcheck=0

Installing

yum install dovecot --enablerepo dovecot_crc_id_au 

Installed:
dovecot.x86_64 1:2.2.31-1.el7
Dependency Installed:
clucene-core.x86_64 0:2.3.3.4-11.el7 

Changes in the configuration files for dovecot:

/etc/dovecot/dovecot-sql.conf.ext

driver = mysql
connect = host=localhost dbname=mail_db user=mail_user password=userpass

password_query = SELECT username as user, password, \
 '/data/mail/%d/%n' as userdb_home, \
 'maildir:/data/mail/%d/%n' as userdb_mail, \
 102 as userdb_uid, 102 as userdb_gid FROM mailbox \
 WHERE username = '%u' AND active = '1'

user_query = SELECT '/data/mail/%d/%n' as home, \
  'maildir:/data/mail/%d/%n' as mail, 102 AS uid, \
  102 AS gid, concat("*:storage=", quota) AS quota \
  FROM mailbox WHERE username = "%u" AND active = "1"

/etc/dovecot/conf.d/10-ssl.conf

ssl = yes

ssl_cert = </etc/letsencrypt/live/mail.mydomain.dom/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.mydomain.dom/privkey.pem

/etc/dovecot/conf.d/10-master.conf

haproxy_timeout = 5 secs
haproxy_trusted_networks = 192.168.121.0/24 192.168.122.0/24 192.168.123.0/24

service imap-login {
  inet_listener imap {
    #port = 143
  }
  inet_listener imaps {
    #port = 993
    #ssl = yes
  }

  inet_listener imap_haproxy {
      port = 10143
      haproxy = yes
  }
  inet_listener imaps_haproxy {
      port = 10993
      ssl = yes
      haproxy = yes
  }

service auth {

  unix_listener auth-userdb {
    mode = 0660
    user = vmail
  }

  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
  mode = 0660
  user = postfix
  group = postfix
  }

  # Auth process is run as this user.
  #user = $default_internal_user
}

/etc/dovecot/conf.d/10-auth.conf

disable_plaintext_auth = no

#!include auth-system.conf.ext
!include auth-sql.conf.ext
#!include auth-ldap.conf.ext
#!include auth-passwdfile.conf.ext
#!include auth-checkpassword.conf.ext
#!include auth-vpopmail.conf.ext
#!include auth-static.conf.ext

/etc/dovecot/conf.d/10-logging.conf

log_path = /var/log/dovecot.log 

Installing dovecot-mysql:

yum install dovecot-mysql

/etc/dovecot/conf.d/auth-sql.conf

passdb {
  driver = sql

  # Path for SQL configuration file, see example-config/dovecot-sql.conf.ext
  args = /etc/dovecot/dovecot-sql.conf.ext
}

# "prefetch" user database means that the passdb already provided the
# needed information and there's no need to do a separate userdb lookup.
# <doc/wiki/UserDatabase.Prefetch.txt>

userdb {
  driver = prefetch
}

userdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
}

The namespace setting is now controlled in two config files:

/etc/dovecot/conf.d/10-mail.conf

mail_location = maildir:/data/mail/%d/%u

mail_uid = 102
mail_gid = 102

first_valid_uid = 102
last_valid_uid = 105

/etc/dovecot/conf.d/15-mailboxes.conf

# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf.
namespace inbox {
  # These mailboxes are widely used and could perhaps be created automatically:
  mailbox Drafts {
    special_use = \Drafts
  }
  mailbox Junk {
    special_use = \Junk
  }
  mailbox Trash {
    special_use = \Trash
  }

  # For \Sent mailboxes there are two widely used names. We'll mark both of
  # them as \Sent. User typically deletes one of them if duplicates are created.
  mailbox Sent {
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }

  # If you have a virtual "All messages" mailbox:
  #mailbox virtual/All {
  #  special_use = \All
  #  comment = All my messages
  #}

  # If you have a virtual "Flagged" mailbox:
  #mailbox virtual/Flagged {
  #  special_use = \Flagged
  #  comment = All my flagged messages
  #}
}

Setting dovecot to listen on only ipv4:

In /etc/dovecot/dovecot.conf:

listen = *

Then:

systemctl start dovecot && systemctl status dovecot -l
systemctl restart dovecot && systemctl status dovecot -l
tail -f -n 100 /var/log/messages 
tail -f -n 100 /var/log/dovecot.log 
systemctl enable dovecot 

07. PostfixAdmin on srv4 (management server)

Installing postfixadmin on the management server (srv4)

Apache is already installed and running on the management server (srv4).
Downloading postfixadmin to /var/www/html and using it from there.

cd /var/www/html
wget https://sourceforge.net/projects/postfixadmin/files/postfixadmin/postfixadmin-3.1/postfixadmin-3.1.tar.gz
tar -xzf postfixadmin-3.1.tar.gz
ln -s postfixadmin-3.1 postfixadmin

Configuring the database settings
The database is on the mail server (srv31 - 192.168.122.123).

// Database Config
// mysql = MySQL 3.23 and 4.0, 4.1 or 5
// mysqli = MySQL 4.1+ or MariaDB
// pgsql = PostgreSQL
// sqlite = SQLite 3
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = '192.168.122.123';
$CONF['database_user'] = 'mail_user';
$CONF['database_password'] = 'userpass';
$CONF['database_name'] = 'mail_db';
systemctl restart httpd && systemctl status httpd

Creating the conf file /etc/httpd/conf.d/postfixadmin.conf

<VirtualHost *:80>
      DocumentRoot /var/www/html/postfixadmin
      ServerName postfixadmin.mydomain.dom
      ServerAlias postfixadmin
      ServerAdmin me@mydomain.dom
      ErrorLog /var/log/httpd/postfixadmin_error.log
      TransferLog /var/log/httpd/postfixadmin_access.log

      <Directory />
          Options +Indexes +FollowSymLinks
          DirectoryIndex index.html index.php
          AllowOverride All
          Order allow,deny
          Allow from all
          Require all granted
      </Directory>
</VirtualHost>

Adding a DNS CNAME

postfixadmin            IN      CNAME   srv4

Then

ERROR: the templates_c directory doesn't exist or isn't writeable for the webserver
mkdir templates_c
chown -R apache:apache /var/www/html/postfixadmin
chmod -R 775 /var/www/html/postfixadmin
systemctl restart httpd && systemctl status httpd

(https://www.reddit.com/r/postfix/comments/4soepj/postfixadmin_install_templates_c_not_writeable/)

creating config.local.php in /var/www/html/postfixadmin

cp config.inc.php config.local.php
chown -R apache:apache *
chmod -R 755 *
systemctl restart httpd && systemctl status httpd

Installing php-imap

yum install php-imap
systemctl restart httpd && systemctl status httpd

Accessing the site - http://postfixadmin.mydomain.dom/setup.php and doing the initial passwords;

postfixadmin
change the setup password:
password

Create superadmin account
Admin: me@mydomain.dom
Password: someotherpass

08. Roundcube on the management server (srv4)

Getting Roundcube:

cd /var/www/html
wget https://github.com/roundcube/roundcubemail/releases/download/1.3.0/roundcubemail-1.3.0-complete.tar.gz
ln -s roundcubemail-1.3.0 webmail

Creating the database for roundcube:

mysql -u root -p
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 85647
Server version: 10.1.20-MariaDB MariaDB Server

Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE roundcubemail /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
Query OK, 1 row affected (0.02 sec)

MariaDB [(none)]> GRANT ALL PRIVILEGES ON roundcubemail.* TO roundcube@localhost IDENTIFIED BY 'somepassword';
Query OK, 0 rows affected (0.07 sec)

MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.04 sec)

MariaDB [(none)]> quit
Bye

Importing schema:

mysql -u root -p roundcubemail < SQL/mysql.initial.sql
Enter password: 

Installing it by accessing the site - http://webmail.mydomain.dom/installer

Setting up the timezone

vi /etc/php.ini
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
;date.timezone =
date.timezone = America/Toronto

09. clamav, amavisd-new, spamassassin

Installing them

yum install clamav amavisd-new clamav-update clamav-scanner-systemd spamassassin

clamav config

rpm -ql clamav-scanner
/etc/clamd.d/scan.conf
/usr/lib/tmpfiles.d/clamd.scan.conf
/var/run/clamd.scan
/var/run/clamd.scan/clamd.sock       

Configuring clamd

/etc/clamd.d/scan.conf

LogFile /var/log/clamd.scan
LogFileMaxSize 2M
LogTime yes

LogSyslog no
PidFile /var/run/clamd.scan/clamd.pid
TemporaryDirectory /var/tmp
DatabaseDirectory /var/lib/clamav
LocalSocket /var/run/clamd.scan/clamd.sock
FixStaleSocket yes
TCPSocket 3310
TCPAddr 127.0.0.1
MaxConnectionQueueLength 30
systemctl start clamd@scan
systemctl enable clamd@scan

Configuring freshclam

vi /etc/freshclam.conf
DatabaseDirectory /var/lib/clamav
UpdateLogFile /var/log/freshclam.log
# Use system logger (can work together with UpdateLogFile).
# Default: no

LogSyslog no

HTTPProxyServer 192.168.122.124
HTTPProxyPort 3128

Testing:

clamdscan --fdpass /var/log/*
ERROR: Can't parse clamd configuration file /etc/clamd.conf

so I do this:

cp /etc/clamd.d/scan.conf /etc/clamd.conf
clamdscan --fdpass /var/log/*
/var/log/anaconda: OK
/var/log/audit: OK
/var/log/boot.log: OK
/var/log/btmp: OK
/var/log/chrony: OK
/var/log/clamd.scan: OK
/var/log/cron: OK
/var/log/cron-20170806: OK
/var/log/dmesg: OK
/var/log/dmesg.old: OK
/var/log/dovecot_debug.log: OK
/var/log/dovecot.log: OK
/var/log/fail2ban.log: OK
/var/log/firewalld: OK
/var/log/freshclam.log: OK
/var/log/grubby: OK
/var/log/grubby_prune_debug: OK
/var/log/lastlog: OK
/var/log/letsencrypt: OK
/var/log/maillog: OK
/var/log/maillog-20170806: OK
/var/log/messages: OK
/var/log/messages-20170806: OK
/var/log/ppp: OK
/var/log/secure: OK
/var/log/secure-20170806: OK
/var/log/spooler: OK
/var/log/spooler-20170806: OK
/var/log/tallylog: OK
/var/log/tuned: OK
/var/log/wpa_supplicant.log: OK
/var/log/wtmp: OK
/var/log/yum.log: OK

----------- SCAN SUMMARY -----------
Infected files: 0
Time: 0.993 sec (0 m 0 s)

Some info:

When installing ClamAV from EPEL to CentOS/Red Hat 7 becomes a nightmare…
How to Install ClamAV on CentOS 7
How to Install ClamAV on CentOS 7
Installing ClamAV on CentOS 7 and Using Freshclam

Configuring amavisd

/etc/amavisd/amavisd.conf

$mydomain = 'mydomain.dom';   # a convenient default for other settings
$do_syslog = 0;              # log via syslogd (preferred)

$LOGFILE = "/var/log/amavis/amavis.log";
@local_domains_maps = ( [".$mydomain"],".mydomain.dom",[".mydomain.org"],".mydomain.org",[".mydomain2.org"],".mydomain2.org",[".mydomain3.org"],".mydomain3.org" );  # list of all local domains

$myhostname = 'mail.mydomain.dom';

$virus_admin               = "alerts\@mydomain.dom";                    # notifications recip.

$mailfrom_notify_admin     = "alerts\@mydomain.dom";                    # notifications sender
$mailfrom_notify_recip     = "alerts\@mydomain.dom";                    # notifications sender
$mailfrom_notify_spamadmin = "alerts\@mydomain.dom";                    # notifications sender
$mailfrom_to_quarantine = ''; # null return path; uses original sender if undef
mkdir /var/log/amavis
touch /var/log/amavis/amavis.log
chown -R amavis:amavis /var/log/amavis

Adding the setings for amavisd to the master.cf configuration file for postfix

/etc/postfix/master.cf

127.0.0.1:10025 inet n - n - - smtpd
  -o content_filter=
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o smtpd_restriction_classes=
  -o mynetworks=127.0.0.0/8,192.168.1.0/24,10.0.2.0/24,192.168.121.0/24,192.168.122.0/24,192.168.123.0/24
  -o smtpd_error_sleep_time=0
  -o smtpd_soft_error_limit=1001
  -o local_recipient_maps=
  -o relay_recipient_maps=
  -o strict_rfc821_envelopes=yes
  -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks


amavis unix - - n - 2 smtp
  -o smtp_data_done_timeout=1200
  -o smtp_never_send_ehlo=yes
  -o disable_dns_lookups=yes
  -o smtp_send_xforward_command=yes
  -o fallback_relay=localhost:10025

and to the main.cf

/etc/postfix/main.cf

# for amavisd-new
content_filter = amavis:[127.0.0.1]:10024
strict_rfc821_envelopes = yes

Then enabling them for boot and start them

systemctl enable amavisd
systemctl enable spamassassin
systemctl stop postfix
systemctl start amavisd
systemctl start spamassassin
systemctl start postfix

Testing:

telnet localhost 10024
Trying ::1...
Connected to localhost.
Escape character is '^]'.
220 [::1] ESMTP amavisd-new service ready
ehlo localhost
250-[::1]
250-VRFY
250-PIPELINING
250-SIZE
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-SMTPUTF8
250-DSN
250 XFORWARD NAME ADDR PORT PROTO HELO IDENT SOURCE
quit
221 2.0.0 [::1] amavisd-new closing transmission channel
Connection closed by foreign host.
telnet localhost 10025
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 mail.mydomain.dom ESMTP Postfix
ehlo localhost
250-mail.mydomain.dom
250-PIPELINING
250-SIZE 10485760
250-ETRN
250-STARTTLS
250-AUTH PLAIN
250-AUTH=PLAIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 SMTPUTF8
quit
221 2.0.0 Bye
Connection closed by foreign host.

Moving the logs out of syslog

/etc/rsyslog.conf

:programname, contains, "amavis"       /var/log/amavis/amavis.log
:programname, contains, "clamd"            /var/log/clamd.scan 

Some info:

Amavisd-new, ClamAV and SpamAssassin
Centos 7 Postfix + Amavisd-new + ClamAV + Spamassassin
Setup amavisd-new with spamassassin and clamav with postfix
Amavisd-new, ClamAV and SpamAssassin with Postfix on CentOS 6.7 Server

10. HAProxy in front for IMAP

Adding in /etc/dovecot/conf.d/10-master-conf on srv31 for dovecot:

haproxy_timeout = 5 secs
haproxy_trusted_networks = 192.168.121.0/24 192.168.122.0/24 192.168.123.0/24

service imap-login {
  inet_listener imap {
    #port = 143
  }
  inet_listener imaps {
    #port = 993
    #ssl = yes
  }

  inet_listener imap_haproxy {
      port = 10143
      haproxy = yes
  }
  inet_listener imaps_haproxy {
      port = 10993
      ssl = yes
      haproxy = yes
  }

Adding in keepalived.conf:

on srv27

vrrp_instance imap {
    state MASTER
    interface ens160
    virtual_router_id 54
    priority 101
    advert_int 1

    virtual_ipaddress {
        192.168.122.122
    }

    track_script {
       check_haproxy
   }
}

on srv10:

vrrp_instance imap {
    state BACKUP
    interface ens160
    virtual_router_id 54
    priority 100
    advert_int 1

    virtual_ipaddress {
        192.168.122.122
    }

    track_script {
       check_haproxy
   }
}

on srv11:

vrrp_instance imap {
    state BACKUP
    interface ens160
    virtual_router_id 54
    priority 99
    advert_int 1

    virtual_ipaddress {
        192.168.122.122
    }

    track_script {
       check_haproxy
   }
}

Adding in haproxy.cfg:

on srv27:

#---------------------------------------------------------------------
# Dovecot IPMAS
#---------------------------------------------------------------------
listen srv27-imaps 192.168.122.122:993
   timeout client 1m
   no option http-server-close
   option log-health-checks
   stick store-request src
   stick-table type ip size 200k expire 30m
   mode tcp
   option tcplog
   server srv31 192.168.122.123:10993 check send-proxy-v2 inter 1900s

on srv10:

#---------------------------------------------------------------------
# Dovecot IPMAS
#---------------------------------------------------------------------
listen srv10-imaps 192.168.122.122:993
   timeout client 1m
   no option http-server-close
   option log-health-checks
   stick store-request src
   stick-table type ip size 200k expire 30m
   mode tcp
   option tcplog
   server srv31 192.168.122.123:10993 check send-proxy-v2 inter 1900s

on srv11:

#---------------------------------------------------------------------
# Dovecot IPMAS
#---------------------------------------------------------------------
listen srv11-imaps 192.168.122.122:993
   timeout client 1m
   no option http-server-close
   option log-health-checks
   stick store-request src
   stick-table type ip size 200k expire 30m
   mode tcp
   option tcplog
   server srv31 192.168.122.123:10993 check send-proxy-v2 inter 1900s

Guiding from here: Postfix Dovecot with HAproxy and other good info: Dovecot SSL Cert Problem.
Official docs: HAProxy Support

11. spf, DKIM, DMARC

spf DNS records

Created with spfwizard.net

domain1.dom. IN TXT “v=spf1 mx a ip4:123.123.123.123 a:some.provider.com -all”
domain2.dom. IN TXT “v=spf1 mx a ip4:123.123.123.123 a:some.provider.com -all”
domain3.dom. IN TXT “v=spf1 mx a ip4:123.123.123.123 a:some.provider.com -all”

Installing and configuring opendkim

yum install opendkim  
systemctl start opendkim && systemctl status opendkim -l 
systemctl enable opendkim

Creating the keys

cd /etc/opendkim/keys

opendkim-genkey --append-domain --bits=2048 --domain domain1.dom --selector=default
mv default.private domain1.dom.default.private
mv default.txt domain1.dom.default.txt
chown opendkim:opendkim domain1.dom.default.private

opendkim-genkey --append-domain --bits=2048 --domain domain2.dom --selector=default
mv default.private domain2.dom.default.private
mv default.txt domain2.dom.default.txt
chown opendkim:opendkim domain2.dom.default.private

opendkim-genkey --append-domain --bits=2048 --domain domain3.dom --selector=default
mv default.private domain3.dom.default.private
mv default.txt domain3.dom.default.txt
chown opendkim:opendkim domain3.dom.default.private

Configuring opendkim

/etc/opendkim.conf

Mode    sv
Syslog  yes
SyslogSuccess   yes
LogWhy  yes
UserID  opendkim:opendkim
Socket  inet:8891@localhost
Umask   002
SoftwareHeader  yes
Canonicalization       relaxed/simple
MinimumKeyBits  1024
KeyTable    /etc/opendkim/KeyTable
SigningTable  refile:/etc/opendkim/SigningTable
ExternalIgnoreList    refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
OversignHeaders From
QueryCache    yes

KeyTable

/etc/opendkim/KeyTable

default._domainkey.domain1.dom domain1.dom:default:/etc/opendkim/keys/domain1.dom.default.private
default._domainkey.domain2.dom domain2.dom:default:/etc/opendkim/keys/domain2.dom.default.private
default._domainkey.domain3.dom domain3.dom:default:/etc/opendkim/keys/domain2.dom.default.private

SigningTable

/etc/opendkim/SigningTable

*@domain1.dom default._domainkey.domain1.dom
*@domain2.dom   default._domainkey.domain2.dom
*@domain3.dom default._domainkey.@domain3.dom

TrustedHosts

/etc/opendkim/TrustedHosts

192.168.122.0/24
123.123.123.123

Connecting with Postfix

Adding to main.cf:

/etc/postfix/main.cf

# DKIM  
smtpd_milters = inet:localhost:8891
non_smtpd_milters       = $smtpd_milters
milter_default_action   = accept

DMARC and pypolicyd-spf

Creating DNS DMARC records using the DMARC Wizard from here: https://www.unlocktheinbox.com/dmarcwizard/

_dmarc.domain1.dom. IN TXT “v=DMARC1; p=reject; sp=reject; rua=mailto:postmaster@domain1.dom; ruf=mailto:postmaster@domain1.dom; rf=afrf; pct=100; ri=86400”
_dmarc.domain2.dom. IN TXT “v=DMARC1; p=reject; sp=reject; rua=mailto:postmaster@domain2.dom; ruf=mailto:postmaster@domain2.dom; rf=afrf; pct=100; ri=86400”
_dmarc.domain3.dom. IN TXT “v=DMARC1; p=reject; sp=reject; rua=mailto:postmaster@domain3.dom; ruf=mailto:postmaster@domain3.dom; rf=afrf; pct=100; ri=86400”

Installing pypolicyd-spf and opendmarc

yum install pypolicyd-spf opendmarc

Configuring pypolicyd-spf

Not rejecting, just adding a header:

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

#  For a fully commented sample config file see policyd-spf.conf.commented

defaultSeedOnly = 0

HELO_reject = False
Mail_From_reject = False

PermError_reject = False
TempError_Defer = False

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

Adding policyd-spf to postfix:

/etc/postfix/master.cf

policy-spf unix  -      n       n        -       -      spawn
   user=nobody argv=/usr/bin/python /usr/libexec/postfix/policyd-spf

/etc/postfix/main.cf

policyd-spf_time_limit = 3600

smtpd_recipient_restrictions =
  permit_mynetworks,
  reject_non_fqdn_hostname,  
  reject_non_fqdn_sender,  
  reject_non_fqdn_recipient,
  reject_unauth_destination,
  reject_unauth_pipelining, 
  reject_invalid_hostname, 
  reject_rbl_client zen.spamhaus.org
  reject_rbl_client cbl.abuseat.org,
  reject_rbl_client dul.dnsbl.sorbs.net,
  check_policy_service unix:private/policyd-spf,
  permit

Configuring opendmarc

/etc/opendmarc.conf

AuthservID mail3.marthemed.org

IgnoreHosts /etc/opendmarc/ignore.hosts

/etc/opendmarc/ignore.hosts

localhost
127.0.0.1
123.123.123.123

Adding a Public Suffix List

/etc/opendmarc.conf

PublicSuffixList /etc/opendmarc/effective_tld_names.dat
cd /etc/opendmarc

/usr/bin/wget --no-check-certificate -q -N -P /etc/opendmarc https://publicsuffix.org/list/effective_tld_names.dat
chown opendmarc:opendmarc /etc/opendmarc/effective_tld_names.dat 

Coonecting with postfix

/etc/postfix/main.cf

# DMARC
smtpd_milters           = inet:localhost:8891, inet:127.0.0.1:8893
non_smtpd_milters       = $smtpd_milters
milter_default_action   = accept
systemctl enable opendmarc
systemctl start opendmarc.service && systemctl status opendmarc.service -l
systemctl restart opendmarc.service && systemctl status opendmarc.service -l

systemctl restart postfix && systemctl status postfix -l

Some Info

DomainKeys Identified Mail (DKIM) Signatures
OpenDMARC
Installing OpenDMARC RPM via Yum with Postfix or Sendmail (for RHEL / CentOS / Fedora)
Postfix with SPF, DKIM and DMARC
Getting DKIM, DMARC and SPF to work with Postfix, OpenDKIM and OpenDMARC
Securing Your Postfix Mail Server with Greylisting, SPF, DKIM and DMARC and TLS
Configure SPF and DKIM With Postfix on Debian 8

12. Fail2ban

Installing fail2ban

yum install fail2ban

Adding to /etc/fail2ban/jail.d/local.conf

# ===========
# for postfix
# ===========

[postfix-postscreen]
enabled         = false
port            = smtp,465,submission
filter          = postfix-postscreen
logpath         = /var/log/maillog
action          = iptables-multiport[name=postfix-postscreen, port="smtp,smtps", protocol=tcp]
                  sendmail[name=postfix-postscreen, dest=admin@email.em]
bantime         = 3600
maxretry        = 5

[postfix-blocklist_de]
enabled         = true
port            = smtp,465,submission
filter          = postfix-blocklist_de
logpath         = /var/log/maillog
action          = iptables-multiport-log[name=postfix-blocklist_de, port="smtp,smtps", protocol=tcp]
                  sendmail[name=postfix-blocklist_de, dest=admin@email.em]
bantime         = 172800
maxretry        = 1


[postfix-spamhaus_org]
enabled         = true
port            = smtp,465,submission
filter          = postfix-spamhaus_org
logpath         = /var/log/maillog
action          = iptables-multiport-log[name=postfix-spamhaus_org, port="smtp,smtps", protocol=tcp]
                  sendmail[name=postfix-spamhaus_org, dest=admin@email.em]
bantime         = 172800
maxretry        = 1