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.