Virtual Mail Server

How to establish a complete virtual user mail system




Postfix will be our MTA (mail transfer agent) to send and recieve encrypted mail. Virtual mail users will be managed with PostfixAdmin, a web interface for Postfix. An unlimited number of domains and domain (specific) user names may be managed and will be stored in a SQL database. Dovecot will be our MDA (mail delivery agent) to access email via secure IMAP from Roundcube, a web-based MUA (mail user agent) or an desktop MUA like Evolution. All encrypted mail communications will be secured with a TLS certificate. SPF (Sender Policy Framework) will ensure that the only verified servers/IP addresses may send mail from a given domain and DKIM (DomainKeys Identified Mail) will sign all outgoing messages with verification keys. This measures prevent our outgoing mail ending up in the junk box or our server being blacklisted for spam entirely. DMARC (Domain-based Message Authentication, Reporting and Conformance) ensures that both DKIM and SPF are properly enforced. Amavis, Spam-assassin will fiter messages for SPAM and ClamAV will be used for virus protection.

Postfix
│ └── PostfixAdmin
├── Dovecot
├──┬── SPF
│  ├── DKIM
│  └── DMARC
├── Amavis
│    ├── SpamAssassin
│    └── ClamAV
└── Roundcube

Postfix

Install Postfix and MariaDB packages.

pacman -Syu postfix mariadb postfix-mysql ca-certificates

Configure Postfix Uncomment and add or modify default Postfix settings.

/etc/postfix/main.cf
mail_owner = postfix
myhostname = mail.wildw1ng.com
mydomain = wildw1ng.com
myorigin = $mydomain
inet_interfaces = all
mydestination = $myhostname, localhost.$mydomain, localhost
mynetworks = 10.0.0.0/22, 127.0.0.0/8
relayhost =
alias_maps = hash:/etc/postfix/aliases
alias_database = $alias_maps
home_mailbox = Maildir/
smtpd_banner = $myhostname ESMTP $mail_name (Arch Linux)
inet_protocols = ipv4
append_dot_mydomain = no
mailbox_size_limit = 0

relay_domains = $mydestination
virtual_alias_maps = proxy:mysql:/etc/postfix/virtual_alias_maps.cf,proxy:mysql:/etc/postfix/virtual_alias_domains_maps.cf
virtual_alias_domains = proxy:mysql:/etc/postfix/virtual_alias_domains.cf
virtual_mailbox_domains = proxy:mysql:/etc/postfix/virtual_mailbox_domains.cf
virtual_mailbox_maps = proxy:mysql:/etc/postfix/virtual_mailbox_maps.cf
virtual_mailbox_base = /home/vmail
virtual_mailbox_limit = 512000000
virtual_minimum_uid = 5000
virtual_transport = virtual
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
local_transport = virtual
local_recipient_maps = $virtual_mailbox_maps
transport_maps = hash:/etc/postfix/transport

# Secure SMTP (receiving)
smtpd_tls_security_level = may
smtpd_use_tls = yes
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.wildw1ng.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.wildw1ng.com/privkey.pem
smtpd_tls_CApath = /etc/ssl/certs
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = /var/run/dovecot/auth-client
smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, check_policy_service unix:private/policy-spf
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
smtpd_sasl_security_options = noanonymous
smtpd_sasl_tls_security_options = $smtpd_sasl_security_options
smtpd_tls_auth_only = yes
smtpd_tls_received_header = yes
smtpd_sasl_local_domain = $mydomain
smtpd_tls_loglevel = 1

# Enable SASL authentication
smtp_sasl_auth_enable = yes
# Disallow any methods that do allow anonymous authentication
smtp_sasl_security_options = noanonymous
# Define the sasl_passwd file location
smtp_sasl_password_maps = hash:/etc/postfix/sasl/sasl_passwd

# Enable STARTTLS encryption
smtp_use_tls = yes

# Secure SMTP (sending)
smtp_tls_security_level = may
# smtp_tls_security_level = secure
# smtp_enforce_tls = yes

# Enable TLS logging
smtp_tls_loglevel = 1

# Discovering servers that support TLS
smtp_tls_note_starttls_offer = yes

non_smtpd_milters   = unix:/run/opendkim/opendkim.sock, unix:/run/opendmarc/opendmarc.sock
smtpd_milters       = unix:/run/opendkim/opendkim.sock, unix:/run/opendmarc/opendmarc.sock

policy-spf_time_limit = 3600s

# Disable VRFY (verify)
disable_vrfy_command = yes

# Block spam using DNS blacklists
smtpd_client_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_rbl_client bl.spamcop.net
# reject_rbl_client zen.spamhaus.org

smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unknown_sender_domain, reject_unknown_reverse_client_hostname, reject_unknown_client_hostname

# Require the client to provide a HELO/EHLO hostname
smtpd_helo_required = yes
smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname
smtp_helo_name = $mydomain

# Unsubscibe header
header_checks = regexp:/etc/postfix/list_unsub_header

# Protecting against forged sender addresses
smtpd_sender_login_maps=mysql:/etc/postfix/virtual_alias_maps.cf

# Hide the sender's IP and user agent in the Received header
smtp_header_checks = regexp:/etc/postfix/smtp_header_checks

/etc/postfix/master.cf
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (no)    (never) (100)
# ==========================================================================
smtp      inet  n       -       n       -       -       smtpd
  -o content_filter=amavisfeed:[127.0.0.1]:10024

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
  -o syslog_name=postfix/submission
  -o smtpd_tls_wrappermode=no
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o content_filter=amavisfeed:[127.0.0.1]:10024
  -o smtpd_sender_restrictions=reject_sender_login_mismatch,permit_sasl_authenticated,reject

policy-spf  unix  -       n       n       -       0       spawn
        user=nobody argv=/usr/bin/policyd-spf

amavisfeed      unix  -    -       n       -       2       smtp
 -o smtp_data_done_timeout=1200
 -o smtp_send_xforward_command=yes
 -o disable_dns_lookups=yes
 -o max_use=20

127.0.0.1:10025 inet n  -       y       -       -       smtpd
 -o content_filter=
 -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,no_milters
 -o local_header_rewrite_clients=

Create unprivileged user

For security reasons, we create a new user vmail to store the mails.

groupadd -g 5000 vmail
useradd -u 5000 -g vmail -s /usr/bin/nologin -d /home/vmail -m vmail

We use a gid and uid of 5000 in both cases so that we do not run into conflicts with regular users.
All our mail will be stored in /home/vmail.


MariaDB

We have to initialize the MariaDB data directory and create the system tables in the mysql database before starting the mariadb.service.

mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql

Enable and start mariadb.service.

systemctl enable mariadb
systemctl start mariadb

Improve the initial security of our MariaDB installation with recommended security measures,
such as removing anonymous accounts and removing the test database.
When prompted to “Switch to unix_socket authentication” enter n for No.

mysql_secure_installation

By default, MySQL will listen on the 0.0.0.0 address, which includes all network interfaces.
We have to restrict MySQL to listen only to the loopback address.

/etc/my.cnf.d/server.cnf
[mysqld]
#bind-address = localhost
bind-address = 127.0.0.1

Restart mariadb.service.

systemctl restart mariadb

Postfix database initialization

We have to create an empty database and give the corresponding user permission to use the database.
postfix_user will have read/write access to the database postfix_db using POSTFIXDBPASSWORD as password.

mysql -u root -p
CREATE DATABASE postfix_db;
GRANT ALL ON postfix_db.* TO 'postfix_user'@'localhost' IDENTIFIED BY 'POSTFIXDBPASSWORD';
FLUSH PRIVILEGES;
QUIT;

We have to set up the necessary configurations for postfix to interact with the database for all its other transport needs.

/etc/postfix/virtual_alias_maps.cf
user = postfix_user
password = POSTFIXDBPASSWORD
hosts = localhost
dbname = postfix_db
table = alias
select_field = goto
where_field = address

/etc/postfix/virtual_mailbox_domains.cf
user = postfix_user
password = POSTFIXDBPASSWORD
hosts = localhost
dbname = postfix_db
table = domain
select_field = domain
where_field = domain

/etc/postfix/virtual_mailbox_maps.cf
user = postfix_user
password = POSTFIXDBPASSWORD
hosts = localhost
dbname = postfix_db
table = mailbox
select_field = maildir
where_field = username

/etc/postfix/virtual_alias_domains_maps.cf
user = postfix_user
password = POSTFIXDBPASSWORD
hosts = localhost
dbname = postfix_db
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' AND alias_domain.active='1'

/etc/postfix/virtual_alias_domains.cf
user = postfix_user
password = POSTFIXDBPASSWORD
hosts = localhost
dbname = postfix_db
query = SELECT alias_domain FROM alias_domain WHERE alias_domain='%s' AND active = '1'

Only postfix should have access rights to these files, as they contain passwords.

chown root:postfix -R /etc/postfix/
chmod 640 /etc/postfix/virtual_*

We have to run postmap on transport to generate its database.

postmap /etc/postfix/transport

Dovecot

Install Dovecot package.

pacman -Syu dovecot

Create the dovecot configuration directory and configuration files.

mkdir /etc/dovecot

/etc/dovecot/dovecot.conf
protocols = imap
listen = *
auth_mechanisms = plain login
passdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf
}
userdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf
}

service auth {
    unix_listener auth-client {
        group = postfix
        mode = 0660
        user = postfix
    }
    user = root
}


mail_home = /home/vmail/%d/%n
mail_location = maildir:~

ssl_dh = </etc/dovecot/dh.pem

ssl_cert = </etc/letsencrypt/live/mail.wildw1ng.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.wildw1ng.com/privkey.pem

/etc/dovecot/dovecot-sql.conf
driver = mysql
connect = host=localhost dbname=postfix_db user=postfix_user password=POSTFIXDBPASSWORD
# It is highly recommended to not use deprecated MD5-CRYPT. Read more at http://wiki2.dovecot.org/Authentication/PasswordSchemes
default_pass_scheme = SHA512-CRYPT
# Get the mailbox
user_query = SELECT '/home/vmail/%d/%n' as home, 'maildir:/home/vmail/%d/%n' as mail, 5000 AS uid, 5000 AS gid, concat('dirsize:storage=',  quota) AS quota FROM mailbox WHERE username = '%u' AND active = '1'
# Get the password
password_query = SELECT username as user, password, '/home/vmail/%d/%n' as userdb_home, 'maildir:/home/vmail/%d/%n' as userdb_mail, 5000 as  userdb_uid, 5000 as userdb_gid FROM mailbox WHERE username = '%u' AND active = '1'
# If using client certificates for authentication, comment the above and uncomment the following
#password_query = SELECT null AS password, ‘%u’ AS user

Set permissions.

chown dovecot:dovecot /etc/dovecot/*

Remove the old temporary SSL parameters file.

rm /var/lib/dovecot/ssl-parameters.dat

We are required to provide DH parameters. Generate a new DH parameters file (this might take a long time).

openssl dhparam -out /etc/dovecot/dh.pem 4096

Enable Dovecot debug logging.

/etc/dovecot/dovecot.conf
auth_verbose = yes
auth_verbose_passwords = no
auth_debug = yes
auth_debug_passwords = yes
mail_debug = yes
verbose_ssl = yes

Testing IMAP.

openssl s_client -connect 127.0.0.1:993
a login admin@wildw1ng.com PASSWORD
a examine inbox
a logout

PostfixAdmin

Web interface for Postfix used to manage mailboxes, virtual domains and aliases. postfixadmin.png


Install PostfixAdmin, Apache and PHP packages.

pacman -Syu postfixadmin apache php-fpm php-imap

Apache HTTP Server configuration

/etc/httpd/conf/httpd.conf
ServerName localhost
Listen 0.0.0.0:80

# php-fpm, an alternative PHP FastCGI implementation with some additional features (mostly) useful for heavy-loaded sites
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

# SSL
LoadModule ssl_module modules/mod_ssl.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule rewrite_module modules/mod_rewrite.so

# Virtual hosts
Include conf/extra/httpd-vhosts.conf

# PostfixAdmin
Include /etc/httpd/conf/postfixadmin.conf

# php-fpm
Include conf/extra/php-fpm.conf

# Secure (SSL/TLS) connections
Include conf/extra/httpd-ssl.conf

<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>

<IfModule mod_ssl.c>
Listen 0.0.0.0:443
</IfModule>
Include /etc/httpd/conf/extra/httpd-vhosts-le-ssl.conf

php-fpm proxy configuration

/etc/httpd/conf/extra/php-fpm.conf
DirectoryIndex index.php index.html
<FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php-fpm/php-fpm.sock|fcgi://localhost/"
</FilesMatch>
chmod 644 /etc/httpd/conf/extra/php-fpm.conf

Configure Apache HTTP Server with php-fpm

/etc/httpd/conf/postfixadmin.conf
Alias /postfixadmin "/usr/share/webapps/postfixadmin/public"
<Directory "/usr/share/webapps/postfixadmin/public">
    DirectoryIndex index.html index.php
    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/postfixadmin/postfixadmin.sock|fcgi://localhost/"
    </FilesMatch>
    AllowOverride All
    Options FollowSymlinks
    Require all granted
    SetEnv PHP_ADMIN_VALUE "open_basedir = /tmp/:/usr/share/webapps/postfixadmin:/etc/webapps/postfixadmin/:/var/cache/postfixadmin/templates_c"
</Directory>

/etc/php/php-fpm.d/postfixadmin.conf
[postfixadmin]
user = postfixadmin
group = postfixadmin
listen = /run/postfixadmin/postfixadmin.sock
listen.owner = root
listen.group = http
listen.mode = 0660
pm = ondemand
pm.max_children = 4
php_admin_value['date.timezone'] = UTC
php_admin_value['session.save_path'] = /tmp
php_admin_value['open_basedir'] = /tmp/:/usr/share/webapps/postfixadmin/:/etc/webapps/postfixadmin/:/usr/bin/doveadm:/var/cache/postfixadmin

PHP configuration

/etc/php/php.ini
open_basedir = /var/cache/postfixadmin/:/etc/webapps/:/usr/share/webapps/:/tmp/:/var/cache/roundcubemail:/usr/share/webapps/roundcubemail:/etc/webapps/roundcubemail:/usr/share/pear/:/var/log/roundcubemail
date.timezone = "UTC"

extension=imap
extension=mysqli
extension=pdo_mysql
extension=iconv
extension=gd
extension=intl
extension=exif
extension=imagick

PostfixAdmin configuration

/etc/webapps/postfixadmin/config.local.php
<?php
$CONF['configured'] = true;
// correspond to dovecot maildir path /home/vmail/%d/%u 
$CONF['domain_path'] = 'YES';
$CONF['domain_in_mailbox'] = 'NO';
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'postfix_user';
$CONF['database_password'] = 'POSTFIXDBPASSWORD';
$CONF['database_name'] = 'postfix_db';

$CONF['default_aliases'] = array (
    'abuse' => 'abuse@wildw1ng.com',
    'hostmaster' => 'hostmaster@wildw1ng.com',
    'postmaster' => 'postmaster@wildw1ng.com',
    'webmaster' => 'webmaster@wildw1ng.com'
);

$CONF['vacation_domain'] = 'autoreply.wildw1ng.com';

$CONF['footer_text'] = 'Return to wildw1ng.com';
$CONF['footer_link'] = 'https://wildw1ng.com';
$CONF['encrypt'] = 'dovecot:SHA512-CRYPT';
$CONF['setup_password'] = 'HASHEDSETUPPASSWORD';

Enable and start Services.

systemctl enable httpd
systemctl enable php-fpm
systemctl enable postfix
systemctl enable dovecot

Generate hashes with non-default hash functions.

doveadm pw -s SHA512-CRYPT -p "DOVEADMPASSWORD"

Write the HASHEDSETUPPASSWORD to the configuration file.

Navigate to http://10.0.1.18/postfixadmin/setup.php.
Now we can create a superadmin account.


Restrict access to setup.php after installation is finished.

chmod 600 /usr/share/webapps/postfixadmin/public/setup.php

Check the apache log for errors.

less /var/log/httpd/error_log

PostfixAdmin pacman hook

The database needs to be upgraded after a version bump.
We will see a message saying ‘The PostfixAdmin database layout is outdated’ on the login page.
Therefore we may set up a hook that runs the needed upgrade.php script automatically via a pacman hook.

/etc/pacman.d/hooks/postfixadmin.hook
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = postfixadmin

[Action]
Description = Run Postfixadmin upgrade.php to make sure database is up to date
When = PostTransaction
Exec = /usr/bin/runuser -u postfixadmin -- /usr/bin/php /usr/share/webapps/postfixadmin/public/upgrade.php


Roundcube

Full-featured, PHP web-based mail client. roundcube.png


Install Roundcube and PHP Plugin packages.

pacman -Syu roundcubemail php-gd php-intl php-imagick librsvg

Warning

Roundcube needs a separate database to work. You should not use the same database for Roundcube and PostfixAdmin.
Create a second database roundcube_db and a new user named roundcube_user.


Create an empty database and give the corresponding user permission to use the database.

mysql -u root -p
CREATE DATABASE `roundcube_db` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
CREATE USER `roundcube_user`@'localhost' IDENTIFIED BY 'ROUNDCUBEDBPASSWORD';
GRANT ALL PRIVILEGES ON `roundcube_db`.* TO `roundcube_user`@`localhost`;
FLUSH PRIVILEGES;
QUIT;

We need to initialize the roundcubemail database tables.

mysql -u root -p roundcube_db < /usr/share/webapps/roundcubemail/SQL/mysql.initial.sql

Copy the default configuration file and set permisions.

cd /etc/webapps/roundcubemail/config
cp config.inc.php.sample config.inc.php
chown http:http config.inc.php
chmod 640 config.inc.php

Set our mail server settings.

/etc/webapps/roundcubemail/config/config.inc.php
?php

$config = [];

// Database connection string (DSN) for read+write operations
// Format (compatible with PEAR MDB2): db_provider://user:password@host/database
// Currently supported db_providers: mysql, pgsql, sqlite, mssql, sqlsrv, oracle
// For examples see http://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php
// NOTE: for SQLite use absolute path (Linux): 'sqlite:////full/path/to/sqlite.db?mode=0646'
//       or (Windows): 'sqlite:///C:/full/path/to/sqlite.db'
$config['db_dsnw'] = 'mysql://roundcube_user:ROUNDCUBEDBPASSWORD@localhost/roundcube_db';
$config['imap_host'] = 'tls://mail.wildw1ng.com';
$config['smtp_host'] = 'tls://mail.wildw1ng.com';
$config['smtp_port'] = 587;
$config['imap_port'] = 993;
$config['mime_types'] = '/etc/webapps/roundcubemail/config/mime.types';

// IMAP host chosen to perform the log-in.
// See defaults.inc.php for the option description.
// $config['imap_host'] = 'localhost:143';

// SMTP server host (for sending mails).
// See defaults.inc.php for the option description.
// $config['smtp_host'] = 'localhost:587';

// SMTP username (if required) if you use %u as the username Roundcube
// will use the current username for login
$config['smtp_user'] = '%u';

// SMTP password (if required) if you use %p as the password Roundcube
// will use the current user's password for login
$config['smtp_pass'] = '%p';

// provide an URL where a user can get support for this Roundcube installation
// PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE!
$config['support_url'] = 'https://wildw1ng.com';

// Name your service. This is displayed on the login screen and in the window title
$config['product_name'] = 'Roundcube Webmail';

// This key is used to encrypt the users imap password which is stored
// in the session record. For the default cipher method it must be
// exactly 24 characters long.
// YOUR KEY MUST BE DIFFERENT THAN THE SAMPLE VALUE FOR SECURITY REASONS
$config['des_key'] = 'LONGRANDOMSTRING';

// List of active plugins (in plugins/ directory)
$config['plugins'] = [
    'archive',
    'zipdownload',
    'password',
];

// skin name: folder from skins/
$config['skin'] = 'elastic';    

Set enable_installer to enable the setup wizard

$config['enable_installer'] = true;

For Roundcube to be able to detect mime-types from filename extensions you need to point it to a mime.types file.
Apache usually comes with one.

cp /etc/httpd/conf/mime.types /etc/webapps/roundcubemail/config/mime.types
chown http:http /etc/webapps/roundcubemail/config/mime.types
chmod 640 /etc/webapps/roundcubemail/config/mime.types
/etc/webapps/roundcubemail/config/config.inc.php
$config['mime_types'] = '/etc/webapps/roundcubemail/config/mime.types';

Info

If you have configured open_basedir in php.ini, make sure it includes /etc/webapps and /usr/share/webapps,
so PHP can open the required Roundcube files.


Enable the password plugin to let users change their passwords from within Roundcube.

/etc/webapps/roundcubemail/config/config.inc.php
$config['plugins'] = password;

Configure the password plugin and make sure you alter the settings accordingly.

/usr/share/webapps/roundcubemail/plugins/password/config.inc.php
<?php

$config['password_driver'] = 'sql';
$config['password_db_dsn'] = 'mysql://postfix_user:POSTFIXDBPASSWORD@localhost/postfix_db';
// If you are not using dovecot specify another algorithm explicitly e.g 'sha256-crypt'
$config['password_algorithm'] = 'dovecot';
// For dovecot salted passwords only (above must be set to 'dovecot')
// $config['password_algorithm_prefix'] = 'true';
// $config['password_dovecotpw'] = 'doveadm pw';
// $config['password_dovecotpw_method'] = 'SHA512-CRYPT';
// $config['password_dovecotpw_with_method'] = true;
$config['password_query'] = 'UPDATE mailbox SET password=%P WHERE username=%u';

Now we finish the Roundcube installation with the wizard in our browser http://10.0.1.18/roundcube/installer.


For security reasons, we have to disable the installer after finishing the wizard and remove the installer directory.

rm /usr/share/webapps/roundcubemail/installer
/etc/webapps/roundcubemail/config/config.inc.php

delete $config['enable_installer'] = true;


DNS Record

We need to set A and MX DNS records pointing our mail server.

dns-records.png

A record pointing our system’s FQDN (hostname) to our mail server IPv4 address.

mail.wildw1ng.com 60 IN A 37.201.217.90

MX record specifies which mail server is responsible for accepting emails on behalf of a recipient’s domain.
All messages sent to @wildw1ng.com email addresses will be accepted by the mail.wildw1ng.com mail server.

wildw1ng.com 3600 IN MX 0 mail.wildw1ng.com


Open ports on mail server

Port Service Description
25 SMTP Transmission of email from email server to email server
993 IMAP Secure session

Check open ports on our machine.

ss -tapn
netstat -tlpn

Get SSL certificates with Certbot via Let’s Encrypt for Apache

/etc/httpd/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
        ServerAdmin admin@wildw1ng.com
        DocumentRoot "/usr/share/webapps/roundcubemail"
        ServerName mail.wildw1ng.com
        ServerAlias mail.wildw1ng.com
        ErrorLog "/var/log/httpd/mail.wildw1ng.com-error.log"
        CustomLog "/var/log/httpd/mail.wildw1ng.com-access.log" common
        <Directory "/usr/share/webapps/roundcubemail">
                AllowOverride All
                Options FollowSymlinks
                Require all granted
                SetEnv PHP_ADMIN_VALUE "open_basedir /tmp/:/var/cache/roundcubemail:/usr/share/webapps/roundcubemail:/etc/webapps/roundcubemail:/usr/share/pear/:/var/log/roundcubemail"
        </Directory>
RewriteEngine on
RewriteCond %{SERVER_NAME} =mail.wildw1ng.com
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

/etc/httpd/conf/extra/httpd-vhosts-le-ssl.conf
<IfModule mod_ssl.c>
SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000)
<VirtualHost *:443>
        ServerAdmin admin@wildw1ng.com
        DocumentRoot "/usr/share/webapps/roundcubemail"
        ServerName mail.wildw1ng.com
        ServerAlias mail.wildw1ng.com
        ErrorLog "/var/log/httpd/mail.wildw1ng.com-error.log"
        CustomLog "/var/log/httpd/mail.wildw1ng.com-access.log" common
        <Directory "/usr/share/webapps/roundcubemail">
                AllowOverride All
                Options FollowSymlinks
                Require all granted
                SetEnv PHP_ADMIN_VALUE "open_basedir /tmp/:/var/cache/roundcubemail:/usr/share/webapps/roundcubemail:/etc/webapps/roundcubemail:/usr/share/pear/:/var/log/roundcubemail"
        </Directory>

Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/mail.wildw1ng.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/mail.wildw1ng.com/privkey.pem
SSLUseStapling on
</VirtualHost>
</IfModule>


Run Certbot to obtain a certificate.

certbot --apache

Certificate is saved at: /etc/letsencrypt/live/mail.wildw1ng.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/mail.wildw1ng.com/privkey.pem



If we get errors, we have to ensure that SSL is not multiple defined.

grep -r "Listen 443" /etc/httpd

Sender Policy Framework

SPF is an email authentication protocol used to stop phishing attacks.
We can specify who is allowed to send email on behalf of our domain.


Install SPF package.

yay -Syu python-spf-engine

Modify Postfix configuration files to enable SPF.

/etc/postfix/main.cf
smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, check_policy_service unix:private/policy-spf
policy-spf_time_limit = 3600s

/etc/postfix/master.cf
policy-spf  unix  -       n       n       -       0       spawn
    user=nobody argv=/usr/bin/policyd-spf

SPF DNS Record

To allow other mail exchangers to validate mails apparently sent from our domain, we need to set a DNS TXT record with v=spf1 mx ~all. We are approving the domain mail servers (mx) and if the SPF check fails, the result will be a soft failure (~all).


DomainKeys Identified Mail

DKIM is a sender authentication protocol that allows signing messages so mailbox providers can verify them.
This method is designed to detect email spoofing by identifying forged sender addresses in email.


Install OpenDKIM package.

pacman -Syu opendkim

Create a directory for dkim.

mkdir /var/db/dkim/

Generate a secret signing key.

opendkim-genkey -r -s default -d wildw1ng.com
chmod 400 /var/db/dkim/default.*

Copy the default configuration file.

cp /usr/share/doc/opendkim/opendkim.conf.sample /etc/opendkim/opendkim.conf
chmod 644 /etc/opendkim/opendkim.conf

Modify OpenDKIM configuration and create a Socket for DKIM.

/etc/opendkim/opendkim.conf
Domain                  wildw1ng.com
KeyFile                 /var/db/dkim/default.private
Selector                default
Socket                  unix:/run/opendkim/opendkim.sock
TemporaryDirectory      /run/opendkim
UMask                   002
UserID                  opendkim
Canonicalization        relaxed/simple

mkdir /run/opendkim
chown opendkim:postfix /run/opendkim
chmod 750 /run/opendkim
mkdir -p /etc/systemd/system/opendkim.service.d/
chmod 755 /etc/systemd/system/opendkim.service.d/

/etc/tmpfiles.d/opendkim.conf
D /run/opendkim 0750 opendkim postfix

chmod 644 /etc/tmpfiles.d/opendkim.conf

/etc/systemd/system/opendkim.service.d/override.conf
[Service]
User=
User=opendkim
Group=
Group=postfix

chmod 644 /etc/systemd/system/opendkim.service.d/override.conf
chown opendkim:postfix /var/db/dkim/
chown opendkim:postfix /var/db/dkim/default.private

Enable and start the opendkim.service.

systemctl enable opendkim

DKIM DNS Record

Add a DNS TXT record with the selector and public key.

less /var/db/dkim/default.txt 

Copy everything in between (" “) without the brackets and quotes into a default._domainkey TXT DNS Record.

v=DKIM1; k=rsa; s=email; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNXDCBiQKBgQDjk96JyEAU2QLfDjZYyHTHVWYP/effPipH3hpgfa+Nk Wg/WmfZXjI3CmDY+N3m+eRmZdIzYO9oPGi+r0h3ceSZe4Cj858/k/0D7aYdG18QQDLIY+x+dmp7MjRK1+/B1xWjWy/Sn4n F1zVmROVxuBraX2eL32deu+qrnZlsu2H9MwIDAQAB

Check the record.

host -t TXT default._domainkey.wildw1ng.com

Domain-based Message Authentication, Reporting and Conformance

DMARC is an email authentication protocol that provides domain-level protection, detecting and preventing email spoofing techniques used in phishing.


Install OpenDMARC package.

pacman -Syu opendmarc

Modify OpenDMARC configuration and create a Socket for DMARC.

/etc/opendmarc/opendmarc.conf
Socket  unix:/run/opendmarc/opendmarc.sock
UMask 002

mkdir /run/opendmarc
chown opendmarc:postfix /run/opendmarc
chmod 750 /run/opendmarc

/etc/tmpfiles.d/opendmarc.conf
D /run/opendmarc 0750 opendmarc postfix

chmod 644 /etc/tmpfiles.d/opendmarc.conf
mkdir -p /etc/systemd/system/opendmarc.service.d/
chmod 755 /etc/systemd/system/opendmarc.service.d/

/etc/systemd/system/opendmarc.service.d/override.conf
[Service]
Group=
Group=postfix

chmod 644 /etc/systemd/system/opendmarc.service.d/override.conf

Add Mail Filter Sockets to our Postfix configuration and make sure that the DMARC milter is declared after the DKIM milter.

/etc/postfix/main.cf
non_smtpd_milters   = unix:/run/opendkim/opendkim.sock, unix:/run/opendmarc/opendmarc.sock
smtpd_milters       = unix:/run/opendkim/opendkim.sock, unix:/run/opendmarc/opendmarc.sock

Enable and start the opendmarc.service.

systemctl enable opendmarc

DMARC DNS Record

To enable DMARC for a domain, add a new TXT record to its DNS zone.


First testing, no harm as (sub)policy is “none”, but start to receive aggregated reports and failing reports (SPF and DKIM).

_dmarc.wildw1ng.com TXT v=DMARC1; rua=mailto:admin@wildw1ng.com; ruf=mailto:admin@wildw1ng.com; adkim=s; fo=1

After a certain time, after analyzing these reports enable the policy, for wildw1ng, for 10% of e-mail traffic.

_dmarc.wildw1ng.com TXT v=DMARC1; p=quarantine; rua=mailto:admin@wildw1ng.com; ruf=mailto:admin@wildw1ng.com; adkim=s; fo=1; pct=10

Then slowly raise the percentage and finalize with policy 100% enabled and only failing reports.

_dmarc.wildw1ng.com TXT v=DMARC1; p=quarantine; ruf=mailto:admin@wildw1ng.com; adkim=s; fo=1

Use DNS blacklists

/etc/postfix/main.cf
# Block spam using DNS blacklists
smtpd_client_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net

List-Unsubscribe header

Set header checks.

/etc/postfix/main.cf
header_checks = regexp:/etc/postfix/list_unsub_header

Create a list_unsub_header file.

/etc/postfix/list_unsub_header
/Content-Transfer-Encoding:/i PREPEND List-Unsubscribe: mailto:admin@wildw1ng.com?subject=unsubscribe
chmod 644 /etc/postfix/list_unsub_header

Amavis and ClamAV

Amavis is an interface between the MTA and content checkers, ClamAV virus scanner and SpamAssassin.


Install packages.

pacman -Syu amavisd-new clamav p7zip unrar arj lrzip lz4 lzo rpmextract

Disable anti-spam, enable logging.

/etc/amavisd/amavisd.conf
@bypass_virus_checks_maps = (1);  # controls running of anti-virus code
@bypass_spam_checks_maps  = (1);  # controls running of anti-spam code
# $bypass_decode_parts = 1;         # controls running of decoders&dearchivers
 
$mydomain = 'wildw1ng.com';
$myhostname = 'mail.wildw1ng.com';
 
$log_level = 5;              # verbosity 0..5, -d

Enable ClamAV support and list the same clamd.sock as in /etc/clamav/clamd.conf.

# http://www.clamav.net/
['ClamAV-clamd',
   \&ask_daemon, ["CONTSCAN {}\n", "/run/clamav/clamd.ctl"],
   qr/\bOK$/m, qr/\bFOUND$/m,
   qr/^.*?: (?!Infected Archive)(.*) FOUND$/m ],
# # NOTE: run clamd under the same user as amavisd - or run it under its own
# #   uid such as clamav, add user clamav to the amavis group, and then add
# # NOTE: match socket name (LocalSocket) in clamav.conf to the socket name in
# #   this entry; when running chrooted one may prefer a socket under $MYHOME.

Add a comment to this line to enable anti-virus scan.

# @bypass_virus_check_maps = (1);  # controls running of anti-virus code

After that, add clamav user to amavis group to avoid permission problems.

usermod -a -G amavis clamav

Updating ClamAV virus definition database

We need to run freshclam before starting the service for the first time or you will run into trouble/errors which will prevent ClamAV from starting correctly.


Start and enable clamav-freshclam.service so that the virus definitions are kept up to date.

systemctl enable clamav-freshclam.service

Start and enable Amavis and ClamAV services.

systemctl enable clamav-daemon.service
systemctl enable amavisd.service

Integration with Postfix

/etc/postfix/master.cf
#
# anti spam & anti virus section
#
amavisfeed      unix  -    -       n       -       2       smtp
 -o smtp_data_done_timeout=1200
 -o smtp_send_xforward_command=yes
 -o disable_dns_lookups=yes
 -o max_use=20
127.0.0.1:10025 inet n  -       y       -       -       smtpd
 -o content_filter=
 -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 smtpd_restrictions_classes=
 -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,no_milters
 -o local_header_rewrite_clients=

In this configuration we assume that postfix and Amavis are running on the same machine (i.e. 127.0.0.1).
If that is not the case edit /etc/amavisd/amavisd.conf and the prevous Postfix entry accordingly.
Postfix will listen to port 10025 so that Amavis can send back checked emails to that port.
We also have to add a configuration in our smtp or submission sections.

-o content_filter=amavisfeed:[127.0.0.1]:10024

Using this options implies that Postfix will send emails to Amavis on port 10024, so that these can be checked.
If mail passes the control then these are sent to port 10025.

We can now restart postfix.service and amavisd.service.


SpamAssasin

Install package.

pacman -Syu spamassassin

Note

Spamassassin is integrated in Amavis so we do not have to start spamassassin.service.


To enable support for Spamassassin comment the following line.

/etc/amavis/amavis.conf
# @bypass_spam_checks_maps = (1);  # controls running of anti-spam code

Edit the SpamAssassin configuration.

$sa_tag_level_deflt  = 1.0;  # add spam info headers if at, or above that level
$sa_tag2_level_deflt = 1.0;  # add 'spam detected' headers at that level
$sa_kill_level_deflt = 5.0;  # triggers spam evasive actions (e.g. blocks mail)
$sa_dsn_cutoff_level = 8;   # spam level beyond which a DSN is not sent
# $sa_quarantine_cutoff_level = 25; # spam level beyond which quarantine is off
$penpals_threshold_high = $sa_kill_level_deflt;  # do not waste time on hi spam
$bounce_killer_score = 100;  # spam score points to add for joe-jobbed bounces

Before we restart the amavisd service we have to run sa-update.

mkdir /etc/mail/spamassassin/sa-update-keys
chown spamd:spamd /etc/mail/spamassassin/sa-update-keys
chmod 700 /etc/mail/spamassassin/sa-update-keys
cd /etc/mail/spamassassin
sudo -u spamd wget "http://spamassassin.apache.org/updates/GPG.KEY"
sudo -u spamd sa-update --import GPG.KEY
rm GPG.KEY
sudo -u spamd sa-update -D
sudo -u spamd sa-compile

Keep SpamAssassin up to date

Manual update.

sudo -u spamd sa-update --channel updates.spamassassin.org
sudo -u spamd sa-compile

Create service to automate the process.

/usr/lib/systemd/system/spamassassin-update.service
[Unit]
Description=SpamAssassin Update
After=network.target

[Service]
User=spamd
Group=spamd
Type=oneshot
# UMask=0022

ExecStart=/usr/bin/vendor_perl/sa-update --channel updates.spamassassin.org
SuccessExitStatus=1
ExecStart=/usr/bin/vendor_perl/sa-compile
# ExecStart=!/usr/bin/systemctl -q --no-block try-restart spamassassin.service

# uncomment the following ExecStart line to train SA's bayes filter
# and specify the path to the mailbox that contains spam email(s)
# ExecStart=/usr/bin/vendor_perl/sa-learn --spam <path_to_your_spam_mailbox>

/usr/lib/systemd/system/spamassassin-update.timer
[Unit]
Description=SpamAssassin Update Timer

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Start and enable spamassassin-update.timer.

systemctl enable spamassassin-update.timer

Check permissions in /var/lib/spamassassin/ if you get errors.