Setup Email Server From Scratch On FreeBSD #2 - 09 Create Virtual Domains
07 RoundCube WebMail <- Intro -> 10 Blocking Spam With Postfix
We believe in data independence, and support others who want data independence.
This tutorial is complete 2025-08-14 except there is no page for setting up postscreen.
This is version 2 and everthing works.
################################# # How to Create Virtual Domains # #################################
This page of the "FreeBSD Email From Scratch" tutorial assumes successful setup of the following ...
1. FreeBSD Server
2. FAMP - FreeBSD Apache MySQL/MariaDB PHP
3. Postfix - smtp server
4. Dovecot - imap server
5. PostfixAdmin - Postfix Web Administration
6. SPF DMARC and DKIM - email authentication and spam filters
7. Roundcube - Webmail
Setup DNS for New Domain(s) in NameCheap, we'll use the name okfig.com in this example. Change your IPv4 IPV4 and domain name as appropriate. Example DNS for coragarden.com.
A @ 147.135.37.135
AAAA @ 2604:2dc0:200:187::1
A imap 147.135.37.135
AAAA imap 2604:2dc0:200:187::1
A mail 147.135.37.135
AAAA mail 2604:2dc0:200:187::1
A smtp 147.135.37.135
AAAA smtp 2604:2dc0:200:187::1
CNAME autoconfig mail.coragarden.com
CNAME autodiscover mail.coragarden.com
CNAME www okfig.com
TXT @ v=spf1 ip4:147.135.37.135 ip6:2604:2dc0:200:187::1/64 mx ~all
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@coragarden.com; ruf=mailto:postmaster@coragarden.com; sp=quarantine
Type Service Protocol Priority Weight Port Target
SRV _autodiscover _tcp 5 0 443 mail.coragarden.com
In Custom MX Records
MX @ smtp.coragarden.com 0
MX @ mail.coragarden.com 10
If one administrator handles all the dmarc reports it is easier to just use one address for all the domains managed. But if you use an email address outside of the virtual domain for sending dmarc reports modify the DNS of the target domain. Example postmaster@okbsd.com recieves dmarc reports from coragarden.com so the TXT _dmarc.coragarden.com uses rua=mailto:postmaster@okbsd.com; ruf=mailto:postmaster@okbsd.com.
Add the following to okbsd.com DNS to allow dmarc reports from coragarden.com.
TXT okfig.com._report._dmarc v=DMARC1;
Add these to the hosts file
nano /etc/hosts
147.135.37.135 coragarden.com www.coragarden.com imap.coragarden.com mail.coragarden.com smtp.coragarden.com
2604:2dc0:200:187::1 coragarden.com www.coragarden.com imap.coragarden.com mail.coragarden.com smtp.coragarden.com
Setup DKIM Records For New Domain
mkdir /usr/local/etc/mail/keys/coragarden.com
opendkim-genkey -b 2048 -d coragarden.com -D /usr/local/etc/mail/keys/coragarden.com -s 20250808 -v
Set permissions, if not set opendkim will report key not secure.
chown -R opendkim:opendkim /usr/local/etc/mail/keys
find /usr/local/etc/mail/keys -type d -exec chmod 500 {} \;
find /usr/local/etc/mail/keys -type f -exec chmod 400 {} \;
Create the TXT record in DNS for coragarden.com.
cat /usr/local/etc/mail/keys/coragarden.com/20250808.txt
20250808._domainkey IN TXT ( "v=DKIM1; k=rsa; "
"p=MIIBIj ... 5V/XUwIDAQAB" ) ; ----- DKIM key 20250808 for coragarden.com
Copy the keys to your name server record and remember to remove the extra quotes and spaces ..." "... in 2 sections of the DKIM line or paste 3 parts between the quotes together.
TXT 20250808._domainkey v=DKIM1; k=rsa; p=MIIBIj ... /XUIDAQAB
Check the record with google name server.
nslookup -type=txt 20250808._domainkey.coragarden.com 8.8.8.8
If you're using bind locally you may need to reload the server.
service named restart
Check if the record is available locally. The output will contain quotes and space but that doesn't mean the entry is wrong, check with mxtoolbox.com.
nslookup -type=txt 20250808._domainkey.coragarden.com
20250808._domainkey.coragarden.com text = "v=DKIM1; k=rsa;p=MIIBIj ... 5V/XUwIDAQAB"
Add the new entries to the opendkim keytable, signingtable, and trustedhosts files.
cat /usr/local/etc/mail/keys/coragarden.com/*.txt >> /usr/local/etc/mail/keytable
nano /usr/local/etc/mail/keytable
20250807._domainkey.okbsd.com okbsd.com:20250807:/usr/local/etc/mail/keys/okbsd.com/20250807.private
20250808._domainkey.coragarden.com coragarden.com:20250808:/usr/local/etc/mail/keys/coragarden.com/20250808.private
nano /usr/local/etc/mail/signingtable
*@okbsd.com 20250807._domainkey.okbsd.com
*@coragarden.com 20250808._domainkey.coragarden.com
nano /usr/local/etc/mail/trustedhosts
127.0.0.1
::1
localhost
147.135.37.135
2604:2dc0:200:187::1
okbsd.com
.okbsd.com
coragarden.com
.coragarden.com
service milter-opendkim restart
By the time this is created the DNS has hopefully propagated and can be tested, test the key. Without -x /usr/local/etc/mail/opendkim.conf opendkim-testkey will report key not secure. If not secure it's not a big issue, but check DNSSEC is enabled with your DNS provider, file permissions in the mail/keys directory, and TrustAnchorFile in mail/opendkim.conf
Update your root.key if needed.
unbound-anchor
Check TrustAnchorFile
nano /usr/local/etc/mail/opendkim.conf
TrustAnchorFile /usr/local/etc/unbound/root.key
Test the new DKIM key.
opendkim-testkey -d coragarden.com -s 20250808 -vvv -x /usr/local/etc/mail/opendkim.conf
opendkim-testkey: checking key '20250808._domainkey.coragarden.com'
opendkim-testkey: key secure
opendkim-testkey: key OK
Setup Domain in PostfixAdmin. Choose if you want to setup the default aliases. It may be easier to choose No, uncheck the box, and create a postmaster account and a catchall alias that points to postmaster. Alternatively, make a forward or alias for postmaster to another account. Adding abuse@, hostmaster@ and webmaster@ adds complexity and will increase time spent on maintenance (with many domains alias lists will be longer).
Domain List -> New Domain
Domain: coragarden.com
Description: Dry Land Corn
Pass expires : 3650
Add Domain
Virtual List - Add Mailbox
postmaster@coragarden.com
jack@coragarden.com
Virtual List - Add Alias (Catchall)
*@coragarden.com postmaster@coragarden.com
Virtual List - A real postmaster@domain.tld is recommended by RFC and real time blacklist ,RBL services, require mail from postmaster@domain.tld to request blacklist removal.
mkdir -p /usr/local/www/coragarden/html
mkdir -p /usr/local/www/coragarden/mail
echo '<?php print "<!DOCTYPE html lang=\"en\"><html><head><title>Title</title></head><body><h1>Hello World!</h1></body></html>"; ?>' > /usr/local/www/coragarden/html/index.php
chown -R root:www /usr/local/www/coragarden
find /usr/local/www/coragarden -type d -exec chmod 750 {} \;
find /usr/local/www/coragarden -type f -exec chmod 640 {} \;
nano /usr/local/etc/apache24/Includes/coragarden.conf
<VirtualHost *:80>
ServerName coragarden.com
ServerAlias www.coragarden.com
ServerAdmin postmaster@coragarden.com
DirectoryIndex index.php index.html
DocumentRoot /usr/local/www/coragarden/html/
<Directory /usr/local/www/coragarden/html/>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
nano /usr/local/etc/apache24/Includes/mail.coragarden.conf
<VirtualHost *:80>
ServerName mail.coragarden.com
ServerAlias smtp.coragarden.com
ServerAlias imap.coragarden.com
ServerAlias autoconfig.coragarden.com
ServerAlias autodiscover.coragarden.com
ServerAdmin postmaster@coragarden.com
DirectoryIndex index.php index.html
DocumentRoot /usr/local/www/coragarden/html/
<Directory /usr/local/www/coragarden/html/>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
Alias /mail "/usr/local/www/coragarden/mail/"
<Directory /usr/local/www/coragarden/mail/>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Certbot needs the virtual host enabled so restart apache. Do you know apache was first created by patching httpd, so was named after 'a-patch-y'.
apachectl configtest
apachectl restart
Test that the non-ssl website works.
http://coragarden.com
http://mail.coragarden.com
Create certificates with certbot.
certbot certonly --apache --agree-tos --redirect --hsts --staple-ocsp --email postmaster@okbsd.com --cert-name coragarden.com -d coragarden.com,www.coragarden.com
Create a certificate for mail services, and mail. web services.
certbot certonly --apache --agree-tos --redirect --hsts --staple-ocsp --email postmaster@okbsd.com --cert-name mail.coragarden.com -d mail.coragarden.com,smtp.coragarden.com,imap.coragarden.com
Secure private key certificate files.
chown -R root:www /usr/local/etc/letsencrypt/live
find /usr/local/etc/letsencrypt/live -type d -exec chmod 755 {} \;
find /usr/local/etc/letsencrypt/live -type f -exec chmod 644 {} \;
chown -R root:www /usr/local/etc/letsencrypt/archive
find /usr/local/etc/letsencrypt/archive -type d -exec chmod 755 {} \;
find /usr/local/etc/letsencrypt/archive -type f -exec chmod 644 {} \;
chmod o-rwx /usr/local/etc/letsencrypt/archive/*/privkey*.pem
Create ssl configurations for new virtual hosts.
nano /usr/local/etc/apache24/Includes/ssl-coragarden.conf
<IfModule mod_ssl.c>
SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000)
<VirtualHost *:443>
ServerName coragarden.com
ServerAlias www.coragarden.com
ServerAdmin postmaster@coragarden.com
TransferLog "/var/log/httpd-access.log"
CustomLog "/var/log/httpd-ssl_request.log" \
"%v:%p %h %l %u %t %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
DirectoryIndex index.php index.html
DocumentRoot /usr/local/www/coragarden/html/
<Directory /usr/local/www/coragarden/html/>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
Include /usr/local/etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /usr/local/etc/letsencrypt/live/coragarden.com/fullchain.pem
SSLCertificateKeyFile /usr/local/etc/letsencrypt/live/coragarden.com/privkey.pem
Header always set Strict-Transport-Security "max-age=31536000"
</VirtualHost>
</IfModule>
nano /usr/local/etc/apache24/Includes/ssl-mail.coragarden.conf
<IfModule mod_ssl.c>
SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000)
<VirtualHost *:443>
ServerName mail.coragarden.com
ServerAdmin postmaster@coragarden.com
# RewriteEngine on
DirectoryIndex index.php index.html
DocumentRoot /usr/local/www/roundcube/
Alias /webmail "/usr/local/www/roundcube"
Alias /roundcube "/usr/local/www/roundcube"
<Directory /usr/local/www/roundcube/>
# Options FollowSymLinks MultiViews
Options FollowSymLinks
AllowOverride All
# Require all granted
Require all denied
Require ip allowed_ip1 allowed_net2/cidr allowed_ipv6 allowed_ipv6/cidr
</Directory>
Alias /admin-login /usr/local/www/postfixadmin/public
<Directory /usr/local/www/postfixadmin/>
Options FollowSymLinks MultiViews
AllowOverride All
Require all denied
Require ip allowed_ip1 allowed_net2/cidr allowed_ipv6 allowed_ipv6/cidr
</Directory>
Alias /mail "/usr/local/www/coragarden/mail/"
Alias "/Autodiscover/Autodiscover.xml" "/usr/local/www/coragarden/mail/Autodiscover.xml/index.php"
<Directory /usr/local/www/coragarden/mail/>
DirectorySlash Off
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
Include /usr/local/etc/letsencrypt/options-ssl-apache.conf
Header always set Strict-Transport-Security "max-age=31536000"
SSLUseStapling on
SSLCertificateFile /usr/local/etc/letsencrypt/live/mail.coragarden.com/fullchain.pem
SSLCertificateKeyFile /usr/local/etc/letsencrypt/live/mail.coragarden.com/privkey.pem
</VirtualHost>
</IfModule>
To allow customer/admin's to add and remove mail accounts, assign them as regular admin's with the domains they are to manage. These regular domain admins can only manage those domains and users they have been assigned. And only the assigned domains show up in postfixadmin.
If not assigning customer/admin's to add and remove mail accounts, the postfixadmin section can be removed from the virtual domain. Use one main host for postfixadmin.
Configure certificates in Postfix and Dovecot
nano /usr/local/etc/postfix/sni_maps
mail.okbsd.com /usr/local/etc/letsencrypt/live/mail.okbsd.com/privkey.pem /usr/local/etc/letsencrypt/live/mail.okbsd.com/fullchain.pem
smtp.okbsd.com /usr/local/etc/letsencrypt/live/mail.okbsd.com/privkey.pem /usr/local/etc/letsencrypt/live/mail.okbsd.com/fullchain.pem
mail.coragarden.com /usr/local/etc/letsencrypt/live/mail.coragarden.com/privkey.pem /usr/local/etc/letsencrypt/live/mail.coragarden.com/fullchain.pem
smtp.coragarden.com /usr/local/etc/letsencrypt/live/mail.coragarden.com/privkey.pem /usr/local/etc/letsencrypt/live/mail.coragarden.com/fullchain.pem
postmap -F /usr/local/etc/postfix/sni_maps
nano /usr/local/etc/dovecot/conf.d/10-ssl.conf
local_name mail.coragarden.com {
ssl_cert = </usr/local/etc/letsencrypt/live/mail.coragarden.com/fullchain.pem
ssl_key = </usr/local/etc/letsencrypt/live/mail.coragarden.com/privkey.pem
}
local_name imap.coragarden.com {
ssl_cert = </usr/local/etc/letsencrypt/live/mail.coragarden.com/fullchain.pem
ssl_key = </usr/local/etc/letsencrypt/live/mail.coragarden.com/privkey.pem
}
service postfix restart
service dovecot restart
Install and edit the Autoconfig and Autodiscover.
cd /tmp
git clone https://github.com/smartlyway/email-autoconfig-php
mkdir -p /usr/local/www/coragarden/mail
cp /tmp/email-autoconfig-php/mail/config-v1.1.xml /usr/local/www/coragarden/mail
cp -r /tmp/email-autoconfig-php/Autodiscover/Autodiscover.xml /usr/local/www/coragarden/mail
Edit these files and change to suit your needs, this is tested and works with Thunderbird. Make sure change the SMTP settings to 587 with STARTTLS in config-v1.1.xml.
nano /usr/local/www/coragarden/mail/config-v1.1.xml
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="coragarden.com">
<domain>coragaren.com</domain>
<displayName>coragarden.com</displayName>
<displayShortName>coragarden.com</displayShortName>
<incomingServer type="imap">
<hostname>imap.coragarden.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>smtp.coragarden.com</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
</outgoingServer>
</emailProvider>
</clientConfig>
Do the same for Outlook clients.
nano /usr/local/www/coragarden/mail/Autodiscover.xml/index.php
<?php
$raw = file_get_contents('php://input');
$matches = array();
preg_match('/<EMailAddress>(.*)<\/EMailAddress>/', $raw, $matches);
header('Content-Type: application/xml');
?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<User>
<DisplayName>coragarden.com</DisplayName>
</User>
<Account>
<AccountType>email</AccountType>
<Action>settings</Action>
<Protocol>
<Type>IMAP</Type>
<Server>imap.coragarden.com</Server>
<Port>993</Port>
<DomainRequired>off</DomainRequired>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
<LoginName><?php echo $matches[1]; ?></LoginName>
</Protocol>
<Protocol>
<Type>SMTP</Type>
<Server>smtp.coragarden.com</Server>
<Port>587</Port>
<DomainRequired>off</DomainRequired>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
<LoginName><?php echo $matches[1]; ?></LoginName>
</Protocol>
</Account>
</Response>
</Autodiscover>
You will need to add a SV records for Autodiscover
Type Service Protocol Priority Weight Port Target
SRV Record _autodiscover _tcp 5 0 443 mail.coragarden.com
Autoconfig needs the path http://autoconfig.okdeb.com/mail/config-v1.1.xml
Restart apache
apachectl configtest
apachectl restart
Test
https://mail.coragarden.com
Test valid xml for autoconfig.
https://mail.coragarden.com/mail/config-v1.1.xml
Test valid xml for autodiscover.
https://mail.coragarden.com/Autodiscover/Autodiscover.xml
Go to the main domain PostfixAdmin login page or the customer admin login page.
https://mail.okbsd.com/admin-login/
https://mail.coragarden.com/admin-login/
Domain List -> Add Domain
Domain: coragarden.com
Description: Cora's Fantastic Garden
Aliases: 0
Mailboxes: 0
Active: x
Add default mail aliaes: UNCHECK THE BOX
Pass expires: 3650
RFC requires that the domain has a postmaster account, and it's a good idea to set a catch all to alias unknown accounts and send to postmaster.
Virtual List -> Add Mailbox
Add Mail Account postmaster@coragarden.com
Username: postmaster
Domain: coragarden.com
Password: ****
Password: ****
Name: Cora Garden Postmaster
Quota: 0
Active: x
Pass expires : 3650
Send Welcome email: no
Add Mailbox
Virtual List -> Add Alias
Alias: *
Domain: coragarden.com
To: postmaster@coragarden.com
Virtual List -> Add Mailbox
Add Mail Account jack@coragarden.com
Username: jack
Domain: coragarden.com
Password: ****
Password: ****
Name: Jack Pumpkin
Quota: 0
Active: x
Send Welcome email: no
Add Mailbox
Add jack@coragarden.com to Thunderbird and test send and recieve to the account.
Login to roundcube to test the new virtual domain, send and recieve to make sure DKIM, DMARC, and SPF are working.
https://mail.coragarden.com
Login: jack@coragarden.com
Password: ***********
If the email was sent and recieved but DKIM signature is missing, check that the new domain was added to /usr/local/etc/mail/signingtable and restart milter-opendkim.
nano /usr/local/etc/mail/signingtable
*@okbsd.com 20250807._domainkey.okbsd.com
*@coragarden.com 202508078._domainkey.coragarden.com
service milter-opendkim restart
Check the source and the headers now have DKIM signature something like ...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=coragarden.com;
s=Cy9jmkzC2USPfcJ4JIuzA07rpB0gS; t=1747783045;
bh=wiMQ3UTGd/zMN6+PeBSiAAUHXfKRtjj7J2UI7ZvJA+A=;
h=Date:From:To:Subject:From;
b=bkk87+VHY7HvSb1b9mq0b9bLc0XBMOzf6CnBMLcoLMAZ0yN1nDyrILNj9RiYUCJsq
hC6QsHu9S4t9Y8q85AoszhY78ddzfU8SLcg/IlTmWuiNWisrkKjAZ9ftPEtxVxkYKZ
VnpSveCK4O5Gw==
Change password worked, but setting identity had an error in roundcube. Fix as follows.
pkg install gnupg
pear install Crypt_GPG
cd /var/www/roundcube/plugins/enigma
cp config.inc.php.dist config.inc.php
$config['enigma_pgp_homedir'] = "/var/www/roundcube/plugins/enigma/home";
mkdir /var/www/roundcube/plugins/enigma/home
chown www:www /var/www/roundcube/plugins/enigma/home
chmod 750 /var/www/roundcube/plugins/enigma/home
Test that everything works with Thunderbird
Test Roundcube, identities, sending mail, recieving mail, filters, changing password, and calendar.
Login with the new account jack@coragarden.com
https://mail.coragarden.com
###################### # Next Blocking Spam # ######################