Setup Email Server From Scratch On FreeBSD #2 - 09 Create Virtual Domains
07 RoundCube WebMail <- Intro -> 10 Blocking Spam
We believe in data independence, and support others who want data independence.
This tutorial is partially complete 2025-08-07
This is version 2 and everthing works up to and including Roundcube.
#################################
# 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: ***********
# Ig 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