2026-08-19
I used to run luke smiths’ script to set up my mail server on my vps.
But since it’s fully automated and I don’t understand everything and got scared, I don’t dare to touch the configuration ever after.
This time, I am using FreeBSD and I will try to figure everything out and do it the manual way, so that hopefully I can better debug / upgrade / customize this later.
The structure and most of the content is based on luke smiths’ landchad guide1. I try to use FreeBSD’s way of setting things up as much as possible.
Some parts of this also referred to Gentoo wiki2
The end result should be:
The stability of services
When restarting the servers and the services, the Email server may malfunction at the start. Don’t worry about this, and wait for a few minutes, then it should be good to go.
Use postfix to send emails.
pkg install postfixFollow the post install message and copy the default config:
Postfix was *not* activated in /usr/local/etc/mail/mailer.conf!
To finish installation run the following commands:
mkdir -p /usr/local/etc/mail
install -m 0644 /usr/local/share/postfix/mailer.conf.postfix //usr/local/etc/mail/mailer.conf
And enable it
sysrc postfix_enable="YES"
sysrc sendmail_enable="NONE" # Already done by bsdThen, follow the bundled documentation from /usr/local/share/doc/postfix/BASIC_CONFIGURATION_README.
At this stage, you can already edit aliases for who receive the mail, see postfix/aliases.
Add a alias from
DMARCaccording yourDMARCconfiguration.
After editing, run newaliases.
REMEMBER TO RUN that, otherwise a error will appear:
error: open database /etc/aliases.db: No such file or directory
In the landchad website it wants you to install mailutils, it’s already present on FreeBSD. 1
Start the daemon and test it with mail(1):
echo "test message" | mail -s "test email sending" <recipient_address_here>Check the log:
tail /var/log/maillogYou’ll likely find out that the mail has been rejected:
Aug 19 14:56:38 vultr postfix/qmgr[8860]: A83699CA20: from=<root@vps>, size=333, nrcpt=1 (queue active)
Aug 19 14:56:39 vultr postfix/smtp[8863]: A83699CA20: host <dest_mail_provider>[1.1.1.1] said: 450 4.1.8 <root@vps>: Sender address rejected: Domain not found (in reply to RCPT TO command)The log indicates that it doesn’t know the domain from the IP.
We need to set up the reverse DNS on the VPS side that points the outbound IP to the DNS address.
See the landchad link for pictures:
https://landchad.net/mail/rdns/
rDNS itself is not enough, we need DKIM to sign our messages to associate the message with the domain.
This basically proves that you are you cryptographically, to prevent people from impersonating as someone else.
See https://landchad.net/mail/validate/ or https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail for an introduction to DKIM.
pkg install opendkimFollow the post install message:
=====
Message from opendkim-2.10.3_23:
--
In order to run this port, write your opendkim.conf and:
if you use sendmail, add the milter socket `socketspec' in
/etc/mail/<your_configuration>.mc:
INPUT_MAIL_FILTER(`dkim-filter', `S=_YOUR_SOCKET_SPEC_, F=T, T=R:2m')
or if you use postfix write your milter socket `socketspec' in
/usr/local/etc/postfix/main.cf:
smtpd_milters = _YOUR_SOCKET_SPEC_
And to run the milter from startup, add milteropendkim_enable="YES" in
your /etc/rc.conf.
Extra options can be found in startup script.
Note: milter sockets must be accessible from postfix/smtpd;
using inet sockets might be preferred.opendkim is installed at at /usr/local/etc/mail/opendkim.conf
There’s an option to use a single Keyfile or a KeyTable. I use KeyTable, so that I can potentially sign my other domains. This is the same choice as the landchad tutorial.
## KeyFile filename
## default (none)
##
## Specifies the path to the private key to use when signing. Ignored if
## SigningTable and KeyTable are used. No default; must be specified for
## signing if SigningTable/KeyTable are not in use.
# KeyFile /var/db/dkim/example.private
## KeyTable dataset
## default (none)
##
## Defines a table that will be queried to convert key names to
## sets of data of the form (signing domain, signing selector, private key).
## The private key can either contain a PEM-formatted private key,https://wiki.gentoo.org/wiki/Special:MyLanguage/Complete_Virtual_Mail_Server
## a base64-encoded DER format private key, or a path to a file containing
## one of those.
KeyTable datasetTo generate them, run:
cd /usr/local/etc/mail/
mkdir dkim && cd dkim
opendkim-genkey --domain=<your domain>Since BSD doesn’t bundle a user for this package, add a user for this and set the permission for the folder.
Configure the daemon to use the user and group for it.
There is a selector option that specifies sub-domains(?). It’s default value is default.
More information on https://datatracker.ietf.org/doc/html/rfc6376#section-3.1
Now, link the keys to a KeyTable and SigningTable:
Documentation of their structure is in opendkim.conf(5)
echo "<name_of_your_choice> <domain>:<sector>:/usr/local/etc/mail/dkim/<sector>.private" > key_table
echo "*@<domain> <name_from_key_table>" > signing_tableNow, add the tables to the config.
SigningTable refile:/usr/local/etc/mail/dkim/signing_table
KeyTable file:/usr/local/etc/mail/dkim/key_table
refile:means the file is a pattern. see opendkim(8) “DATA SETS”
Finish setting up by modifying configurations that’s not commented.
Take note of the Socket setting, set it to a local inet port.
Socket inet:12301@localhostAlso, set up a user for its operation.
adduser
# Enter information and add it to mail groupAdd the following to rc.conf
milteropendkim_enable="YES"
milteropendkim_uid="<uid>"
milteropendkim_gid="<gid>"Now, we need to make these software play together.
DKIM is going to expose a listening socket at localhost port 12301, connect postfix to it.
# For external mail that arrive from smtp
smtpd_milters = inet:localhost:12301
# For internal unix mails
non_smtpd_milters = inet:localhost:12301For receivers, they need to verify the sender (us) ’s identity, which is by looking up a TXT record from our server’s domain name.
Find the TXT record from /usr/local/etc/mail/dkim/default.txt
The content inside of the quotes will be the text of TXT record. Copy paste them into the TXT record of
<selector>._domainkey.<your_domain>
The specs is here: https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail#Verification
Use some online tool to verify that the TXT record is valid according to the spec in Wikipedia.
DMARC is basically telling others what authentication you’re using, and what to do when authentication failed.3
We add a mail user to receive violation reports, and specify the actions according to the specification:
v=DMARC1; p=reject; adkim=s; aspf=s; rua=mailto:dmarc@<your_mail_domain>; fo=1The (simplified) specs is here: https://en.wikipedia.org/wiki/DMARC#DNS_record
The original RFC standard for the parameters: https://www.rfc-editor.org/info/rfc9989/#name-dmarc-policy-record-format
Add it to a TXT record for sub-domain _dmarc.
And remember to add a dmarc alias to some users (prolly root)
This makes your email domain less likely to be used as spamming.
See more here: https://en.wikipedia.org/wiki/Sender_Policy_Framework#Reasons_to_implement
Add the following to TXT record for the domain of mail server:
v=spf1 mx a:boxstr.nl ip4:<ip_4_addr> ip6:<ip_6_addr> -allThe IP addresses should match the one in reverse DNS.
Refer to the previous chaptor to try it out.
Also, the following site can be used and check if your mail is in a spam list:
We can update postfix to use shared domains.
Documentation for postfix can be found at
/usr/local/share/doc/postfix/VIRTUAL_READMETODO: figure this out one day
Now, SMTP has been finished. This server can send mails just fine. We need to access incoming mail with a email client using the IMAP protocol.
There are other protocols, but IMAP is the most modern, has the most features and is the most widely used one. 4
pkg install dovecotThe postinstall messages has said dovecot is installed with UNIX authentication, and mails will be put into /var/mail/$USER directory. I’m fine with it, and I’m not gonna do anything about it:
=====
Message from cyrus-sasl-2.1.28_6:
--
You can use sasldb2 for authentication, to add users use:
saslpasswd2 -c username
If you want to enable SMTP AUTH with the system Sendmail, read
Sendmail.README
NOTE: This port has been compiled with a default pwcheck_method of
auxprop. If you want to authenticate your user by /etc/passwd,
PAM or LDAP, install ports/security/cyrus-sasl2-saslauthd and
set sasl_pwcheck_method to saslauthd after installing the
Cyrus-IMAPd 2.X port. You should also check the
/usr/local/lib/sasl2/*.conf files for the correct
pwcheck_method.
If you want to use GSSAPI mechanism, install
ports/security/cyrus-sasl2-gssapi.
If you want to use SRP mechanism, install
ports/security/cyrus-sasl2-srp.
If you want to use LDAP auxprop plugin, install
ports/security/cyrus-sasl2-ldapdb.
=====
Message from openldap26-client-2.6.13:
--
The OpenLDAP client package has been successfully installed.
Edit
/usr/local/etc/openldap/ldap.conf
to change the system-wide client defaults.
Try `man ldap.conf' and visit the OpenLDAP FAQ-O-Matic at
http://www.OpenLDAP.org/faq/index.cgi?file=3
for more information.
=====
Message from dovecot-2.3.21.1_3:
--
You must create the configuration files yourself. Copy them over
to /usr/local/etc/dovecot and edit them as desired:
cp -R /usr/local/etc/dovecot/example-config/* \
/usr/local/etc/dovecot
The default configuration includes IMAP and POP3 services, will
authenticate users agains the system's passwd file, and will use
the default /var/mail/$USER mbox files.
Next, enable dovecot in /etc/rc.conf:
dovecot_enable="YES"
To avoid a risk of mailbox corruption, do not set the
security.bsd.see_other_uids or .see_other_gids sysctls to 0
if Dovecot is storing mail for multiple concurrent users (PR 218392).
Similarly, setting sysctls security.bsd.hardlink_check_uid or
security.bsd.hardlink_check_gid to 1 might result in non-working
mailboxes, depending on what mailbox locking mechanism is used
(PR 242223).
If you want to be able to search within attachments using the
decode2text plugin, you'll need to install textproc/catdoc, and
one of graphics/xpdf or graphics/poppler-utils.
There are some potentially breaking changes in Dovecot 2.3. If you
are upgrading from Dovecot 2.2:
* https://doc.dovecot.org/2.3/installation_guide/upgrading/from-2.2-to-2.3/
* Merge the configuration file changes from
/usr/local/etc/dovecot/examples-config/
Ignore the ones from cyrus-sasl and openldap for now.
Follow the message and copy the configuration files:
cp -R /usr/local/etc/dovecot/example-config/* /usr/local/etc/dovecotMake sure uid / gid hiding is not set:
sysctl security.bsd.see_other_uids
sysctl security.bsd.see_other_gidsNone of the above should return 1.
You can edit the default mail-boxes now, by editing /usr/local/etc/dovecot/conf.d/15-mailboxes.conf.
Usually the
autooption should be set there!
Luke’s guide5 has some more settings, but they are redundant to me, I’ve omitted them.
Also set up the mail folder from 10-mail.conf, in mail_location:
mail_location = maildir:~/MaildirThis is the standard configuration. 6
Now, we need to connect this with postfix. 7
Edit 10-master.conf:
service auth {
# Uncomment the postfix part
# And allow postfix to have full access to all lookups (might not be required?)
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
}From postfix’s configuration:
# Interop with dovecot
# smtpd_sasl_auth_enable = yes
# smtpd_sasl_type = dovecot
# smtpd_sasl_path = private/auth
mailbox_command = /usr/local/libexec/dovecot/deliverThe sasl can be skipped, since we will user Dovecot’s submission proxy to handle uploading and authentication. And we have set it up so that it doesn’t need authentication when sending from localhost. (verify the exact setting for this TODO)
We need to allow dovecot to use TLS to encrypt our IMAP connection.
I use caddy for other web-hosting, it has a feature named automatic HTTPS, which will automatically request and renew your certificates, and by default it puts the new certificate in the file system.
Since web and IMAP use different ports, we can use the same certificate made by caddy for IMAP server.
Here, We will use a sub-domain for IMAP connections, so that the root domain can use some other certs.
Add two sub-domain DNS record just for the IMAP server:
imap.example.com) to the root domain for mail (example.com)MX means send the SMTP traffic for the host to the look up result. In this case, we send mail to our domain with our mail server on the same domain.
pkg install caddyFollow the postinstall message to set up:
To enable caddy:
- Edit /usr/local/etc/caddy/Caddyfile
See https://caddyserver.com/docs/
- Run 'service caddy enable'
Note while Caddy currently defaults to running as root:wheel, it is strongly
recommended to run the server as an unprivileged user, such as www:www --
- Use security/portacl-rc to enable privileged port binding:
# pkg install security/portacl-rc
# sysrc portacl_users+=www
# sysrc portacl_user_www_tcp="http https"
# sysrc portacl_user_www_udp="https"
# service portacl enable
# service portacl start
- Configure caddy to run as www:www
# sysrc caddy_user=www caddy_group=www
- Note if Caddy has been started as root previously, files in
/var/log/caddy, /var/db/caddy, and /var/run/caddy may require their ownership
changing manually.
/usr/local/etc/rc.d/caddy has the following defaults:
- Server log: /var/log/caddy/caddy.log
(runtime messages, NOT an access.log)
- Automatic SSL certificate storage: /var/db/caddy/data/caddy/
- Administration endpoint: //unix/var/run/caddy/caddy.sock
- Runs as root:wheel (this will change to www:www in the future)Edit the Caddyfile to just send a response:
<your_mail_domain> {
respond "Use an IMAP client to access the emails"
}
After starting it, caddy will automatically acquire a TLS certificate in /var/db/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<your_imap_domain>
Dovecot will read the files when it’s still running as root, so no permission change is needed. 8
Also, a DH parameter is needed.
Generate it with:
openssl dhparam -out /usr/local/etc/dovecot/dh.pem 4096Edit /usr/local/etc/dovecot/conf.d/10-ssl.conf:
SSL = required
ssl_cert = </etc/SSL/certs/dovecot.pem
ssl_key = </etc/SSL/private/dovecot.pem
ssh_dh = </usr/local/etc/dovecot/dh.pemOn vim, use
to complete files and paths See the official documentation for more SSL options.9 The default ones are pretty sane to me, so I didn’t change anything else except for trying to use the latest TLS version possible.
TLS-enabled submission proxy
Postfix doesn’t have good support for TLS, but dovecot do. Dovecot also has a submission server that forwards your submission mail to the local agent.10.
The mail sending works now using un-encrypted port 25 SMTP messages, but that’s obviously unsafe and will leak your passwords. So, we use the dovecot’s submission proxy for this.
- Enable the submission protocol on
dovecot.conf- Head over to
conf.d/20-submission.conf, and edit the required parameter, that is the smtp server host, in this case127.0.0.1- Enable the
submissionslisten port, and addssl = yesto the configuration.Separate certificates for separate domains
Currently, if the imap subdomain is named
imap, we can have another subdomain namedsmtpto avoid confusion.It’s possible to separate your certs by protocols, ips, and SNI.
The default
ssl_certandssl_keyare still needed, so that the config check passes.Get a address and certs just like earlier, and set up like following:
Example (
20-submission.conf):local_name <smtp.your.domain> { protocol submission { # Space-separated list of plugins to load (default is global mail_plugins). #mail_plugins = $mail_plugins # Maximum number of SMTP submission connections allowed for a user from # each IP address. # NOTE: The username is compared case-sensitively. #mail_max_userip_connections = 10 ssl_cert = </var/db/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<path_to_cert> ssl_key = </var/db/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<path_to_key> } }Other goodies for IMAP
At this stage, you can already login to the email using a email client like thunderbird.
Add a user with
However, many features are still missing.
Searching (FTS)
To support server side fast mail body searching, a
ftsplugin can be used.I use
dovecot-fts-flatcurvefor local based indexing. 11In
imap.conf:protocol imap { # Space separated list of plugins to load (default is global mail_plugins). mail_plugins = $mail_plugins fts fts_flatcurve plugin { # Define "flatcurve" as the FTS driver. fts = flatcurve # These are not flatcurve settings, but required for Dovecot FTS. See # Dovecot FTS Configuration link above for further information. fts_languages = en nl fr de fts_tokenizers = generic email-address fts_filters = lowercase stopwords # Force more consistent result fts_enforced = yes } # Maximum number of IMAP connections allowed for a user from each IP address. # NOTE: The username is compared case-sensitively. #mail_max_userip_connections = 10 }Automatically sort and deliver emails to a folder (Pigeonhole)
An overview of this can be found at Dovecot’s official documentation
pkg install dovecot-pigeonholeThe package will install more example configs, copy them to
conf.d:/usr/local/share/doc/dovecot/example-config/conf.d/20-managesieve.conf /usr/local/share/doc/dovecot/example-config/conf.d/90-sieve-extprograms.conf /usr/local/share/doc/dovecot/example-config/conf.d/90-sieve.confAs instructed by
90-sieve.conf, add it toLMTPandLDAprotocols’ plugins.As far as I know, LMTP is used to transfer mail received from SMTP to a user’s local folder
In
90-sieve.confSetup default sieve locations, I’ve changed them for FreeBSD style hierarchy.plugin { # The location of the user's main Sieve script or script storage. The LDA # Sieve plugin uses this to find the active script for Sieve filtering at # delivery. The "include" extension uses this location for retrieving # :personal" scripts. This is also where the ManageSieve service will store # the user's scripts, if supported. # # Currently only the 'file:' location type supports ManageSieve operation. # Other location types like 'dict:' and 'ldap:' can currently only # be used as a read-only script source (). # # For the 'file:' type: use the ';active=' parameter to specify where the # active script symlink is located. # For other types: use the ';name=' parameter to specify the name of the # default/active script. sieve = file:~/sieve;active=~/.dovecot.sieve # The default Sieve script when the user has none. This is the location of a # global sieve script file, which gets executed ONLY if user's personal Sieve # script doesn't exist. Be sure to pre-compile this script manually using the # sievec command line tool if the binary is not stored in a global location. # --> See sieve_before for executing scripts before the user's personal # script. sieve_default = /usr/local/lib/dovecot/sieve/default.sieve # The name by which the default Sieve script (as configured by the # sieve_default setting) is visible to the user through ManageSieve. #sieve_default_name = # Location for ":global" include scripts as used by the "include" extension. sieve_global = /usr/local/lib/dovecot/sieve/ }Spam and Virus protection (Amavis with spamassassin and clamav)
Postfix can be configured to stop some spam:12
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.netGentoo uses a intermediate smtp server to route incoming messages and check for virus.
Luke’s landchad uses content filter on smtp server.
I perfer Luke’s approach here, because it’s basically one less server to set up.
First, the email is going to be received by postfix, which is then added a header for spam status. Lastly, it’s sent to dovecot and the sieves are going to take care of the spams by putting them into the spam folder.
Postfix spam marking part
TODO: figure this out oneday.
I’ve skipped this one right now because it has too much dependencies and is hard to set up.
Dovecot put into spam folder part
There is spamtest and virustest. They don’t require installing new applications.
It’s relatively easy to configure, just like this:
plugin { sieve_extensions = +spamtest +spamtestplus +virustest # CUSTOM: spamtest and virustest configuration sieve_spamtest_status_type = text sieve_spamtest_status_header = X-Spam-Status sieve_spamtest_text_value1 = No sieve_spamtest_text_value10 = Yes sieve_virustest_status_type = text sieve_virustest_status_header = X-Virus-Scan: Found to be (.+)\. sieve_virustest_text_value1 = clean sieve_virustest_text_value5 = infected }Security and Privacy
Auth limit the authentication:
smtpd_client_auth_rate_limit = 4Reject dubious incoming mail:
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unknown_recipient_domainRemove some headers like IP:
/^Received:.*/ IGNORE /^X-Originating-IP:/ IGNORE /^User-Agent:/ IGNORE /^X-Mailer:/ IGNOREFirewall with PF
Reference the FreeBSD Wiki and the online tutorial, set up the following:
- Block everything
- Set up services
- ssh
- smtp (postfix mail transport)
- smtps (dovecot submission)
- domain (dns)
- https
- imaps
- ntp
- Allow the services
- Allow from localhost to localhost
https://wiki.gentoo.org/wiki/Special:MyLanguage/Complete_Virtual_Mail_Server ↩
https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail#Relationship_to_SPF_and_DMARC ↩
https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol#Advantages_over_POP ↩
https://en.wikipedia.org/wiki/Maildir also see the dovecot documentation:
/usr/local/share/doc/dovecot/wiki/MailLocation.txt↩https://doc.dovecot.org/2.3/configuration_manual/howto/postfix_and_dovecot_sasl/ ↩
https://doc.dovecot.org/2.3/configuration_manual/dovecot_ssl_configuration/ ↩
https://doc.dovecot.org/2.3/configuration_manual/dovecot_ssl_configuration/#ssl-security-settings ↩
https://doc.dovecot.org/2.3/admin_manual/submission_server/ ↩
https://doc.dovecot.org/2.3/settings/plugin/fts-plugin/#plugin_setting-fts-fts_tokenizers ↩
https://wiki.gentoo.org/wiki/Complete_Virtual_Mail_Server/amavisd_spamassassin_clamav#Postfix ↩