mailserver-trixie
Contrary to popular belief, it's entirely possible to self-host email servers. Like others, I listened to the propaganda that “it's no longer feasible to self-host email” or “it's too complex and servers won't respect your mail health anyway” or other such explanations. In 2014, while running workshops for students on security and networking, one of my student's parents (a Ruby dev) said he agreed with me and that as far as he knew, postfix was fairly straightforward. From that day forward, I decided to approach self-hosting email servers the same way as I approach self-hosting any instance (Nextcloud, Airsonic, Gitlab, etc.). I decided that it must be entirely possible and that it was merely a question of how. So, from 2014 - 2018, from Wheezy to Buster, I began using my spare hacking time to create my first smtp relay in 2015 and later setup my first proper email server in 2018. As it turns out, it is relatively straight forward to setup a functional base server. Getting the ecosystem to respect your email, however, takes a little tender care. It's entirely doable though: My Business Webmail.
I did not migrate my personal emails and/or business infrastructure until 2021. During 2018 - 2021, I would intermittently test, identify and fix failures, breakages, and read up more on DNS records and worked to gain a deeper understanding of the ecosystem. I spent countless hobbyist hours reviewing forums, Stack Exchange, and, of course, Linux Babe. I also shared notes and perspectives with a local colleague and fellow IT/networking professional, Schaefer Consulting. I was not in any rush to migrate, and I also wanted to develop a system that balanced complexity with reliability/convenience. After initially developing a mail relay recipe both under/alongside Schaefer and on my own, I ultimately decided to switch to postfix for incoming/outgoing, or what I call a proper email server (not merely a relay or send-only MTA). This was pure chance, namely, as the first server recipe I got working for IMAP/dovecot was on my postfix VPS not the exim4 VPS, so I simply got motivated to keep fixing it until it all worked. Until that time, circa 2018, I was tinkering back and forth on two different VPSs, one with exim4 and another with postfix, testing different strategies. To this day, I continue to use exim4 for relaying email from hosts behind NAT. For proper email servers, I currently use postfix. In getting everything to work, my goal was to only increase complexity if/when it was required for proper functioning. For this reason, I chose to use simple UNIX users.
Results: Mail Tester Results
In my case, the servers I built are on VMs that reside on a custom virtualization stack that uses virsh and kvm/qemu on a Debian SuperMicro server (Xeon Silvers) that I co-locate at Brown Rice data center. My virtualization stack has roughly 30 VMs at present, with the host boasting over 500 virtual cores and 384GB of RAM. My primary business email server, for haacksnetworking.org, is an 8-core Virtual Machine with 8GB of RAM that resides on that same SuperMicro. Remember, though, as long as your VPS host provides PTR (reverse DNS) access, you can do this on a very basic ($5-$10 per month) VPS. My Taos server is also the proud host of the following services:
This particular tutorial is CC BY-NC-SA 4.0 instead of FDL 1.3 (like the majority of posts on this Wiki). If you like the tutorials and/or services I provide, please consider giving back at Libera Pay. Alright, let's get this email server set up!
Ultimately, I chose postfix for delivery, dovecot for IMAP/LMTP, and eventually added a LAMP stack for Roundcude serving. (Make sure you know how to set up a basic LAMP stack: Apache Survival.) I only added Roundcube last year! I am a devout Thunderbird user so there was little reason for this at the outset. However, spam and the need to easily set spam rules for different accounts ultimately changed this sentiment after 5 years of using client-side spam filtering. This recipe is designed for 20-100 users tops. After that, you would want some form of graphical and backend user management (LDAP, AD, etc.). This tutorial, however, leverages simple UNIX users names for its scope and targeted audience. This is because for use-cases in the range specified above, it is both more practical and technically beneficial to manually edit and control user names and server access. It's a controlled environment where you as the sysadmin know and have rights to every email, either because there is business-level trust, or family trust. In addition to setting up a sufficiently hardened VM/VPS, the sysadmin must establish A, AAAA, MX, DMARC, SPF, and PTR records for their domain. As for the DKIM record, that's created later when the keypairs are generated. Let's review the records, why they are required and/or important, and what each part of the records mean or drive.
PTR (A): 125.86.28.8.in-addr.arpa domain name pointer mail.haacksnetworking.org. PTR (AAAA): 8.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.4.a.f.4.0.6.2.ip6.arpa domain name pointer mail.haacksnetworking.org.
As a data center co-locater, I have authority to cut and/or request my own PTR entries for both A and AAAA records. The examples above are the results from running host 8.28.86.125 and host 2604:fa40:0:10::18. As you can see from the output, the IP address returns the domain, i.e., reverse DNS. If you don't get that output, stop and sort your PTR issue before proceeding. I do understand that not everyone co-locates and, therefore, it is imperative you discuss or research PTR with your hosting provider before committing to this tutorial. If you don't have access to change/edit PTR, your emails will not reach their destination. To limit spam, rejecting emails without PTR is standard for Google, Microsoft, and even small email servers because this means you can't verify that the sender's IP originates from that domain. In short, lacking this record indicates misconfiguration and/or potential spoofing/abuse of the domain. I noticed that Gmail starting rejecting all of my non-PTR emails in 2015, so that's when I began ensuring they were established. In 2024, Google began additionally requiring that senders have either an SPF or a DKIM record established in order to avoid rejection. It should be noted that Google emails account for roughly 30% of the world's email ecosystem. Microsoft servers and others, moreover, employ similar policies.
A: 8.28.86.125 AAAA: 2604:fa40:0:10::18
Creating A and AAAA records are fairly straightforward. Just ensure that your VM/VPS has those addresses established in your interfaces file if using ifupdown, and established in your yaml configuration if using netplan. I prefer ifupdown and I bridge the ipv6 and ipv4 routes to the VMs (e.g., mail.haacksnetworking.org) using virsh and kvm/qemu. As I mentioned earlier, these interfaces get their routes from my virtualization environment, but you can just as easily do all of this on a VPS host that supports PTR. If, however, you are also interested in setting up your own hosting environment, you can review my Virtualization Stack design.
MX (domain.tld): 0 mail.haacksnetworking.org MX (mail.domain.tld): 10 mail.haacksnetworking.org
Establishing MX records and priority is essential. In my case, I establish an MX record for haacksnetworking.org with the highest priority (0) and mail.haacksnetworking.org with the second highest priority (10). Having both the domain and subdomain MX records established helps ensure redundancy and/or deliverability. The primary record is obviously a hard requirement; the subdomain MX record covers verifications, compliance and error handling. For example, Gmail servers will query subdomain MX during SMTP handshakes and DMARC/SPF alignment checks, and lacking this record means there is no way to validate the target domain via the root MX.
DMARC (domain.tld): v=DMARC1; p=none; fo=1; pct=100; rua=mailto:dmarc@oemb1905 DMARC (mail.domain.tld): v=DMARC1; p=none; fo=1; pct=100; rua=mailto:dmarc@oemb1905
The dmarc record, or domain-based message authentication reporting and conformance record, is a TXT policy that governs how failures (against SPF/DKIM) are handled and how domain owners are notified. I strongly recommend that you keep p=none, which means that your policy is to monitor only, instead of rejecting and/or quarantining failures. It is crucial that you keep records and mail server policies as lenient and permissive as possible during initial setup (or even indefinitely). This eliminates the potential for phantom rejections, i.e., being unable to pinpoint why an email failed to arrive in your server. Once you've mastered this setup and ensured that your base setup has no failures for a reasonably large period of time, it is okay to consider adjusting this flag to be more restrictive (e.g., quarantine, reject). The fo=1 instructs recipient servers to email me at the specified address if and when either SPF or DKIM records fail. Using fo=0 will only notify the domain owner if both fail simultaneously and using fo=d or fo=s are for individually reporting DKIM and SPF failures, respectively. I recommend starting with fo=1 so you can receive reports any time either record fails. For ease, make sure that the destination email you specify in the DMARC record uses the same domain. If you are unable to do that, you must specify a TXT record on the target domain for v=DMARC1; for the receiving subdomain targetdomain.org._report._dmarc. If you do not do this, servers will not send you reports since they will be unable to authenticate the destination email as being a valid target. This is because common servers, for example Gmail, will perform a verification record check (RFC 7489 Section 7.2.1, etc.) in order to ensure their dmarc reports are not being sent to unintended recipients.
SPF (domain.tld): v=spf1 a mx ip4:8.28.86.125 ip6:2a10:e780:10:28::2 ~all SPF (mail.domain.tld): v=spf1 a mx ip4:8.28.86.125 ip6:2a10:e780:10:28::2 ~all
The SPF record, or sender policy framework, instructs mail servers about which IPs are permitted to send email from a given domain. I include the a flag for redundancy and or for safety. Allowing any A record in the domain to send using this domain is very permissive and also helps in case you misconfigure an explicit record while testing. The mx flag ensures that email sent from my own MX servers is explicitly permitted. Although not essentially required to include a or mx if your server's IP is specified, I find it helpful to provide extra resilience for cases where you change or scale. For example, if I decide to add a backup mail server, the mx flag automatically approves it, i.e., I don't need to change the record each time. If I migrate to a different node and/or IP, the A specification allows sending immediately and I can edit the individual TXT records later. So, specifying all three together (A, MX, and IPs) is beneficial for updating, changing, or scaling up your email offering. Additionally, bounces are often sent from the mx server, not the outbound IP, which could potentially differ from the IP of the primary MTA. Some email servers, moreover, compare spf IP to sender domain, so including a and mx alongside the IPs provides full alignment and therefore better email health. As for the ~all, I use that because it specifies “soft fail” if an origin IP does not align. This is important because mistakes happen and sysadmins don't want phantom rejections. If you instead set this flag to -all (reject aka hard fail), then recipients won't receive emails if you configured an IP or DNS record in error. Conversely, setting the flag to +all defeats the purpose of the record by permitting everything. The soft fail flag ~all allows sysadmins to identify misconfigurations and fix them, while ensuring that no emails are auto-rejected by the recipient due to your own spf policy. Of course, after sufficient testing, you can optionally change this flag to hard fail, but proceed with caution. The pct flag controls what portion of emails should be evaluated by dmarc, which I keep at 100%. By specifying 100%, pct=100, you are stating that all your emails should be checked. In thus testing the whole population (instead of a random sample) of your emails, you increase your own chances of failing merely by increasing the number of evaluated emails. However, it is important to remember that mail servers rarely reject incoming email due to dmarc failure alone (almost never), so just because you increase dmarc failures, those won't result in rejections per se. As long as all the other records are solid, I prefer to error on the side of getting more reports and every email evaluated, rather than permitting phantom allowances.
The first part of the tutorial, i.e., setting up DNS records, is now complete. It's now time to address installation and setup of postfix+dovecot. Do not proceed to this part of the tutorial unless you completed the DNS steps above. Put simply, postfix+dovecot both require proper DNS resolution to work. The only exception to this rule is for our DKIM record, which requires us first configuring the server before we cut the keypair and create the associated TXT record. Other than that, make sure DNS is ready to go before proceeding. Okay, let's ssh into the VM/VPS and do the following:
sudo apt update && sudo apt upgrade -y sudo apt install mailutils postfix ufw fail2ban nginx apache2 php8.4-fpm php8.4-mysql php8.4-curl php8.4-gd php8.4-mbstring php8.4-xml php8.4-zip dovecot-core dovecot-imapd dovecot-lmtpd
It's also important that that the host knows how to identify itself properly. Let's open it up /etc/hosts and make it is setup correctly.
127.0.0.1 localhost 127.0.1.1 mail.haacksnetworking.org haacksnetworking ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters 8.28.86.125 mail.haacksnetworking.org localhost
Make sure that /etc/hostname and /etc/mailname are both set to mail.domain.com. The last line ensures that local processes will resolve the fully qualified domain name of the host to the external IP instantly and without the need for external DNS. In short, you are declaring that the fqdn is always 8.28.86.125. Next, let's install common mail utilities and postfix. Remember, postfix is going to be used for SMTP and/or as the outgoing mail server.
sudo apt-get install mailutils postfix -y
For the mail name, put haacksnetworking.org or domain.com.
Leave most fields at default values; make sure other destinations populated correctly.
Do not select All unless you have properly configured records and interfaces for both. Only select and specify what you have records for, otherwise they will fail if they hop to the unsupported protocol. I speak from direct experience.
For the mail name, put haacksnetworking.org or domain.com. Leave most fields at default values; make sure other destinations populated correctly. Do not select All unless you have properly configured records and interfaces for both. Only select and specify what you have records for, otherwise they will fail if they hop to the unsupported protocol. I speak from direct experience. Now that a basic postfix setup is in place, you can optionally install a firewall. If you choose to do this, I recommend the Uncomplicated Firewall, or ufw. Install it and open up all the ports required by postfix+dovecot and no others.
sudo apt install ufw sudo ufw allow 22/tcp 25/tcp 587/tcp 143/tcp 465/tcp 993/tcp 80 443 4190 sudo ufw enable
You should also setup fail2ban, which is outside the scope of this tutorial, but refer to my write-up if you need help! Next, I specified the incoming mail size quota, which I set to 50MB. This is plenty large for attachments and most folks know to use DAV or something else instead if they need to send more and/or larger attachments, etc.
sudo postconf -e message_size_limit=52428800
Next, I edited the aliases file in order for messages internal to the host to get routed to an external address. Presuming that you've created the user webmaster on the mail server, all messages will for postmaster and/or root, will be delivered to webmaster@haacksnetworking.org or webmaster@domain.com.
sudo nano /etc/aliases <postmaster: root> <root: webmaster> sudo newaliases
Make sure that 00-default.conf in /etc/apache2/sites-enabled/ has the ServerName, ServerAdmin, and DocumentRoot parameters specified for the domain.com, or haacksnetworking.org. Additionally, make a new vhost, mail.haacksnetworking.org.conf or mail.domain.com for what will later be the Roundcube instance. Once both sites are enabled with a2ensite, you can proceed to setting up TLS with Let's Encrypt on both. Trust me when I say that setting up the default vhost and using this command below - and then later editing the Roundcube vhost config - is far simpler than doing stand-alone and/or trying to directly add the cert to the reverse proxy vhost.
sudo apt install certbot letsencrypt python3-certbot-apache\ sudo certbot --authenticator standalone --installer apache -d domain.com --pre-hook "systemctl stop apache2" --post-hook "systemctl start apache2"
Make sure to create vhost-mail.conf and enable it with a2ensite for the mail.haacksnetworking.org or mail.domain.com and run the same certbot command again for Let's Encrypt to create a cert for this subdomain as well. You need to specify and create a different webroot for ServerName, ServerAdmin, and DocumentRoot this subdomain, just like the parent domain. Don't run the certbot commands on both domains together as it might interpret that command as being wildcards and/or nested certificates (e.g., multi-site WordPress). When you installed postfix, it automatically populated the global config file over in /etc/postfix/main.cf with the specified choices. It's now time to configure individual services for postfix (smtpd, submission, etc.), so open up /etc/postfix/master.cf and add these blocks where appropriate. You can view an actual config of mine in the link below this block.
submission inet n - y - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o smtpd_tls_wrappermode=no -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject -o smtpd_sasl_type=dovecot -o smtpd_sasl_path=private/auth smtps inet n - y - - smtpd -o syslog_name=postfix/smtps -o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject -o smtpd_sasl_type=dovecot -o smtpd_sasl_path=private/auth
Download master.cf
The changes above add service definitions and configurations for smtps (465) and submission (587), both of which handle mail submission, or the sending of email. Examples of these configurations can be found in /usr/share/doc/postfix/examples/. Submission is the newer protocol, but I retain smtps for compatibility with older clients. Once that's done, it's time to edit /etc/postfix/main.cf to account for the TLS certificate we just cut with Let's Encrypt a few steps earlier. We can additionally specify for postfix to use auth mechanisms that leverage this cert and are consistent with the services setup in master.cf. Let's open /etc/postfix/main.cf and review/add/edit the following:
# A) Leave the following upper-block defaults smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU) biff = no append_dot_mydomain = no compatibility_level = 2 # B) TLS parameters - comment out the self-signed and/or redundant #smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem #smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key #smtpd_tls_security_level=may #smtp_tls_CApath=/etc/ssl/certs #smtp_tls_security_level=may #smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache # C) Enable TLS Encryption for incoming mail (smtpd) using the LE pair smtpd_tls_cert_file=/etc/letsencrypt/live/mail.haacksnetworking.org/fullchain.pem smtpd_tls_key_file=/etc/letsencrypt/live/mail.haacksnetworking.org/privkey.pem smtpd_tls_security_level = may smtpd_tls_loglevel = 1 smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache # D) Enable TLS Encryption for outgoing mail (smtp) smtp_tls_security_level = may smtp_tls_loglevel = 1 smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache # E) Reject all incoming/outgoing protocols besides TLSv1.3 or TLSv1.2 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 # F) Primary Block; additional services go below or in dedicated config smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination myhostname = mail.haacksnetworking.org alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases myorigin = /etc/mailname mydestination = mail.haacksnetworking.org, haacksnetworking.org, mail.haacksnetworking.org, localhost.haacksnetworking.org, localhost relayhost = mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 mailbox_size_limit = 0 recipient_delimiter = + inet_interfaces = all inet_protocols = all message_size_limit = 52428800
Download main.cf
The options above have comments that describe each block. The block which rejects old protocols was taken from Linux Babe, but the basis for that is covered in the Postfix documentation. Using the Let's Encrypt keypair is straightforward as are most of the lines in the primary block. This base config reflects months and even years of fine-tuning. It's tested and reilable. It's now time to install dovecot, which is our IMAP server for incoming email. We will have to return to postfix's main.cf later and specify that we are using dovecot. But, to do that, we need to install dovecot, which is our incoming mail server (imap).
sudo apt install dovecot-core dovecot-imapd dovecot-lmtpd
In order for dovecot to work, we need to specify which protocols we allow, the mail directory location, ensure that dovecot is a member of the mail group, and enable lmtp the for delivery agent. Lmtp is a local delivery agent and hands off processed incoming emails to the storage directories without requiring external authentication. This ensures everything is delivered locally, or within the hardened host. Larger distributed email systems require smtp for network hand offs between servers, but that's not required or helpful for mail server self-hosters. Open up /etc/dovecot/dovecot.conf and specify the protocols. It is also required to specfify the storage and config versions (updated for Trixie):
protocols = imap lmtpdovecot_storage_version = 2.4.1 dovecot_config_version = 2.4 # If you upgraded from Bookworm, comment out dictionary # dict { # quota = file:/var/lib/dovecot/quota # }
Next, we need to edit /etc/dovecot/conf.d/10-mail.conf (updated for Trixie):
mail_driver = maildirmail_path = ~/Maildirmail_inbox_path = ~/Maildir/.INBOX
Make sure that the dovecot user and the simple UNIX users are both part of the mail group. Remember, this tutorial does not use virtual users and/or an associated database. This tutorial uses simple UNIX user names which means the yourusername listed below will have the email yourusername@haacksnetworking.org, or yourusername@domain.com.
sudo adduser yourusernamesudo adduser dovecot mailsudo adduser yourusername mail
It is required that lmtp be configured for use with dovecot. To do that, edit /etc/dovecot/conf.d/10-master.conf:
service lmtp { unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix } }
Postfix also needs to know the delivery agent that's being used, otherwise it won't know what service to which it should send incoming email. In short, lmtp hands off emails from postfix's mail queue to dovecot's mail storage. This is done via a socket, so that needs to be configured in the main postfix configuration. Open up /etc/postfix/main.cf and define the socket as follows. It is recommended that you also disable UTF-8 support in headers and addresses so that legacy mail servers can handle emails from your server.
mailbox_transport = lmtp:unix:private/dovecot-lmtpsmtputf8_enable = no
It's now time to instruct dovecot as to what authorization mechanisms to accept. Remember, plain login, contrary to popular belief, is not in any way insecure. Remeber that they both use Base64 encoding to begin with, which is then wrapped in TLS. Therefore, as long as you enforce TLS, there's very little security concern. Adding strong passwords with 14 or more characters and regularly rotating client credentials limits the surface vector even moreso. Okay, to setup the login mechanisms, open up /etc/dovecot/conf.d/10-auth.conf and configure the following parameters (updated for Trixie):
auth_username_format = %{user|username|lower} auth_mechanisms = plain login
Now, let's enforce ssl so that the plain and login mechanisms above are wrapped - end to end - in TLS handshakes. It is also important to explicitly define the least secure and/or oldest TLS handshake your server will permit. Additionally, make sure that you comment out ssl_prefer_server_ciphers and/or anything like ssl_server_dh file, both of which are now deprecated. To do this, open up /etc/dovecot/conf.d/10-ssl.conf and edit the following parameters (updated for Trixie):
ssl = required ssl_server_cert_file = /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem ssl_server_key_file = /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem ssl_min_protocol = TLSv1.2
Add SASL listener in /etc/dovecot/conf.d/10-master.conf so that postfix can query dovecot and this socket for user credentials for incoming email. Combined with the configuration smtpd_sasl_path=private/auth in main.cf, this also allows postfix to authenticate smtp requests. In all cases, dovecot leverages the UNIX users for credentials.
service auth { unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix } }
Lastly, before testing, make sure that you only authorize your mynetworks and properly authenticated users. Failing to do this will mean your server could potentially be used for public relay. This block rejects any unauthenticated senders (besides localhost) and requires senders to be authenticated (or to be localhost) while only permitting incoming email directed to @haacksnetworking.org or @domain.com. Please note that if you continue with the optional configurations later in this tutorial, you will integrate these stanzas into other blocks.
smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, rejectsmtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
At this time, the bare minimum requirements are in place for sending and receiving email. You can either fire up Thunderbird and field test it all, or use swaks, ncat, telnet, or openssl to test the TLS handshakes. Here's the ones I use in addition to field testing with Thunderbird:
openssl s_client -connect mail.yourdomain.com:465openssl s_client -connect mail.yourdomain.com:587 openssl s_client -connect mail.yourdomain.com:993openssl s_client -starttls smtp -connect mail.yourdomain.com:25swaks --tls --server mail.yourdomain.com -p 587swaks --tls --server mail.yourdomain.com -p 465
The above steps conclude what I call the base server. You must complete the testing above and ensure that your TLS handshakes and base server communications are functional before you proceed.
In the section that follows, I will cover the following:
The last outgoing server step, namely creating a DKIM keypair for smtp, will raise the server's email health to 10/10 once configured properly. Please remember to use the mail-tester.com website to test your server's outgoing health and pinpoint misconfigurations during this tutorial. The remaining incoming server refinements are how one manages incoming email according to the policies they create, including policies for spam. In this tutorial, you are taught how to create three server policies for spf, dkim, and dmarc, which spam assassin will use to rank your email. From there, I show you how to use the sieve language - either on its own via the CLI or via Roundcube's GUI - both of which instruct dovecot what to do with email based on certain conditions that spam assassin gathers about your email.
As with elsewhere, this tutorial does not use any of the policies for rejecting email - everything is accepted. As your server matures and you complete testing, you can optionally harden your server's policies, but bear in mind, this means you are explicitly configuring it to reject incoming email in some cases. So, if those rules aren't perfect, then you are guaranteeing the possibility of an invalid rejection, or a false negative. In lay terms, that means you are choosing to not get emails from people that send to you. So, proceed with caution if you choose to make server-wide reject rules, either in postfix generally and/or with your policies. Personally, I don't ever do that. I want to receive any and all incoming emails. Let's start this second part by installing our spf policy with sudo apt install postfix-policyd-spf-python. After it's installed, you can configure the policy daemon and socket in /etc/postfix/master.cf by adding this at the bottom of the config:
#spf policy\ policyd-spf unix - n n - 0 spawn\ user=policyd-spf argv=/usr/bin/policyd-spf
After that, open up /etc/postfix/main.cf and put the spf policy together with postfix's recipient restrictions. The reason I organize the block this way is because it makes logical sense to group together the criteria under which email is received, whether because it is postfix's general policy and/or your SPF policy.
#spf incoming policy and recipient restrictions\ policyd-spf_time_limit = 3600\ smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, check_policy_service unix:private/policyd-spf
Make sure you understand what each line does. In short, this mandates authentication (except for localhost), and does not allow use of the server for public relay, i.e., to deliver emails for other domains besides haacksnetworking.org or domain.com. Okay, I now disable the SPF policy's default behavior of rejecting emails when the HELO/EHLO hostname does not match the sender's SPF policy. I also disable the second line as well, which checks the origin IP against the SPF policy of the sender. In short, I instruct the SPF policy to log failures instead of rejecting emails because of it. This is also essential for spam assassin, which will later use this policy to identify and rank the health of incoming email, thus allowing you to create management rules for priority senders or to manage spam. Head over to /etc/postfix-policyd-spf-python/policyd-spf.conf and edit:
HELO_reject = False Mail_From_reject = False
Since I just took care of recipient restrictions, it makes sense to cover sender restrictions. Remember, in the base server, both recipient and sender configurations were covered in brief. If you do these recommended and optional steps, you should expand your sender restrictions block and include other forms of checking. Some of the those options are below. Please note that similar hardening can also be done via postfix on the recipient-side as well:
smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, #reject_unknown_reverse_client_hostname, #reject_unknown_client_hostname, #reject_unknown_sender_domain, reject_unauthenticated_sender_login_mismatch, reject_sender_login_mismatch, permit
Now that the SPF/recipient and PTRI am now going to set up the DKIM policy for incoming email but/and, I will also use the same package to cut a DKIM keypair for use with the server's outgoing email. First, I install the policy and add opendkim to the postfix group.
sudo apt install opendkim opendkim-tools sudo adduser postfix opendkim
Once that's done, it's time to open /etc/opendkim.conf and setup the configuration for the server's keypair:
Canonicalization relaxed/simple Mode sv SubDomains no Nameservers 8.8.8.8,1.1.1.1 KeyTable refile:/etc/opendkim/key.table SigningTable refile:/etc/opendkim/signing.table ExternalIgnoreList /etc/opendkim/trusted.hosts InternalHosts /etc/opendkim/trusted.hosts
It's now time to create the keys:
sudo mkdir -p /etc/opendkim/keys sudo chown -R opendkim:opendkim /etc/opendkim sudo chmod 711 /etc/opendkim/keys sudo mkdir /etc/opendkim/keys/yourdomain.com sudo opendkim-genkey -b 2048 -d yourdomain.com -D /etc/opendkim/keys/yourdomain.com -s default -v sudo chown opendkim:opendkim /etc/opendkim/keys/yourdomain.com/default.private sudo chmod 600 /etc/opendkim/keys/yourdomain.com/default.private
After creating the keys, edit the signing table so that outgoing emails can verify against the keypair. Open up /etc/opendkim/signing.table and add:
'*@yourdomain.com default._domainkey.yourdomain.com' '*@*.yourdomain.com default._domainkey.yourdomain.com'
Similarly, in the key table, over in /etc/opendkim/key.table make sure specify the selector that your server (and associated DNS record) will use for DKIM verification:
default._domainkey.yourdomain.com yourdomain.com:default:/etc/opendkim/keys/yourdomain.com/default.private
Lastly, instruct you server to trust only localhost and your domain. Open up /etc/opendkim/trusted.hosts and enter:
127.0.0.1\ localhost\ .yourdomain.com
Use the built-in tool to test your DKIM keypair:
sudo opendkim-testkey -d yourdomain.com -s default -vvv
It's now time to build your last DNS record. To do that, you need your public key you created above. To view that on the CLI, run the cat command below. You then take that output to your DNS host of choice, and create a TXT record containing this value. In the subdomain field, you enter your selector, i.e., default._domainkey and in the target, you enter the output of the command below.
sudo cat /etc/opendkim/keys/yourdomain.com/default.txt
Okay, now that your keypair for smtp has been created and DNS updated, it's a good time to create the server's DKIM policy. First, you need to give postfix access to opendkim's tooling:
sudo mkdir /var/spool/postfix/opendkim\ sudo chown opendkim:postfix /var/spool/postfix/opendkim
After that's done, you need to open /etc/opendkim.conf and instruct your DKIM policy as to which socket to utilize. This is not a distributed mail system, but a small selfhosted one. Accordingly, this specifies to use localhost for the socket.
Socket local:/var/spool/postfix/opendkim/opendkim.sock
It is also required to specify this socket in the global configuration file for the DKIM policy. Head over to /etc/default/opendkim and add the following:
SOCKET="local:/var/spool/postfix/opendkim/opendkim.sock"
Now that the server's DKIM policy is configured, it's required to let postfix know of the policy, where the socket is, and how to leverage the policy (reject/accept). Please note that the other policies will add to and/or expand this block, notably the smtpd_milters stanza, which will expand with each additional policy and associated socket that gets added. Open up the main postfix configuration in /etc/postfix/main.cf and enter the following:
milter_default_action = accept milter_protocol = 6 smtpd_milters = local:opendkim/opendkim.sock non_smtpd_milters = $smtpd_milters
The most important line is the uppermost line, which specifies that email should not be rejected as a result of leveraging the policy. Again, this ensures that spam assassin has what it needs to help users sort and organize email, without the possibility of phantom rejections, prohibiting email from having ever arrived in your inbox. After your DKIM keypair and DKIM policy are setup, you can setup a DMARC policy as well. Install the policy with sudo apt install opendmarc. After it installs, open up /etc/opendmarc.conf and enter the following:
AuthservID OpenDMARC TrustedAuthservIDs mail.yourdomain.com RejectFailures false IgnoreAuthenticatedClients true RequireHeaders true SPFSelfValidate true Socket local:/var/spool/postfix/opendmarc/opendmarc.sock
Similarly to the DKIM policy, we need to give postfix permissions to the DMARC tooling and socket. It's also required to add opendmarc to the postfix group:
sudo mkdir -p /var/spool/postfix/opendmarc sudo chown opendmarc:opendmarc /var/spool/postfix/opendmarc -R sudo chmod 750 /var/spool/postfix/opendmarc/ -R sudo adduser postfix opendmarc sudo systemctl restart opendmarc
Similarly to SPF and DKIM, it's essential to define the policy in the main postfix configuration file. Open up /etc/postfix/main.cf and add a block for dmarc. Expand the smtpd_milters section to include DMARC's socket. Note again how I specify accept so as to ensure proper logging for spam assassin without any possibility of phantom rejections, or email lost at the gates.
#dmarc policy milter_default_action = accept milter_protocol = 6 smtpd_milters = local:opendkim/opendkim.sock,local:opendmarc/opendmarc.sock non_smtpd_milters = $smtpd_milters
Now that all three policies are established and postfix is sufficiently hardened for recipients and senders, it's time to install and configure spam assassin (for assessing email health) and then set up sieve rules (to manage spam and/or email). Sieve rules can be set up server-wide for simpler or more granular approaches, and/or you can setup Roundsube to manage sieve for the server and/or users. I have instances that do both. It just depends on use-case. First, let's install spam assassin and dovecot's sieve tooling:
sudo apt install dovecot-sieve dovecot-managesieved spamassassin spamc spamass-milter
After that's done, open up dovecot's primary configuration /etc/dovecot/dovecot.conf and add the sieve to the permitted protocols stanza:
protocols = imap lmtp sieve
First, let's add sieve to the local delivery agent over in /etc/dovecot/conf.d/15-lda.conf. This is essential for any time you are manually testing and/or processing emails on the shell, which will utilize the LDA instead of LMTP. This ensures that the local delivery agent is sieve-ready. Head over to /etc/dovecot/conf.d/15-lda.conf and edit the block:
protocol lda { mail_plugins = $mail_plugins sieve }
It is also imperative that LMTP be configured for using sieve as well. Head over to /etc/dovecot/conf.d/20-lmtp.conf and edit the block:
protocol lmtp { mail_plugins = quota sieve }
As with any service, it's essential to configure postfix to use it. So, head to /etc/postfix/main.cf and add the spam assassin socket to the primary milter. Just as with DKIM and DMARC, you are expanding the existing smtpd_milters clause, not making redundant and/or duplicate stanzas. Head over to /etc/postfix/main.cf and edit the policy block we created above for DKIM, DMARC, and now spam assassin.
milter_default_action = accept milter_protocol = 6 smtpd_milters = local:opendkim/opendkim.sock,local:opendmarc/opendmarc.sock,local:spamass/spamass.sock non_smtpd_milters = $smtpd_milters
Configure the spam assassin users and restrict it to being managed by localhost by opening /etc/default/spamass-milter and ensuring this line is uncommented:
OPTIONS="-u spamass-milter -i 127.0.0.1"
The next part of the tutorial covers:
Sieve is already enabled in dovecot and postfix. That was done up above. Now, the tutorial is discussing how to leverage the sieve syntax or language to get desired user results for incoming email. The most basic way to do this is by setting up a global, or server-wide rule, that filters emails before dovecot, via lmtp, delivers the emails to their final destination. This is done by spam assassin adding custom fields and scoring to incoming email headers. The sieve plugin assesses these headers and then makes the correct determination for the final destination, which dovecot, via lmtp, carries out. In summary, spam assassin does the ranking and header-adding, sieve assesses the spam assassin scores and uses the global sieve rule to make determinations for all users, and finally dovecot+limtp handles the mail delivery. To create a global config for all users in this way, head over to /etc/dovecot/conf.d/90-sieve.conf and then add/uncomment this line:
sieve_before = /var/mail/SpamToJunk.sieve
Now, create the file that you just referenced above in /var/mail/SpamToJunk.sieve and enter the following:
require "fileinto"; if header :contains "X-Spam-Flag" "YES" { fileinto "Junk"; stop; }
Load the rule by issuing the sudo sievec /var/mail/SpamToJunk.sieve command. I would also recommend restarting postfix, dovecot, and spam assassin. Now, the header called X-Spam-Flag is populated with either a Yes or No response by the spam assassin service. Spam assassin determines the yes/no score based on the scoring rules specified in the configuration file. There are many other options and far more complex rules one can establish than this basic example. This is just to get folks started. Here's an example set of rules from the /etc/spamassassin/local.cf main configuration file:
report_contact webmaster@yourdomain.com required_score 5.0 report_safe 0 ifplugin Mail::SpamAssassin::Plugin::Shortcircuitendif # Mail::SpamAssassin::Plugin::Shortcircuit # uncomment the line below once unbound is working (optional) # dns_server 127.0.0.1score MISSING_FROM 5.0 score MISSING_DATE 5.0 score MISSING_HEADERS 3.0 score PDS_FROM_2_EMAILS 3.0 score FREEMAIL_FORGED_REPLYTO 3.5 score DKIM_ADSP_NXDOMAIN 5.0 score FORGED_GMAIL_RCVD 2.5 score FREEMAIL_FORGED_FROMDOMAIN 3.0 score HEADER_FROM_DIFFERENT_DOMAINS 3.0 score FREEMAIL_FROM 3.0 score ACCT_PHISHING 3.0 score AD_PREFS 3.0 score ADMAIL 3.0 score ADMITS_SPAM 3.0 score CONFIRMED_FORGED 3.0 score FROM_PAYPAL_SPOOF 3.0 score SPF_SOFTFAIL 2.0 score SPF_FAIL 5.0 whitelist_from *@statefarm.com blacklist_from *@email.freethinkerdaily.com
Download local.cf:
This basic configuration shows you where to place the rules/scoring, etc. The names above are referred to technically as symbolic headers and I found the examples above by searching documentation and/or forum hunting. I also spent time reviewing the rules for accuracy and testing with spamassassin -t -D < example.eml regularly until assassing scored emails properly. It takes time to perfec this, so keep it permissive for starters. Make sure to check logs regularly for errors/clues using journalctl -u spamass-milter -u postfix -u dovecot -u opendkim -u opendmarc. This will help you track what is and is not working for spam assassin and for you, and to thereby adjust/alter/remove scores or change points to fit your use-case and preferences. The whitelist and blacklist options can be scaled as needed and are self-explanatory. This setup is very elegant and helpful for single user email servers and/or tight-knit and close groups of family/people. As more users are needed, the ability of a one-sizefits-all rule to meet everyone's individual needs becomes more and more difficult. For this reason, I chose to install Roundcube in order to leverage the filters feature in the webgui to more easily manage spam rules. Here's how to install Roundcube and use it to manage sieve.
cd /var/www wget https://github.com/roundcube/roundcubemail/releases/download/1.6.1/roundcubemail-1.6.1-complete.tar.gz tar xvf roundcubemail-1.6.1-complete.tar.gz ln -s roundcubemail-1.6.1/ roundcube chown root:root -R roundcube cd roundcube sudo chown www-data:www-data temp/ logs/ -R sudo apt install software-properties-common php-net-ldap2 php-net-ldap3 php-imagick php8.4-fpm php8.4-common php8.4-gd php8.4-imap php8.4-mysql php8.4-curl php8.4-zip php8.4-xml php8.4-mbstring php8.4-bz2 php8.4-intl php8.4-gmp php8.4-redis
Obviously, go check the git repo and make sure to download the latest stable version. Roundcube requires a database, so let's set that up:
sudo mysql -u root CREATE DATABASE roundcube DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE USER roundcube@localhost IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON roundcube.* TO roundcube@localhost; FLUSH PRIVILEGES; EXIT;
After the database is setup, import the initial tables into the db for first-time setup:
sudo mysql -u root -p roundcube < /var/www/roundcube/SQL/mysql.initial.sql
Earlier in the tutorial, you were instructed to setup a vhost for mail.haacksnetworking.org / mail.domain.com. You already created vhost-mail.conf for the purposes of having an associated A record and website destination, which is helpful for complete validation of email's record health. Now, you are going leverage this certificate for the purpose of encrypting the apache2 front-end, or reverse proxy. Apache receives the public requests, and then the new vhost config, which enables the reverse proxy, forwards those requests upstream to the roundcube service. First, replace vhost-mail.conf with something like the following:
<VirtualHost *:80> ServerName mail.domain.com ServerAdmin email@email.com DocumentRoot /var/www/roundcube/ ErrorLog ${APACHE_LOG_DIR}/roundcube_error.log CustomLog ${APACHE_LOG_DIR}/roundcube_access.log combined <Directory /> Options FollowSymLinks AllowOverride All </Directory> <Directory /var/www/roundcube/> Options FollowSymLinks MultiViews AllowOverride All Order allow,deny Allow from all </Directory> <FilesMatch ".+\.ph(ar|p|tml)$"> SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost" </FilesMatch> RewriteEngine on RewriteCond %{SERVER_NAME} =mail.domain.com RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] </VirtualHost>
After that's done, edit the vhost-mail-le.conf file with something like the following:
<IfModule mod_ssl.c> <VirtualHost *:443> ServerName mail.domain.com ServerAdmin email@email.com DocumentRoot /var/www/roundcube/ ErrorLog ${APACHE_LOG_DIR}/roundcube_error.log CustomLog ${APACHE_LOG_DIR}/roundcube_access.log combined <Directory /> Options FollowSymLinks AllowOverride All </Directory> <Directory /var/www/roundcube/> Options FollowSymLinks MultiViews AllowOverride All Order allow,deny Allow from all </Directory> <FilesMatch ".+\.ph(ar|p|tml)$"> SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost" </FilesMatch> SSLCertificateFile /etc/letsencrypt/live/mail.domain.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/mail.domain.com/privkey.pem </VirtualHost> </IfModule>
Since the certificate was already created, these configs can just be dropped in to the http vhost and https vhost, respectively. These two vhosts were created earlier by you (http) and subsequently by Let's Encrypt (https). It's now time to connect Roundcube to the database that was created earlier:
cd /var/www/roundcube/config/ sudo cp config.inc.php.sample config.inc.php sudo nano config.inc.php # Remove the "<" and ">" !! <$config['db_dsnw'] = 'mysql://roundcube:pass@localhost/roundcube';> <$config['des_key'] = 'rcmail-!24ByteDESkey*Str';? <$config['imap_host'] = 'startls://domain.com:143';> <$config['smtp_host'] = 'tls://mail.domain.com:587';> <$config['enable_spellcheck'] = true;>
In addition to configuring the database and renaming the sample config, it's imperative to edit the plugins block at the end of the primary configuration as well. Add and/or replace the bottom plugin block as follows:
$config['plugins'] = [ 'archive', 'zipdownload', 'acl', 'additional_message_headers', 'attachment_reminder', 'autologon', 'debug_logger', 'emoticons', 'enigma', 'filesystem_attachments', 'help', 'hide_blockquote', 'http_authentication', 'identicon', 'identity_select', 'jqueryui', 'krb_authentication', 'managesieve', 'markasjunk', 'new_user_dialog', 'new_user_identity', 'newmail_notifier', 'password', 'reconnect', 'redundant_attachments', 'show_additional_headers', 'squirrelmail_usercopy', 'subscriptions_option', 'userinfo', 'vcard_attachments', 'virtuser_file', 'virtuser_query' ];
At this point, you should be able to log in to mail.haacksnetworking.org / mail.domain.com using a common web browser and your credentials. Your user name is the UNIX user name, i.e., only the handle without the domain. So, my user name is jonathan, for example … and without haacksnetworking.org at the end. Your password is whatever you adduser yourusername specified when creating your simple UNIX user. At this point, you want to open your web browser and login. Head to the Filters section under Settings. Click the cog at the top and select Edit Filter Set. In the block to the right, replace the contents with the following:
require ["fileinto"]; # rule:[whitelist] if header :contains "from" ["nice@goodemail.com","happy@ham.com"] { keep; stop; } # rule:[blacklist] if header :contains "from" ["powell@supportbaggies.com","bad@email.com"] { fileinto "Junk"; stop; } # rule:[spamcheck] if anyof (header :contains "x-spam-status" "Yes", header :contains "x-spam-flag" "YES", header :contains "x-spam-level" "*****") { fileinto "Junk"; stop; }
roundcube-edit-filter-set.sieve
Use this as a jumping-off point. Once you save it, it will change your user interface and allow you to add/edit rules using the nested filters that were just created with this sieve config. Remember that the Roundcube filters and associated sieve configs it is creating on the underbelly are processed in order linearly. Consider this well when developing additional AND/OR logic rules beyond the scope of whitelist, blacklist, and spam check sequence. Now, subsequent editing is very convenient, and user-based. Make sure to comment out sieve_before = /var/mail/SpamToJunk.sieve in /etc/dovecot/conf.d/90-sieve.conf if you choose to only use this method. It's also fine to retain the server-wide rule and use that for exclusively global changes that fit the community.
As you can see in the asset above, you now have access to refined sieve rules for each user. Once I decided that I needed or wanted user-level spam controls, it no longer made sense for me to manage this exclusively on the CLI. For this reason, I added Roundcube and began managing spam for email accounts individually. When Trixie came out, Dovecot's sieve implementation needed a bit of fine-tuning. First, head to /etc/dovecot/conf.d/90-sieve.conf and change the block as follows:
# Comment out the old block: #plugin { #sieve = file:~/sieve;active=~/.dovecot.sieve #} # Enable the new block: sieve_script personal { driver = file path = ~/sieve active_path = ~/.dovecot.sieve }
It's important to be able to monitor how your setup is performing and what is or is not working correctly. No better way to do that than to get some analytics emailed to you each day. To do that, let's install pflogsumm and use rsyslog for logging. Install the packages sudo apt install pflogsumm rsyslog and then create the log rotation rule over in /etc/logrotate.d/postfix-log and enter the following:
/var/log/mail.log { missingok daily rotate 7 create compress start 0 }
You need to make sure to comment out or remove #mail.log from /etc/logrotate.d/rsyslog. This is done so as to replace the default rotation. Now, I will create a script to run pflogsumm on the archives. Create /usr/local/bin/pflog-run.sh or something similar and enter the following:
#!/bin/sh #/usr/sbin/logrotate -f /etc/logrotate.d/postfix-log gunzip /var/log/mail.log.0.gz /usr/sbin/pflogsumm /var/log/mail.log.0 --problems-first --rej-add-from --verbose-msg-detail -q | mail -s "[pflog-lastlog]-$(hostname -f)-$(date)" alerts@haacksnetworking.org gzip /var/log/mail.log.0 sleep 2s systemctl restart rsyslog systemctl restart postfix systemctl restart dovecot exit 0
Next, open crontab -e and create a job to run this script daily at a time of your choosing. Here's mine:
30 12 * * * /bin/bash /usr/local/bin/pflog-run.sh >> /home/logs/pflog-run.log
I later learned this is far easier and you can just use the yesterday flag. Who knew?!
usr/sbin/pflogsumm /var/log/mail.log -d yesterday --problems-first --rej-add-from --verbose-msg-detail -q | mail -s "[pflog-lastlog]-$(hostname -f)-$(date)" alerts@haacksnetworking.org
Some of the spam assassin tooling that uses RBL will not work unless you use your own recursive DNS instead of, for example, 8.8.8.8. It should also be noted that using your own DNS is better from both a speed and privacy perspective, so setting this up kills two birds with one proverbial stone. I have a separate tutorial about this on my blog and it has an associated wiki entry as well. As you review these tutorials, please remember that unbound can be used for both LAN and WAN settings. For this tutorial, an email server, one would use the WAN configuration. Here's a concise recap of how to set that up, which you will do directly on the email server. First, let's install unbound sudo apt install unbound, by far the most hassle free way to do recursive DNS. Once it is up and running, it's time to put a WAN-based config in place in /etc/unbound/unbound.conf. In that file, add the following:
server: # Bind to localhost only interface: 127.0.0.1 interface: ::1 port: 53 do-ip4: yes do-ip6: yes prefer-ip6: yes access-control: 127.0.0.0/8 allow access-control: 0.0.0.0/0 refuse access-control: ::0/0 refuse # Optimize for 8 cores num-threads: 4 msg-cache-slabs: 4 rrset-cache-slabs: 4 infra-cache-slabs: 4 key-cache-slabs: 4 # Cache settings for high query volume cache-max-ttl: 86400 cache-min-ttl: 3600 rrset-cache-size: 128m msg-cache-size: 64m key-cache-size: 32m neg-cache-size: 8m # Enable prefetch and expired responses prefetch: yes prefetch-key: yes serve-expired: yes serve-expired-ttl: 3600 # DNSSEC validation for DANE #do-dnssec: yes harden-dnssec-stripped: yes harden-referral-path: yes harden-below-nxdomain: yes harden-algo-downgrade: no # Performance tweaks #so-rcvbuf: 4m #so-sndbuf: 4m edns-buffer-size: 1232 outgoing-range: 4096 num-queries-per-thread: 1024 jostle-timeout: 200 #low-resolver-mem: no # Logging (minimal) verbosity: 1 log-queries: no log-replies: no use-syslog: yes # Security and privacy hide-identity: yes hide-version: yes use-caps-for-id: yes qname-minimisation: yes harden-large-queries: yes harden-glue: yes aggressive-nsec: yes # Protocol settings do-tcp: yes do-udp: yes # Enable full recursion - no longer needed, retained for history # do-not-query-localhost: no # root-hints: "/usr/share/dns/root.hints" # Disable subnetcache module-config: "validator iterator" # Forward to upstream resolvers # forward-zone: # name: "." # forward-addr: 1.1.1.1 # Cloudflare # forward-addr: 8.8.8.8 # Google
You should certainly familiarize yourself with all of these settings. This tutorial itself, took a few months of research and time. In addition to this primary config, you also need to set up logging and app armor. For added safety, I disable systemd's built-in resolver as well. Open up /etc/rsyslog.d/unbound.conf and add
if $programname == 'unbound' then /var/log/unbound/unbound.log & stop
Next, open up /etc/logrotate.d/unbound and enter the rotation logic:
/var/log/unbound/unbound.log { daily rotate 7 missingok create 0640 root adm postrotate /usr/lib/rsyslog/rsyslog-rotate endscript }
Next, disable systemd's resolver:
systemctl disable --now unbound-resolvconf.service sed -Ei 's/^unbound_conf=/#unbound_conf=/' /etc/resolvconf.conf rm /etc/unbound/unbound.conf.d/resolvconf_resolvers.conf
After that, set up apparmor. In nano /etc/apparmor.d/local/usr.sbin.unbound add the following:
/var/log/unbound/unbound.log rw,
After that, run the following commands to set everything up:
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.unbound sudo service apparmor restart sudo mkdir -p /var/log/unbound sudo touch /var/log/unbound/unbound.log sudo chown unbound /var/log/unbound/unbound.log
Once you've completed this, you can change /etc/resolv.conf to reflect this setup and then reboot:
nameserver ::1 nameserver 127.0.0.1
Use ping4 and/or ping6 to test routing after reboot. Remember, the primary reason to do this is so as to enable the server's unbound recursive DNS server, which allows direct RBL querying and more advanced spam controls. Up above, I pointed out that there was a line commented out in /etc/spamassassin/local.cf which you can now add. Open that config and uncomment:
dns_server 127.0.0.1
Now, your email server can directly query the default RBLs that spam assassin has access to. You should see the errors and failures disappear from your logs now.
Setting up autodiscovery is a nicety that I never did at first. But, at some point, I got bored and added it, somewhere in the middle and/or late in 2024. It turned out to be helpful when setting up new clients manually, as they would, as the name suggests, simply autdiscover the server settings. First, just like we did with domain.com and mail.domain.com, let's setup another A and AAAA record for autodiscover.domain.com and an associated virtual host, e.g., vhost-autodiscover.conf and then after enabling the vhost a2ensite make sure to cut it a cert:
sudo certbot --authenticator standalone --installer apache -d autodiscover.domain.com --pre-hook "systemctl stop apache2" --post-hook "systemctl start apache2"
Set up the following SRV records on your DNS host. Adapt them for autodiscover.domain.com.
Again, make sure that you also setup A and AAAA records up above. Once that's done, the next step is to create an .xml file that the autodiscover subdomain can serve to clients/queries. First, create the autodiscovery subdirectory and .xml file:
sudo mkdir /var/www/autodiscover.haacksnetworking.org/public_html/autodiscover\ sudo nano /var/www/autodiscover.haacksnetworking.org/public_html/autodiscover/autodiscover.xml
In the .xml file, put the following, adjusting for domain.com:
<?xml version="1.0" encoding="UTF-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <Account> <AccountType>email</AccountType> <Action>settings</Action> <Protocol> <Type>IMAP</Type> <Server>mail.haacksnetworking.org</Server> <Port>993</Port> <LoginName>%EMAILADDRESS%</LoginName> <Domain>haacksnetworking.org</Domain> <Encryption>SSL</Encryption> </Protocol> <Protocol> <Type>IMAP</Type> <Server>mail.haacksnetworking.org</Server> <Port>143</Port> <LoginName>%EMAILADDRESS%</LoginName> <Domain>haacksnetworking.org</Domain> <Encryption>STARTTLS</Encryption> </Protocol> <Protocol> <Type>SMTP</Type> <Server>mail.haacksnetworking.org</Server> <Port>465</Port> <LoginName>%EMAILADDRESS%</LoginName> <Domain>haacksnetworking.org</Domain> <Encryption>SSL</Encryption> </Protocol> <Protocol> <Type>SMTP</Type> <Server>mail.haacksnetworking.org</Server> <Port>587</Port> <LoginName>%EMAILADDRESS%</LoginName> <Domain>haacksnetworking.org</Domain> <Encryption>STARTTLS</Encryption> </Protocol> </Account> </Response> </Autodiscover>
Once this is done, clients should have an easy time finding your settings. If not, remember that this tutorial uses:
If your client does not honor autodiscovery and/or you choose to enter manually, use the port recommendations and protocols above. Other options also exist.
The options below are results of small things that came up while using my own server over the last 5 years or so. First, I noticed that clients would not set up the standard directories and it turns out you need to tell dovevot to do that over in /etc/dovecot/conf.d/15-mailboxes.conf by enabling the auto = create in the folder blocks for which you desire auto-population.
mailbox Drafts { auto = create special_use = \Drafts }
The next issue that came up was almost immediate. Upon switching to my own server, I noticed that E*Trade and StateFarm emails stopped arriving. Not all of them, just some. After years of using alternate emails and testing when time permitted, I discovered that the issue was due to the mass email tools changing my email from, for example, user@haacksnetworking.org to USER@haacksnetworking.org. However, as it turns out, according to RFC 2821, Section 2.4, local-parts of email addresses “MUST BE” treated as case sensitive. Dovecot and lmtp enforce this RFC, so these emails were getting rejected. After investigating work arounds, I found a Stack Exchange comment on a related but different thread that suggested using postfix's virtual aliases. To do that, add a block in the main configuration over in /etc/postfix/main.cf and put the following inside:
virtual_alias_maps = regexp:/etc/postfix/virtual_alias
Inside the file /etc/postfix/virtual_alias, enter your aliases. Make sure to append @domain.com otherwise any emails you send to users of the same name will get sent to that user instead of the destination. For example, if I omit the domain in this config, but then send to JONATHAN@jonathanhaack.com OR jonathan@jonathanhaack.com BOTH of those emails will be sent to jonathan@haacksnetworking.org (because the domain was left off the alias). So, make sure to pay attention to how I included the @domain.com in the alias table and why that's essential.
/^[Jj][Oo][Nn][Aa][Tt][Hh][Aa][Nn]@yourdomain.com/ jonathan\ /^[Ww][Ee][Bb][Mm][Aa][Ss][Tt][Ee][Rr]@yourdomain.com/ webmaster
Once you edited the file, load the changes with sudo postmap /etc/postfix/virtual_alias.
Another thing I researched when reviewing Linux Babe's tutorial, but ultimately rejected doing was body and header inspection. To do that, install postfix's regular expression tooling with sudo apt install postfix-pcre and then edit the main configuration /etc/postfix/main.cf and enter the following stanzas:
header_checks = pcre:/etc/postfix/header_checks body_checks = pcre:/etc/postfix/body_checks
Once the tool is installed and postfix has been instructed where to look for the configuration files, you can create the files and put in expression checking. Here are some options:
In /etc/postfix/header_checks and/or in /etc/postfix/body_checks, you could put entries such as but not limited to:
/free mortgage quote/ REJECT /free mortgage quote/ DISCARD
When that's done, be sure to load the changes:
sudo postmap /etc/postfix/header_checks sudo postmap /etc/postfix/body_checks
Before, during, and after the creation of this email server tutorial, I've had a need to use messaging/chat apps. I've used them all, whether Signal, Telegram, Nextcloud Talk, and loads of more boutique and experimental platforms. After years of debate with friends and colleagues, a friend suggested Delta Chat, a chat app that - wait for it - uses email servers for chatting. Given my email server was already set up and purring, I gave it a try and I've used it since for family and business conversations, that is, small and trusted audiences. If you get through this tutorial, it's worth giving it a try! Just edit /etc/dovecot/conf.d/20-imap.conf and ensure the imap_idle_notify_interval = 1min idle notify interval is 1 or 2 mins. For small use cases, increasing this frequency will harm nothing and improve the snappiness of the Delta Chat experience. Everything else is already perfectly compatible with Delta Chat. Just export and save your keys!
If other quirky issues come up, I'll besure to add them right here!
Next Steps
I rewrote the mail server tutorial for the presentation Your Email, Your Rules: Self-Hosting Simplified. The SeaGL presentation can be found on their calendar.
— oemb1905 2025/11/09 05:45