Self-hosting email for small to medium companies, business owners, non-profit organizations and individuals. There is no real limit to who may self-host, but privacy conscious groups will benefit directly and see immediate results. Small to medium companies will get a fully functioning system that adheres to all current encryption and security protocols, especially the PCI specification.
There are tons of reasons why self-hosting is beneficial, here are some of them:
Hosting at the major known providers is free, at first glance. A second look at their business practices will give you an indication that nothing is free, since YOU are the product. Your information is being analysed, categorized, fed into AI, given to advertisers and other 3rd parties.
The major players have to be very compatible, to make sure everyone has access. In a self-hosting environment that does not apply. You can set your own rules, enforce strict limits that wouldn’t be applicable to general hosting.
Self-hosting allows you to do a lot more, like use custom configuration options, run experimental code, test new features and provide a more bespoke environment for your hosting needs.
Self-hosting can be achieved with open source tools, a huge saving over commercial control panels and enterprise solutions.
Please make sure your server has a fully qualified domain name (FQDN) and the /etc/hosts
file has the IP of the server point to the FQDN and not the short name. This is a requirement for some tools like OpenDKIM, which will fail to work without an FQDN.
To test if your FQDN is properly setup, run:
# hostname -f
server.example.tld
If you only see the server name and not the FQDN then use the systemd hostnamectl
command to set your FQDN:
# hostnamectl set-hostname server.example.tld
The first step is to install some basic packages, use your package manager:
dnf for newer versions of EL8/EL9:
# dnf install php php-cli php-pdo php-mbstring epel-release unzip wget
yum for EL7:
# yum install php php-cli php-pdo php-mbstring epel-release unzip wget
for Fedora the packages are slightly different:
# dnf install php php-cli php-pdo php-mbstring unzip wget
In the second step we’ll install the Aetolos virtual hosting control panel, which only generates configuration files and does not make any other modifications to the system:
# wget -O /root/master.zip 'https://gitlab.com/noumenia/aetolos/-/archive/master/aetolos-master.zip'
# unzip /root/master.zip -d /root/
# mv /root/aetolos-master /root/aetolos
# rm -rf /root/master.zip
The latest development code can be downloaded from the git repository:
git clone 'https://gitlab.com/noumenia/aetolos.git'
Now it is time to choose what services the server will provide. Run Aetolos with the status parameter to get an idea of all the available services:
# ~/aetolos/aetolos --status
+---------+--------------+---------+------------+--------------+
| Enabled | Module | SystemD | Repository | Dependencies |
+---------+--------------+---------+------------+--------------+
| [ ] | apache | | | |
| [ ] | clamav | | epel | postfix |
| [ ] | dehydrated | | | |
| [ ] | dovecot | | | |
| [ ] | haproxy | | | |
| [ ] | mariadb | | | |
| [ ] | mtasts | | | apache |
| [ ] | nsd | | epel | |
| [ ] | opendkim | | epel | postfix |
| [ ] | opendmarc | | epel | postfix |
| [✓] | php | | | |
| [ ] | postfix | | | |
| [ ] | postgrey | | epel | postfix |
| [ ] | spamassassin | | | postfix |
| [✓] | virtualhost | | | |
+---------+--------------+---------+------------+--------------+
As this is going to be a self-hosted email server, we are going to enable the services that give us a complete set of features for email hosting:
Apache, a web server for responding on ACME requests
Dehydrated, a bash script for Let’s Encrypt certificates
Dovecot, a POP3/IMAP mail server
OpenDKIM, a DKIM validator and signer
OpenDMARC, a DMARC validator
Postfix, an SMTP server
Spamassassin, spam protection
We will not enable ClamAV because it requires at least 4GB of memory, the reader may opt to enable it based on the resources available on the server.
Let’s enable these services with Aetolos:
# ~/aetolos/aetolos --enable=apache
# ~/aetolos/aetolos --enable=dehydrated
# ~/aetolos/aetolos --enable=dovecot
# ~/aetolos/aetolos --enable=opendkim
# ~/aetolos/aetolos --enable=opendmarc
# ~/aetolos/aetolos --enable=postfix
# ~/aetolos/aetolos --enable=spamassassin
Now we may re-run Aetolos with the status parameter to see the following table:
# ~/aetolos/aetolos --status
+---------+--------------+---------+------------+--------------+
| Enabled | Module | SystemD | Repository | Dependencies |
+---------+--------------+---------+------------+--------------+
| [✓] | apache | dead | | |
| [ ] | clamav | | epel | postfix |
| [✓] | dehydrated | | | |
| [✓] | dovecot | dead | | |
| [ ] | haproxy | | | |
| [ ] | mariadb | | | |
| [ ] | mtasts | | | apache |
| [ ] | nsd | | epel | |
| [✓] | opendkim | dead | epel | postfix |
| [✓] | opendmarc | dead | epel | postfix |
| [✓] | php | dead | | |
| [✓] | postfix | dead | | |
| [ ] | postgrey | | epel | postfix |
| [✓] | spamassassin | dead | | postfix |
| [✓] | virtualhost | | | |
+---------+--------------+---------+------------+--------------+
Note the SystemD column which shows most services as dead
. This is not an issue, because we have not configured/started those services, yet.
Finally, we may run Aetolos setup to install the required RPM packages and generate all the configuration files. We prefer to see the verbose output with the extra --verbose
parameter.
You will notice two warnings, the first warning is about the dehydrated script, which tells us to specify the registration email address for Let’s Encrypt and the second warning is about generating the DH parameters prime number, this procedure takes quite some time (10 minutes on average), so please take the time to read the rest of this article and prepare for what is to come.
# ~/aetolos/aetolos --verbose --setup
[DEBUG] Check running system
[DEBUG] Detected: AlmaLinux 8.7 (Stone Smilodon)
[DEBUG] Check system memory
[DEBUG] Detected: 460MiB
[DEBUG] Check CPU cores
[DEBUG] Detected: 2
[DEBUG] Check proxy
[DEBUG] Loading Aetolos configuration
[DEBUG] Starting operating system setup
[DEBUG] Checking repository dependencies
[DEBUG] Checking package dependencies
[DEBUG] Checking SELinux requirements
[DEBUG] SELinux: enable dovecot_home_etc_aetolos.pp
[DEBUG] SELinux: enable postfix_socket_clamav_aetolos.pp
[DEBUG] SELinux: enable spamd_resolv_aetolos.pp
[DEBUG] Checking certificate requirements
[DEBUG] Checking ACME client
[DEBUG] Downloading ACME client: dehydrated 0.7.1
[DEBUG] Installing ACME client under: /root/dehydrated/
[WARNING] ACME registration email set to: postmaster
[WARNING] Please change the ACME registration email by executing: '/root/aetolos/aetolos --verbose --module=dehydrated --registration-email=new-email'
[DEBUG] Checking module dependencies
[DEBUG] Save configuration: dehydrated
[DEBUG] Writing to file: /root/dehydrated/config
[DEBUG] Writing to file: /root/dehydrated/hook.sh
[DEBUG] Writing to file: /etc/pki/letsencrypt/domains.txt
[DEBUG] Writing to file: /etc/cron.daily/dehydrated.cron
[DEBUG] Save configuration: dovecot
[WARNING] Generating DH parameters. This is going to take a long time...
[DEBUG] Writing to file: /etc/dovecot/dovecot.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/10-auth.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/10-mail.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/10-master.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/10-ssl.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/15-lda.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/15-mailboxes.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/20-imap.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/20-pop3.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/90-acl.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/90-quota.conf
[DEBUG] Writing to file: /etc/dovecot/conf.d/auth-passwdfile.conf.ext
[DEBUG] Save configuration: opendkim
[DEBUG] Writing to file: /etc/sysconfig/opendkim
[DEBUG] Writing to file: /etc/opendkim.conf
[DEBUG] Save configuration: opendmarc
[DEBUG] Writing to file: /etc/cron.weekly/publicsuffix.cron
[DEBUG] Writing to file: /etc/opendmarc.conf
[DEBUG] Save configuration: php
[DEBUG] Writing to file: /etc/systemd/system/php-fpm.service.d/override.conf
[DEBUG] Writing to file: /etc/php.ini
[DEBUG] Writing to file: /etc/php.d/10-opcache.ini
[DEBUG] Save configuration: postfix
[DEBUG] Writing to file: /etc/postfix/virtual_mailbox_domains
[DEBUG] Writing to file: /etc/postfix/transport
[DEBUG] Writing to file: /etc/postfix/smtp_header_checks
[DEBUG] Writing to file: /etc/postfix/mime_header_checks
[DEBUG] Override template file: /root/aetolos/modules/el8/postfix/templates.d/maincf.tpl
[DEBUG] Writing to file: /etc/postfix/main.cf
[DEBUG] Writing to file: /etc/postfix/master.cf
[DEBUG] Save configuration: spamassassin
[DEBUG] Writing to file: /etc/mail/spamassassin/init.pre
[DEBUG] Writing to file: /etc/sysconfig/spamass-milter
[DEBUG] Writing to file: /etc/mail/spamassassin/local.cf
[DEBUG] Save configuration: virtualhost
[DEBUG] Writing to file: /etc/tmpfiles.d/hometmp.conf
As per the warning, we add an email address for Let’s Encrypt:
# ~/aetolos/aetolos --verbose --module=dehydrated [email protected]
Now we are ready to add as many virtual hosts as we like (or as many as the server can handle). Before running any commands, we may take a second to think which virtual hosts will have a prefix subdomain, sometimes we may have a domain like example.tld with a www subdomain (www.example.tld), other times we may not want to use a prefix because we want our email addresses only at the top domain ([email protected] and not [email protected]).
As an example, we will add three virtual hosts, two of them without a subdomain:
# ~/aetolos/aetolos --verbose --module=virtualhost --add-virtualhost=example1.tld --no-prefix
[DEBUG] Verify domain: example1.tld
[DEBUG] Add virtual host: example1.tld
# ~/aetolos/aetolos --verbose --module=virtualhost --add-virtualhost=example2.tld --no-prefix
[DEBUG] Verify domain: example2.tld
[DEBUG] Add virtual host: example2.tld
# ~/aetolos/aetolos --verbose --module=virtualhost --add-virtualhost=example3.tld
[DEBUG] Verify domain: example3.tld
[DEBUG] Add virtual host: example3.tld
We can now list all virtual hosts:
# ~/aetolos/aetolos --module=virtualhost --list-virtualhosts
+----+---------------------+--------------+-------------+----------------+--------+--------------+
| ID | Timestamp | Virtual host | System user | MariaDB prefix | Parked | Prefix alias |
+----+---------------------+--------------+-------------+----------------+--------+--------------+
| 1 | 2023-01-10 08:51:36 | example1.tld | example1tld | example1 | [ ] | |
| 2 | 2023-01-10 08:51:40 | example2.tld | example2tld | example2 | [ ] | |
| 3 | 2023-01-10 08:51:45 | example3.tld | example3tld | example3 | [ ] | www |
+----+---------------------+--------------+-------------+----------------+--------+--------------+
Each virtual host will “live” isolated within its own home directory, with its own user/group ownership, so lets take a look at our /home:
# ls -la /home
total 0
drwxr-xr-x. 5 root root 63 Jan 20 10:51 .
dr-xr-xr-x. 17 root root 224 Dec 21 2021 ..
drwx--x--x. 8 example1tld example1tld 159 Jan 10 08:51 example1tld
drwx--x--x. 8 example2tld example2tld 159 Jan 10 08:51 example2tld
drwx--x--x. 8 example3tld example3tld 159 Jan 10 08:51 example3tld
After we made modifications with the Aetolos command, we need to generate new configuration files for all the relevant services (Apache, Dovecot, Postfix, etc). Thus, we execute Aetolos setup one last time.
# ~/aetolos/aetolos --setup
At this point all services will use the same self-signed certificate, thus it is now a good time to run dehydrated manually to gather all virtual host certificates. Since Aetolos uses the ACME protocol with domain validation via HTTP port 80, we first start our Apache server to service those requests.
# systemctl start httpd
# ~/dehydrated/dehydrated --cron --config /root/dehydrated/config --keep-going
# systemctl stop httpd
# ~/aetolos/aetolos --setup
We may now enable and start all services.
# systemctl --now enable httpd [email protected] [email protected] [email protected] dovecot opendkim opendmarc postfix spamassassin
Created symlink /etc/systemd/system/multi-user.target.wants/[email protected] → /usr/lib/systemd/system/[email protected].
Created symlink /etc/systemd/system/multi-user.target.wants/[email protected] → /usr/lib/systemd/system/[email protected].
Created symlink /etc/systemd/system/multi-user.target.wants/[email protected] → /usr/lib/systemd/system/[email protected].
You will notice that each virtual host has its own Apache service, this is a feature provided by systemd and allows us to define the virtual hosts into separate systemd services. The --now
parameter will start all services.
For optimal deliverability we need to setup at least SPF and DKIM records, optionally we may also setup DMARC. While the setup of these TXT records falls outside the scope of this guide, we should point out that the DKIM key is stored under /etc/opendkim/keys/default.txt
. Aetolos has wiki pages with additional help:
First create a text file with the password, it is better security to not pass the password as a command-line parameter. Then add the new email address, optionally set a quota in bytes:
# ~/aetolos/aetolos --module=dovecot --virtualhost=example1.tld --add-email=sales --password-file='password.txt' --quota=5000000000
To change the password of an existing email address, is pretty straight forward:
~/aetolos/aetolos --module=dovecot --virtualhost=example1.tld --modify-email=sales --password-file='password.txt'
To change the quota use any value above zero in bytes, while a zero value will remove the quota limit:
~/aetolos/aetolos --module=dovecot --virtualhost=example1.tld --modify-email=sales --quota=0
It is equally easy to remove an email address, just keep in mind that all stored emails and related files will be deleted:
~/aetolos/aetolos --module=dovecot --virtualhost=example1.tld --remove-email=info
It is possible to list all email addresses associated with a virtual host:
# ~/aetolos/aetolos --module=dovecot --virtualhost=example1.tld --list-emails
+-------+-------+-------+----------------------+
| Email | Quota | Usage | Last password change |
+-------+-------+-------+----------------------+
| sales | 0 | | 10/10/23 |
+-------+-------+-------+----------------------+
There should be special mention about the architecture used for storing the emails within dovecot. For security reasons, each virtual host is isolated within unix accounts stored under their own /home
directory. Email accounts and their passwords are under the subdirectory etc
and mail is stored under the subdirectory mail
.
/home/example1tld -> virtual host home
/home/example1tld/etc -> virtual host + parked domains
/home/example1tld/etc/example1.tld/forwarders -> forwarder addresses
/home/example1tld/etc/example1.tld/passwd -> email addresses
/home/example1tld/etc/example1.tld/shadow -> passwords
/home/example1tld/mail/example1.tld -> email storage
At this point Aetolos has configured the server without any custom modifications, all changes are done on the default configuration files. We may delete Aetolos from the server and continue modifications manually or we may keep Aetolos in case we want to add more virtual hosts and/or email addresses in the future.
Article photo by Manuel Geissinger from pexels