Setup Email Server From Scratch On FreeBSD #2 - 05 PostfixAdmin

04 Dovecot IMAP <- Intro -> 06 SPF DMARC And DKIM

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.

#################
# Postfix Admin #
#################

cd /usr/local/www
git clone https://github.com/postfixadmin/postfixadmin.git
cd /usr/local/www/postfixadmin
git checkout postfixadmin-3.3.10

mysql
> create database postfixadmin;
> CREATE USER 'postfixadmin'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'secretpasswd';
> GRANT ALL PRIVILEGES ON `postfixadmin` . * TO 'postfixadmin'@'localhost';
> FLUSH PRIVILEGES;
exit;

# Install php and php modules dependencies - some of these won't actually be used but the postfixadmin
# setup page will warn you if they're missing.

pkg install php83 mod_php83 php83-bcmath php83-bz2 php83-curl php83-gd php83-gmp php83-imap php83-intl 
pkg install php83-ldap php83-mbstring php83-mysqli php83-pdo php83-pdo_pgsql php83-pdo_sqlite php83-pecl-imagick 
pkg install php83-pecl-redis php83-pgsql php83-session php83-tokenizer php83-xml php83-zip php83-zlib php83-pdo_mysql

# Once fixing dependencies restart php_fpm and apache or postfixadmin may throw errors.

service php_fpm restart
apachectl restart

# Configure Postfixadmin

nano /usr/local/www/postfixadmin/config.local.php
# ---
$CONF['configured'] = true;
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_socket'] = '/tmp/mysql.sock';
$CONF['database_port'] = '3306';
$CONF['database_user'] = 'postfixadmin';
$CONF['database_password'] = 'secretpasswd';
$CONF['database_name'] = 'postfixadmin';
$CONF['encrypt'] = 'dovecot:ARGON2I';
$CONF['dovecotpw'] = "/usr/local/bin/doveadm pw -r 5";
$CONF['base_url'] = '/admin-login/';
// $CONF['setup_password'] = '';
# ---

mkdir /usr/local/www/postfixadmin/templates_c
chown -R www:www /usr/local/www/postfixadmin/templates_c

# Configure Postfixadmin in Apache

# Add the directory to the virtual host and secure it so only allowed ip's have
# access. Check apache docs on how to setup user authentication if you wish. 
# This doesn't provide total security but reduces the attack surface. Login is 
# still protected by https encrypted username and password. It is also a good 
# idea to configure this with a different alias or as a different virtual host 
# separate from the website and roundcube login path.

nano /usr/local/etc/apache24/Includes/ssl-mail.okbsd.conf
# ---
        Alias /admin-login /usr/share/postfixadmin/public
        <Directory /usr/share/postfixadmin/>
                Options FollowSymLinks MultiViews
                AllowOverride All
                Require all denied
                Require ip allowed_ip1 allowed_net2/cidr allowed_ipv6 allowed_ipv6/cidr
        </Directory>
# ---

# Go to the setup page and create a setup password.

https://mail.okbsd.com/admin-login/setup.php

# Copy the password to conf.local.php

nano nano /usr/local/www/postfixadmin/config.local.php
# ---
$CONF['setup_password'] = '$2y$10$fds_SAMPLE_HASH_ONLY_ddlfdfudedEFDFGdsnjalksd';
# ---

# Reload the setup page and wait a few seconds.

# Navigate to the setup.php page again and enter the setup password you created.

https://mail.okbsd.com/admin-login/setup.php

# Setup Administrator password

Admin: jack@okbsd.com
Password: mysecret
Password (again) : mysecret

The following 'super-admin' accounts have already been added to the database.
    jack@okbsd.com

# Click the link at the bottom 'login to PostfixAdmin'.

https://mail.okbsd.com/admin-login

Login: jack@okbsd.com
Password: mysecret

# Setup statistics in Dovecot

nano /usr/local/etc/dovecot/conf.d/10-master.conf
# --- add to end of file
service stats {
    unix_listener stats-reader {
    user = www     
    group = www     
    mode = 0660
}   
   
unix_listener stats-writer {
    user = www                                                   
    group = www     
    mode = 0660
  }
}
# ---

# Add www to group dovecot and set permissions

pw groupmod dovecot -m www
service dovecot restart
chown www:www /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer
chmod 660 /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer

# Configure Postfix to use MySQL

nano /usr/local/etc/postfix/main.cf
# --- add to end of file

mailbox_transport = lmtp:unix:private/dovecot-lmtp
smtputf8_enable = no

virtual_mailbox_domains = proxy:mysql:/usr/local/etc/postfix/sql/mysql_virtual_domains_maps.cf
virtual_mailbox_maps =
   proxy:mysql:/usr/local/etc/postfix/sql/mysql_virtual_mailbox_maps.cf,
   proxy:mysql:/usr/local/etc/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf
virtual_alias_maps =
   proxy:mysql:/usr/local/etc/postfix/sql/mysql_virtual_alias_maps.cf,
   proxy:mysql:/usr/local/etc/postfix/sql/mysql_virtual_alias_domain_maps.cf,
   proxy:mysql:/usr/local/etc/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf

virtual_transport = lmtp:unix:private/dovecot-lmtp
# ---

mkdir -p /usr/local/etc/postfix/sql
nano /usr/local/etc/postfix/sql/mysql_virtual_domains_maps.cf
# ---
user = postfixadmin
password = secretpasswd
hosts = localhost
dbname = postfixadmin
query = SELECT domain FROM domain WHERE domain='%s' AND active = '1'
# ---

nano /usr/local/etc/postfix/sql/mysql_virtual_mailbox_maps.cf
# ---
user = postfixadmin
password = secretpasswd
hosts = localhost
dbname = postfixadmin
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1'
# ---

nano /etc/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf
# ---
user = postfixadmin
password = secertpasswd
hosts = localhost
dbname = postfixadmin
query = SELECT maildir FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' and mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) AND mailbox.active = 1 AND alias_domain.active='1'
# ---

nano /etc/postfix/sql/mysql_virtual_alias_maps.cf
# ---
user = postfixadmin
password = secretpasswd
hosts = localhost
dbname = postfixadmin
query = SELECT goto FROM alias WHERE address='%s' AND active = '1'
# ---

nano /etc/postfix/sql/mysql_virtual_alias_domain_maps.cf
# ---
user = postfixadmin
password = secretpasswd
hosts = localhost
dbname = postfixadmin
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('%u', '@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'
# ---

nano /etc/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf
# ---
user = postfixadmin
password = secretpasswd
hosts = localhost
dbname = postfixadmin
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'

# Set restrictive file permissions since the secretpasswd is in the files.

chown -R root:postfix /usr/local/etc/postfix/sql
chmod 0640 /usr/local/etc/postfix/sql/*
chmod 0750 /usr/local/etc/postfix/sql

# Change postfix destination
postconf mydestination
mydestination = $mydomain, $myhostname, localhost.$mydomain, localhost

postconf -e "mydestination = \$myhostname, localhost.\$mydomain, localhost"

# Add to end of file - use <control> - v several times to go to end of file.

nano /usr/local/etc/postfix/main.cf
# ---
virtual_mailbox_base = /var/vmail
virtual_minimum_uid = 2000
virtual_uid_maps = static:2000
virtual_gid_maps = static:2000
# ---

service postfix restart

# check if vmail user and group have already been added

grep vmail /etc/group /etc/passwd
/etc/group:vmail:*:2000:
/etc/passwd:vmail:*:2000:2000::/nonexistent:/usr/sbin/nologin

# If not add them.

pw useradd -c "" -n vmail -s /usr/bin/nologin -d /nonexistent -u 2000

# Create virtual mailbox directories.

mkdir /var/vmail/
chown -R vmail:vmail /var/vmail

# Configure Dovecot

# Add mail_home to 10-mail.conf

nano /usr/local/etc/dovecot/conf.d/10-mail.conf
# ---
mail_location = maildir:~/Maildir
mail_home = /var/vmail/%d/%n
# ---

# Change username_format to %u and enable auth-sql.conf.ext and disable auth-system.conf.ext

nano /usr/local/etc/dovecot/conf.d/10-auth.conf
# ---
auth_username_format = %u
auth_default_realm = okbsd.com
#!include auth-system.conf.ext
!include auth-sql.conf.ext
auth_debug = yes
auth_debug_passwords = yes
# ---

# Change the following

nano /usr/local/etc/dovecot/dovecot-sql.conf.ext
# ---
driver = mysql
connect = host=localhost dbname=postfixadmin user=postfixadmin password=secretpasswd
default_pass_scheme = ARGON2I
password_query = SELECT username AS user,password FROM mailbox WHERE username = '%u' AND active='1'
user_query = SELECT CONCAT('/var/vmail/', maildir) AS home, 2000 AS uid, 2000 AS gid, CONCAT('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active='1'
iterate_query = SELECT username AS user FROM mailbox
# ---

service dovecot restart

# At this point Thunderbird user@okbsd.com may get disconnected because Dovecot
# will be using the new virtual mailbox database and jack@okbsd.com doesn't exist
# there.

# Add domain and mailboxes
Add Domain
Domain: okbsd.com
Description: my first mail domain
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.

Add Mail Account postmaster@okbsd.com
Username: postmaster
Domain: okbsd.com
Password: ****
Password: ****
Name: OkBSD Postmaster
Quota: 0
Active: x
Send Welcome email: no
Add Mailbox

# Virtual List -> Add Alias
Alias: *
Domain: okbsd.com
To: postmaster@okbsd.com

# Virtual List -> Add Mailbox

Add Mail Account jack@okbsd.com
Username: jack
Domain: okbsd.com
Password: ****
Password: ****
Name: Jack Pumpkin
Quota: 0
Active: x
Send Welcome email: no
Add Mailbox

# Add jack@okbsd.com to Thunderbird and test send and recieve to the account
# created entirely by PostfixAdmin

# Forgot PostfixAdmin Password!

# If you ever lost access to PostfixAdmin - I did because when I went through setup
# I hadn't enabled ARGON2I, when I enabled later my password was wrong. I checked
# the database and the password field had something like {1} which isn't ARGON2I.

mysql
use postfixadmin;
select * from admin;

# Disabled the  setup_password, enabled ARGGON2I and went to setup.php again generated
# a new setup password, added a temporary superadmin, logged in, updated the old admin
# password, logged out and back in, deleted the temporary superadmin. Upon checking the
# database the password field now starts with {ARGON2I}$argon2i$v=19.

##############
# SUGGESTION #
##############

If everything works as expected it is a good idea to backup the configurations 
now. We've done a lot of changes and made a lot of progress so let's make sure 
if we have problems later we can roll back to a working configuration.

cd /usr/local/etc
tar cfzv dovecot_backup.tgz dovecot
tar cfzv postfix_backup.tgz postfix
tar cfzv apache24_backup.tgz apache24

cd /usr/local/www
tar cfzv postfixadmin_backup.tgz postfixadmin
tar cfzv okbsd_backup.tgz okbsd

If you have other users on your system make these tgz files unreadable and/or
move them to a safe directory.
chmod 600 <filename.tgz>

# Next: Setup spam filtering and configure SPF, DKIM, and DMARC email authentication.

04 Dovecot IMAP <- Intro -> 06 SPF DMARC And DKIM