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 #
######################

07 RoundCube WebMail <- Intro -> 10 Blocking Spam With Postfix