Core Mail Server Setup: Postfix, Dovecot 2.4, and PostfixAdmin

Part 2 of the Building a Modern Mail Server on Debian 13 series

Introduction

This guide walks you through installing and configuring the complete core mail server stack. By the end, you’ll have a fully functional mail server capable of sending and receiving email, with web-based administration for managing virtual domains and mailboxes.

What this guide covers:

  • MariaDB database installation with unix_socket authentication
  • Virtual mail user (vmail) setup
  • Postfix SMTP server for sending and receiving mail
  • Dovecot 2.4 with correct new syntax
  • PostfixAdmin web interface for domain/mailbox management
  • SSL/TLS configuration for secure connections
  • Complete mail flow testing

Prerequisites:

  • Completed Part 1: Mail Server Preparation
  • Variables file at /root/mail-server-vars.sh configured
  • SSL certificates installed with acme.sh
  • Root access to your server

Step 1: Update Variables File with Database Password

First, we need to add the database password to our variables file:

source /root/mail-server-vars.sh

# Generate secure database password
DB_PASSWORD=$(openssl rand -base64 32)
echo "Generated database password: $DB_PASSWORD"
echo "SAVE THIS PASSWORD!"

# Update variables file
sed -i "s/^export DB_PASSWORD=\"\"/export DB_PASSWORD=\"${DB_PASSWORD}\"/" /root/mail-server-vars.sh

# Reload to confirm
source /root/mail-server-vars.sh
echo "DB_PASSWORD is now set: $DB_PASSWORD"

Step 2: MariaDB Installation and Database Setup

2.1: Install MariaDB

apt install -y mariadb-server mariadb-client
systemctl enable mariadb
systemctl start mariadb

2.2: Secure MariaDB

mariadb-secure-installation

Answer the prompts:

  • Enter current root password: [Press Enter]
  • Switch to unix_socket authentication: Y
  • Change root password: n (unix_socket is more secure)
  • Remove anonymous users: Y
  • Disallow root login remotely: Y
  • Remove test database: Y
  • Reload privilege tables: Y

Important: We’re using unix_socket authentication for the root user, which is more secure than password authentication. Root can only connect locally via sudo mariadb.

2.3: Create Database and User

source /root/mail-server-vars.sh

mariadb -u root << EOF
CREATE DATABASE ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
EOF

Note: We’re creating an empty database. PostfixAdmin’s setup wizard will automatically create all the necessary tables (domain, mailbox, alias, etc.).

2.4: Verify Database

mariadb -u $DB_USER -p$DB_PASSWORD $DB_NAME -e "SHOW TABLES;"

Should return:

Empty set (0.00 sec)

The tables will be created when we run PostfixAdmin’s setup in Step 6.

Step 3: Virtual Mail User

Dovecot needs a system user to own the mail directories.

3.1: Create vmail User and Group

source /root/mail-server-vars.sh

# Create group
groupadd -g $VMAIL_GID vmail

# Create user
useradd -g vmail -u $VMAIL_UID -d $VMAIL_HOME -m -s /usr/sbin/nologin vmail

# Create mail directory
mkdir -p $VMAIL_HOME
chown -R vmail:vmail $VMAIL_HOME
chmod -R 770 $VMAIL_HOME

3.2: Verify

id vmail
ls -ld $VMAIL_HOME

Should show:

uid=5000(vmail) gid=5000(vmail) groups=5000(vmail)
drwxrwx--- 2 vmail vmail 4096 Nov 27 12:00 /var/vmail

Step 4: Postfix Installation and Configuration

4.1: Install Postfix

apt install -y postfix postfix-mysql

During installation:

  • General type: Internet Site
  • System mail name: Your FQDN from variables (e.g., mail.example.com)

4.2: Backup Original Configuration

cp /etc/postfix/main.cf /etc/postfix/main.cf.orig
cp /etc/postfix/master.cf /etc/postfix/master.cf.orig

4.3: Create Clean main.cf

source /root/mail-server-vars.sh

cat > /etc/postfix/main.cf << EOF
# Basic configuration
myhostname = ${MX1_FQDN}
mydomain = ${DOMAIN}
myorigin = \$mydomain
mydestination = localhost
mynetworks = 127.0.0.0/8 [::1]/128

# Virtual domain settings
virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = mysql:/etc/postfix/sql/virtual-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/sql/virtual-mailboxes.cf
virtual_alias_maps = mysql:/etc/postfix/sql/virtual-aliases.cf

# TLS parameters
smtpd_tls_cert_file = ${SSL_FULLCHAIN}
smtpd_tls_key_file = ${SSL_KEY}
smtpd_tls_security_level = may
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_ciphers = high
smtpd_tls_exclude_ciphers = aNULL, MD5, DES, 3DES, RC4
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_ciphers = high
smtpd_tls_session_cache_database = btree:\${data_directory}/smtpd_scache

# Outbound TLS
smtp_tls_security_level = may
smtp_tls_session_cache_database = btree:\${data_directory}/smtp_scache
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_ciphers = high

# SASL authentication
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = \$myhostname
broken_sasl_auth_clients = yes

# Mail size limit (50MB)
message_size_limit = 52428800

# Mailbox size limit (disabled, quota handled by Dovecot)
mailbox_size_limit = 0

# SMTP restrictions
smtpd_recipient_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_unauth_destination,
    reject_invalid_hostname,
    reject_non_fqdn_sender,
    reject_non_fqdn_recipient,
    reject_unknown_sender_domain,
    reject_unknown_recipient_domain,
    reject_rbl_client zen.spamhaus.org,
    reject_rbl_client bl.spamcop.net,
    permit

smtpd_helo_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_invalid_helo_hostname,
    reject_non_fqdn_helo_hostname,
    reject_unknown_helo_hostname,
    permit

smtpd_sender_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_non_fqdn_sender,
    reject_unknown_sender_domain,
    permit

# Alias configuration
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases

# Milter support (for future Rspamd integration)
#non_smtpd_milters = inet:localhost:11332
#smtpd_milters = inet:localhost:11332
milter_default_action = accept
milter_protocol = 6

# Miscellaneous
biff = no
append_dot_mydomain = no
readme_directory = no
compatibility_level = 3.6
EOF

4.4: Create Clean master.cf

cat > /etc/postfix/master.cf << 'EOF'
# Postfix master process configuration

# SMTP on port 25 (receiving mail from other servers)
smtp      inet  n       -       y       -       -       smtpd

# Submission on port 587 (authenticated users)
submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject

# SMTPS on port 465 (implicit TLS)
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject

# Other standard services
pickup    unix  n       -       y       60      1       pickup
cleanup   unix  n       -       y       -       0       cleanup
qmgr      unix  n       -       n       300     1       qmgr
tlsmgr    unix  -       -       y       1000?   1       tlsmgr
rewrite   unix  -       -       y       -       -       trivial-rewrite
bounce    unix  -       -       y       -       0       bounce
defer     unix  -       -       y       -       0       bounce
trace     unix  -       -       y       -       0       bounce
verify    unix  -       -       y       -       1       verify
flush     unix  n       -       y       1000?   0       flush
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
smtp      unix  -       -       y       -       -       smtp
relay     unix  -       -       y       -       -       smtp
showq     unix  n       -       y       -       -       showq
error     unix  -       -       y       -       -       error
retry     unix  -       -       y       -       -       error
discard   unix  -       -       y       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       y       -       -       lmtp
anvil     unix  -       -       y       -       1       anvil
scache    unix  -       -       y       -       1       scache
EOF

4.5: Create MySQL Lookup Files

source /root/mail-server-vars.sh

# Create directory
mkdir -p /etc/postfix/sql
chmod 750 /etc/postfix/sql

# virtual-domains.cf
cat > /etc/postfix/sql/virtual-domains.cf << EOF
user = ${DB_USER}
password = ${DB_PASSWORD}
hosts = 127.0.0.1
dbname = ${DB_NAME}
query = SELECT domain FROM domain WHERE domain='%s' AND active = 1
EOF

# virtual-mailboxes.cf
cat > /etc/postfix/sql/virtual-mailboxes.cf << EOF
user = ${DB_USER}
password = ${DB_PASSWORD}
hosts = 127.0.0.1
dbname = ${DB_NAME}
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = 1
EOF

# virtual-aliases.cf
cat > /etc/postfix/sql/virtual-aliases.cf << EOF
user = ${DB_USER}
password = ${DB_PASSWORD}
hosts = 127.0.0.1
dbname = ${DB_NAME}
query = SELECT goto FROM alias WHERE address='%s' AND active = 1
EOF

# Set permissions
chmod 640 /etc/postfix/sql/*
chown root:postfix /etc/postfix/sql/*

4.6: Verify Postfix Chroot DNS Resolution

Postfix runs in a chroot environment and needs /etc/resolv.conf copied to its chroot.

Since systemd-resolved was disabled in Part 1, Postfix automatically maintains this. Verify:

postfix check
# Should return no errors

ls -l /var/spool/postfix/etc/resolv.conf
# Should show: -rw-r--r-- 1 root root ...

Why this works:

  • Postfix automatically copies /etc/resolv.conf to its chroot
  • postfix check updates it with correct ownership (root:root)
  • With systemd-resolved disabled, this happens automatically

4.7: Verify Postfix Configuration

postfix check

Should return no errors. You may see warnings about missing optional files, but there should be no error messages.

4.8: Restart Postfix

systemctl restart postfix
systemctl status postfix

Check logs for errors:

journalctl -u postfix -n 50

Step 5: Dovecot 2.4 Installation and Configuration

Critical: Dovecot 2.4 introduced major syntax changes. This section uses the new Dovecot 2.4 syntax throughout.

Key Dovecot 2.4 changes:

  • Version configuration required – Must specify dovecot_config_version and dovecot_storage_version
  • SSL parameter names changed – ssl_server_cert_file instead of ssl_cert
  • SQL configuration inline – Database connection defined directly in auth config
  • Protocol blocks – New nested syntax for protocol configuration
  • Variable syntax updated – %{user | username | lower} instead of %Lu
  • Auth settings renamed – auth_allow_cleartext instead of disable_plaintext_auth

5.1: Install Dovecot

apt install -y dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql

Note: We’re not installing dovecot-pop3d as POP3 is a legacy protocol. If you need POP3 for specific clients, install it and configure accordingly.

5.2: Backup Original Configuration

cp -r /etc/dovecot /etc/dovecot.orig

5.3: Main Configuration File (dovecot.conf)

cat > /etc/dovecot/dovecot.conf << 'EOF'
# Dovecot 2.4 Configuration for Debian 13 (Trixie)
# Main configuration file

# CRITICAL: Version declaration must be first
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0

# Include all configuration files from conf.d/
!include conf.d/*.conf

# Include authentication configuration
!include conf.d/auth-sql.conf.ext
EOF

5.4: Create Configuration Directory

mkdir -p /etc/dovecot/conf.d

5.5: Authentication Configuration (10-auth.conf)

cat > /etc/dovecot/conf.d/10-auth.conf << 'EOF'
###
### Authentication Configuration
###

# Space separated list of wanted authentication mechanisms
auth_mechanisms = plain login

# Disable LOGIN command and all other plaintext authentications unless
# SSL/TLS is used (CRITICAL for security)
# Dovecot 2.4 uses auth_allow_cleartext instead of disable_plaintext_auth
auth_allow_cleartext = no

# Authentication username format normalization
auth_username_format = %{user | lower}

# Authentication cache settings
auth_cache_size = 10M
auth_cache_ttl = 1 hour
auth_cache_negative_ttl = 1 hour

# Verbose logging for troubleshooting
auth_verbose = yes
auth_verbose_passwords = no
auth_debug = no

###
### Authentication Backends
###

# System authentication disabled - we use SQL only
#!include auth-system.conf.ext

# SQL authentication enabled
!include auth-sql.conf.ext
EOF

5.6: Logging Configuration (10-logging.conf)

cat > /etc/dovecot/conf.d/10-logging.conf << 'EOF'
###
### Logging Configuration
###

log_path = syslog
info_log_path = 
debug_log_path = 
syslog_facility = mail

# Log timestamp format
log_timestamp = "%Y-%m-%d %H:%M:%S "

# Mail debugging (uncomment if needed)
#mail_debug = yes
EOF

5.7: Mail Location and Storage (10-mail.conf)

source /root/mail-server-vars.sh

cat > /etc/dovecot/conf.d/10-mail.conf << EOF
###
### Mail Location and Storage
###

mail_uid = vmail
mail_gid = vmail
mail_privileged_group = mail

# Dovecot 2.4 mail location syntax
mail_driver = maildir
mail_path = ~/Maildir
mail_home = ${VMAIL_HOME}/%{user | domain}/%{user | username}

# Mailbox layout
mailbox_list_layout = fs

# First valid UID/GID (prevents using system users)
first_valid_uid = ${VMAIL_UID}
first_valid_gid = ${VMAIL_GID}

# Address tagging delimiter
recipient_delimiter = +
EOF

5.8: Service Definitions (10-master.conf)

cat > /etc/dovecot/conf.d/10-master.conf << 'EOF'
###
### Dovecot Services
###

service imap-login {
  # Only encrypted IMAPS (port 993)
  inet_listener imaps {
    port = 993
    ssl = yes
  }
  # Explicitly disable port 143 (unencrypted IMAP)
  inet_listener imap {
    port = 0
  }
}

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}

service auth {
  # Auth socket for Postfix SMTP
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
  
  # Auth socket for LMTP
  unix_listener auth-userdb {
    mode = 0600
    user = vmail
  }
  
  user = dovecot
}

service auth-worker {
  user = vmail
}

service dict {
  unix_listener dict {
    mode = 0600
    user = vmail
  }
}
EOF

5.9: SSL/TLS Configuration (10-ssl.conf)

source /root/mail-server-vars.sh

cat > /etc/dovecot/conf.d/10-ssl.conf << EOF
###
### SSL/TLS Configuration
###

ssl = required
ssl_server_cert_file = ${SSL_FULLCHAIN}
ssl_server_key_file = ${SSL_KEY}
ssl_server_dh_file = /usr/share/dovecot/dh.pem

# SSL protocols and ciphers (Mozilla Intermediate)
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl_server_prefer_ciphers = server
EOF

5.10: Namespaces and Special Folders (15-mailboxes.conf)

cat > /etc/dovecot/conf.d/15-mailboxes.conf << 'EOF'
###
### Namespaces and Mailboxes
###

namespace inbox {
  inbox = yes
  
  mailbox Drafts {
    special_use = \Drafts
    auto = subscribe
  }
  
  mailbox Sent {
    special_use = \Sent
    auto = subscribe
  }
  
  mailbox "Sent Messages" {
    special_use = \Sent
  }
  
  mailbox Trash {
    special_use = \Trash
    auto = subscribe
  }
  
  mailbox Junk {
    special_use = \Junk
    auto = subscribe
  }
}
EOF

5.11: IMAP Protocol Settings (20-imap.conf)

cat > /etc/dovecot/conf.d/20-imap.conf << 'EOF'
###
### IMAP Protocol Settings
###

protocols {
  imap = yes
}

protocol imap {
  mail_plugins {
    quota = yes         # Base quota plugin
    imap_quota = yes    # IMAP quota commands (depends on quota)
  }
  mail_max_userip_connections = 50
  imap_idle_notify_interval = 29 mins
}
EOF

5.12: LMTP Protocol Settings (20-lmtp.conf)

source /root/mail-server-vars.sh

cat > /etc/dovecot/conf.d/20-lmtp.conf << EOF
###
### LMTP Protocol Settings
###

protocols {
  lmtp = yes
}

protocol lmtp {
  mail_plugins {
    quota = yes
  }
  
  # Automatically create mailbox if it doesn't exist
  lmtp_save_to_detail_mailbox = yes
  
  # Recipient delimiter for address tagging
  recipient_delimiter = +
  
  # Postmaster address
  postmaster_address = postmaster@${DOMAIN}
}
EOF

5.13: Quota Configuration (90-quota.conf)

cat > /etc/dovecot/conf.d/90-quota.conf << 'EOF'
###
### Quota Configuration
###

quota "User quota" {
  driver = count
}

quota_exceeded_message = User %{user} has exceeded the storage volume. / User %{user} has exhausted allowed storage space.

# Quota rules (fallback values if database doesn't specify)
# Note: PostfixAdmin manages per-user quotas in the database
# These are fallback values if database doesn't specify
# quota_rule = *:storage=1G
# quota_rule2 = Trash:storage=+100M
EOF

5.14: SQL Authentication Backend (auth-sql.conf.ext)

This is the critical file with Dovecot 2.4 inline SQL syntax:

source /root/mail-server-vars.sh

cat > /etc/dovecot/conf.d/auth-sql.conf.ext << EOF
###
### SQL Authentication Backend Configuration
###

###
### SQL Database Connection
###
sql_driver = mysql

mysql localhost {
  user = ${DB_USER}
  password = ${DB_PASSWORD}
  dbname = ${DB_NAME}
}

###
### Password Database - validates user credentials
###
passdb sql {
  query = SELECT username AS user, password \\
          FROM mailbox \\
          WHERE username='%{user | lower}' AND active = '1'
}

###
### User Database - returns home directory, uid, gid
###
userdb sql {
  query = SELECT \\
            CONCAT('${VMAIL_HOME}/', '%{user | domain}', '/', '%{user | username}') AS home, \\
            ${VMAIL_UID} AS uid, \\
            ${VMAIL_GID} AS gid \\
          FROM mailbox \\
          WHERE username='%{user | lower}' AND active='1'
  
  iterate_query = SELECT username FROM mailbox WHERE active='1'
}
EOF

5.15: Set Permissions

chmod 644 /etc/dovecot/dovecot.conf
chmod 755 /etc/dovecot/conf.d
chmod 644 /etc/dovecot/conf.d/*.conf
chown -R root:root /etc/dovecot

5.16: Verify Configuration

# Check if config is valid
doveconf -n

# Alternative verification
doveconf -n > /dev/null && echo "✅ Config OK" || echo "❌ Config has errors"

Expected output: Should show your complete configuration without errors.

5.17: Start and Enable Dovecot

systemctl restart dovecot
systemctl enable dovecot
systemctl status dovecot

5.18: Test Configuration

# Check if Dovecot is listening on correct ports
ss -tlnp | grep dovecot

# Should show ONLY:
# *:993 (IMAPS - encrypted)
# Should NOT show:
# *:143 (IMAP - unencrypted)

# Check logs
journalctl -u dovecot -f*

If port 143 is still open:

# Verify imap listener is disabled in master.conf
doveconf -n | grep -A10 "service imap-login"

# Should show:
# inet_listener imap {
#   port = 0
# }

# If not, check /etc/dovecot/conf.d/10-master.conf and restart
systemctl restart dovecot

Common Troubleshooting:

# If you see permission errors
chmod +x /etc/dovecot

# If you see SQL connection errors
doveconf -n | grep -A5 "mysql localhost"

# Test authentication (requires existing mailbox created in next step)
# doveadm auth test user@example.com

Step 6: Apache Installation and Basic Setup

Before configuring PostfixAdmin, we’ll install Apache and verify it’s working. This allows us to test connectivity before adding complexity.

6.1: Install Apache

apt install -y apache2
systemctl enable apache2
systemctl start apache2
systemctl status apache2

6.2: Test Basic Apache Access

If using reverse proxy setup: Access your server’s IP or hostname via your reverse proxy to see the default Apache page. This confirms Apache is running and accessible.

If using direct access: You can test Apache on the default port 80 (we’ll configure custom ports later):

curl -I http://localhost

Should show: HTTP/1.1 200 OK and Server: Apache

Step 7: Configure PostfixAdmin Access on Non-Standard Port

We’ll configure PostfixAdmin to be accessible on a non-standard port (25443) for enhanced security and container compatibility.

Why Use Port 25443?

Security Benefits:

  • Invisible to port scanners – Standard scanning tools check ports 80, 443, 8080, 8443 – not 25443
  • Protection from automated attacks – Bots and scripts target known ports only
  • Reduces attack surface – Attackers must discover both hostname AND port
  • Additional authentication layer – Port number acts as additional “secret”
  • Cleaner logs – No noise from random scanning attempts
  • Not publicly indexed – Search engines won’t discover the admin interface

Why Port 25443 Specifically?

  • Memorable: 25 (SMTP mail) + 443 (HTTPS) = 25443
  • Logical and purposeful for a mail server admin interface
  • Not in commonly scanned ranges

Note: You can use any port above 1024 (e.g., 9443, 40000). This guide uses 25443 as an example.

Container/VM Compatibility

Important: On Incus/LXD hosts where a container is already using ports 80/443 (common with reverse proxy containers), direct access to PostfixAdmin on standard ports won’t work without additional reverse proxy configuration. Using a non-standard port like 25443 provides:

  • ✅ Direct access via simple port forwarding
  • ✅ No conflicts with existing services
  • ✅ No complex reverse proxy setup needed

Alternative: If you need PostfixAdmin accessible on standard port 443 via a dedicated subdomain (e.g., https://pa.example.com), this requires reverse proxy configuration (Nginx, Apache, Traefik, etc.) which is beyond the scope of this guide.

7.1: Configure Apache for Port 25443

Add port 25443 to Apache:

# Add port 25443 to Apache
echo "Listen 25443" >> /etc/apache2/ports.conf

7.2: Create Virtual Host

source /root/mail-server-vars.sh

cat > /etc/apache2/sites-available/postfixadmin.conf << EOF
<VirtualHost *:25443>
    ServerName ${MAIL_FQDN}
    
    # Temporarily use default Apache directory for testing
    DocumentRoot /var/www/html
    
    <Directory /var/www/html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
    
    ErrorLog \${APACHE_LOG_DIR}/postfixadmin_error.log
    CustomLog \${APACHE_LOG_DIR}/postfixadmin_access.log combined
    
    # SSL Configuration
    SSLEngine on
    SSLCertificateFile ${SSL_FULLCHAIN}
    SSLCertificateKeyFile ${SSL_KEY}
    
    # Modern SSL Configuration
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder off
    SSLSessionTickets off
</VirtualHost>
EOF

# Enable the site and SSL module
a2enmod ssl
a2ensite postfixadmin
systemctl reload apache2

Test Apache Access:

You should now be able to access the default Apache page at:

https://mail.example.com:25443

This confirms Apache is working with SSL on the custom port before we install PostfixAdmin.

Note: We’ll update the DocumentRoot to point to PostfixAdmin in Step 8.10 after installation.

7.3: Configure Port Forwarding

Ensure port 25443 is forwarded from your host to the mail server container/VM.

Add a forwarding rule for port 25443/tcp using your firewall configuration (iptables, nftables, etc.). You should already have forwarding rules for mail ports 25, 465, 587, and 993 – add 25443 to that same configuration.

7.4: Firewall Rules

Ensure your firewall allows incoming traffic on port 25443/tcp to reach the mail server.

7.5: Access PostfixAdmin

Access PostfixAdmin at:

https://mail.example.com:25443

Or using the mx hostname:

https://mx1.example.com:25443

Security Best Practices:

  • Only share this URL with authorized administrators
  • Consider IP whitelisting at firewall level for additional security
  • Use strong passwords and enable TOTP 2FA (Step 9.4)
  • Monitor access logs: /var/log/apache2/postfixadmin_access.log

Tip: Bookmark the URL in your browser. The port number provides security through obscurity – it’s easy to remember but hard to guess.

Step 8: PostfixAdmin Installation and Configuration

PostfixAdmin 4.0.1 provides a modern web interface for managing domains, mailboxes, and aliases, with TOTP 2FA support.

Security Architecture

We’ll configure PostfixAdmin with enhanced security:

  • Dedicated user (postfixadmin) instead of www-data
  • PHP-FPM instead of mod_php for process isolation
  • Restricted permissions on application files
  • No php-imap (discontinued due to security concerns, not required by PostfixAdmin 4.0+)

8.1: Add PHP Repository

Debian 13 (Trixie) ships with PHP 8.4 by default, but we’ll add Ondřej Surý’s PHP repository for future flexibility and faster security updates.

Why add Sury repository even though Debian 13 has PHP 8.4?

  • Future upgrades: Easy path to PHP 8.5, 8.6, 8.7 when released
  • Faster security patches: Sury often releases fixes faster than Debian
  • Independent upgrades: Update PHP without waiting for Debian point releases
  • Same maintainer: Ondřej Surý maintains both Debian and Sury PHP packages
  • Industry standard: Widely used for production PHP applications

Install wget first:

apt install -y wget

Add the repository:

# Add GPG key
wget -qO /etc/apt/keyrings/php-sury.gpg https://packages.sury.org/php/apt.gpg

# Install lsb-release package
apt install lsb-release -y

# Add repository
echo "deb [signed-by=/etc/apt/keyrings/php-sury.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php-sury.list

# Update package list
apt update

Note: If you prefer to use only Debian’s packages, you can skip this step and install php php-fpm php-cli ... instead of php8.4 php8.4-fpm php8.4-cli ... in the next step. However, the Sury repository is recommended for production mail servers.

8.2: Install PHP and Required Extensions

apt install -y php8.4 php8.4-fpm php8.4-cli php8.4-mysql \
  php8.4-mbstring php8.4-xml php8.4-intl php8.4-gd php8.4-curl php8.4-sqlite3 zip

Important packages:

  • php8.4-gd – Required for TOTP 2FA QR code generation
  • php8.4-sqlite3 and zip – Required for PostfixAdmin installation
  • Note: We do NOT install php8.4-imap – it’s been discontinued due to security concerns and PostfixAdmin 4.0+ doesn’t require it

8.3: Create Dedicated PostfixAdmin User

For security, run PostfixAdmin under its own user account:

# Create postfixadmin system user
useradd -r -s /usr/sbin/nologin -d /var/www/postfixadmin -c "PostfixAdmin" postfixadmin

8.4: Configure Apache for PHP-FPM

Apache was already installed in Step 6. Now we’ll enable the required modules for PHP-FPM:

# Enable proxy modules for PHP-FPM
a2enmod proxy_fcgi setenvif rewrite headers

# Enable PHP-FPM configuration
a2enconf php8.4-fpm

systemctl restart apache2

Note: We don’t need to enable the SSL module again – it was already enabled in Step 7.2.

Why PHP-FPM instead of mod_php?

  • Process isolation – Each application runs as its own user
  • Better resource management – Independent PHP processes
  • Enhanced security – Compromise of one app doesn’t affect others
  • Modern standard – Industry best practice

8.5: Download PostfixAdmin 4.0.1

cd /tmp
wget https://github.com/postfixadmin/postfixadmin/archive/refs/tags/postfixadmin-4.0.1.tar.gz
tar -xzf postfixadmin-4.0.1.tar.gz
mv postfixadmin-postfixadmin-4.0.1 /var/www/postfixadmin

Note: We use wget for GitHub downloads because it handles redirects more reliably than curl for release archives. Although curl is preferred for most downloads, wget is better suited for GitHub’s release system.

8.6: Run PostfixAdmin Install Script

cd /var/www/postfixadmin
chmod +x install.sh
./install.sh

Expected output:

 * Checking for composer.phar
 * 'composer' not found in your path, will try to download from https://getcomposer.org/download/latest-stable/composer.phar
 * Using composer ( /var/www/postfixadmin/composer.phar )
 * Installing libraries ( composer install --no-dev ... )
Composer plugins have been disabled for safety in this non-interactive session.
Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.
Do not run Composer as root/super user! See https://getcomposer.org/root for details
No composer.lock file present. Updating dependencies to latest instead of installing from lock file.
Loading composer repositories with package information
Updating dependencies
Lock file operations: 86 installs, 0 updates, 0 removals
  - Locking bacon/bacon-qr-code (v3.0.3)
  - Locking beberlei/assert (v3.3.3)
  [... many more packages ...]
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 86 installs, 0 updates, 0 removals
  [... installation progress ...]

Warning:
  templates_c directory didn't exist, now created.
  You should change the ownership and reduce permissions on templates_c to 750.
  The ownership needs to match the user used to execute PHP scripts, perhaps 'www-data' or 'httpd'
  e.g. chown www-data templates_c && chmod 750 templates_c
Please continue configuration / setup within your web browser.

About the warnings:

  1. Composer superuser warning – Safe to ignore. The install.sh script must run as root to install system packages. Composer disables plugins automatically for safety when running as root, which is the correct behavior.
  2. templates_c ownership warning – Expected. We’ll set the correct ownership (postfixadmin user, not www-data) in Step 8.8 for better security isolation.
  3. Web browser setup – We’ll ignore this and use the CLI method instead (Step 8.11).

Remove install.sh for security:

rm /var/www/postfixadmin/install.sh

Security: Like setup.php, the install.sh script should be removed after use to prevent potential security issues if someone gains access to the filesystem.

8.7: Configure PostfixAdmin

source /root/mail-server-vars.sh

cd /var/www/postfixadmin

# Create PostfixAdmin configuration
cat > config.local.php << EOF
<?php
\$CONF['configured'] = true;

// Database configuration
\$CONF['database_type'] = 'mysqli';
\$CONF['database_host'] = 'localhost';
\$CONF['database_user'] = '${DB_USER}';
\$CONF['database_password'] = '${DB_PASSWORD}';
\$CONF['database_name'] = '${DB_NAME}';

// Site URL
\$CONF['default_language'] = 'en';

// Domain Configuration
\$CONF['default_aliases'] = array (
    'abuse' => 'abuse@${DOMAIN}',
    'hostmaster' => 'hostmaster@${DOMAIN}',
    'postmaster' => 'postmaster@${DOMAIN}',
    'webmaster' => 'webmaster@${DOMAIN}'
);

// Mail configuration
\$CONF['domain_path'] = 'YES';
\$CONF['domain_in_mailbox'] = 'NO';
\$CONF['quota'] = 'YES';
\$CONF['quota_multiplier'] = '1024000';

// Password settings - ARGON2I is more secure
\$CONF['encrypt'] = 'ARGON2I';
\$CONF['dovecotpw'] = '/usr/bin/doveadm pw -r 5';

// Enable TOTP 2FA support
\$CONF['totp'] = 'YES';

// Maximum quota per mailbox (in MB) - actual quota set when creating mailbox
\$CONF['maxquota'] = '10240';  // 10 GB maximum

// Mailbox settings
\$CONF['mailboxes'] = '50';
\$CONF['aliases'] = '50';

// Virtual mail settings
\$CONF['transport'] = 'YES';
\$CONF['transport_default'] = 'virtual';
\$CONF['transport_options'] = array(
    'virtual',
    'local',
    'relay'
);

// Admin email for notifications
\$CONF['admin_email'] = 'Support Person <support@${DOMAIN}>';

// Footer link configuration (prevents "Return to change-this-to-your.domain.tld")
\$CONF['show_footer_text'] = 'YES';
\$CONF['footer_text'] = 'Return to ${MX1_FQDN}';
\$CONF['footer_link'] = 'https://${MX1_FQDN}:25443/main.php';
?>
EOF

Key configuration changes:

  • Using ARGON2I encryption (more secure than SHA512-CRYPT)
  • TOTP 2FA enabled for enhanced security
  • Quota multiplier set for MB units (1024000 bytes per MB)
  • maxquota set to 10240 MB (10 GB maximum per mailbox)
  • Actual quota set individually when creating each mailbox
  • Admin email configured for notifications
  • Footer configured to link back to PostfixAdmin (prevents default “change-this-to-your.domain.tld” text)

8.8: Set Permissions (Security Hardening)

Set strict permissions with the dedicated postfixadmin user:

# Set ownership to postfixadmin user
chown -R postfixadmin:postfixadmin /var/www/postfixadmin

# Restrict permissions - read-only except where needed
chmod -R 755 /var/www/postfixadmin
chmod -R 750 /var/www/postfixadmin/templates_c

# Ensure /etc/dovecot has execute permissions (for doveadm)
chmod +x /etc/dovecot

Test that doveadm works with postfixadmin user:

# Test as postfixadmin user
sudo -u postfixadmin /usr/bin/doveadm pw -s ARGON2I -p testpassword

Should output an ARGON2I hash like: $argon2i$v=19$m=65536,t=3,p=4$...

If you get permission errors:

This is unlikely with our setup, but if doveadm pw fails with permission errors:

# Option 1: Give postfixadmin user read access to Dovecot configs (safe)
sudo chmod -R o+rX /etc/dovecot/

# Option 2: If using ACLs (alternative)
sudo setfacl -R -m u:postfixadmin:rx /etc/dovecot/

Security Architecture Explanation:

Our setup uses a dedicated postfixadmin user for process isolation, which is a security best practice even on a dedicated mail server.

Why use a dedicated user instead of www-data?

While this is a dedicated mail server (not running WordPress or other web apps), the isolation principle still provides important benefits:

  1. Future-proofing: If you later add Roundcube webmail (Part 5 of this series), it would run under its own roundcube user with a separate PHP-FPM pool
  2. Defense in depth: Each component runs under its own user (postfix, dovecot, postfixadmin, roundcube)
  3. Best practice: Professional mail servers use process isolation regardless of what else runs on the system
  4. Limited blast radius: If PostfixAdmin has a security vulnerability, the attacker only gets postfixadmin user, not system-wide access

Traditional setup (single www-data user):

All web apps → www-data user → shared access to everything

Our setup (dedicated users):

PostfixAdmin → postfixadmin user (isolated)
Roundcube   → roundcube user (isolated)     [Part 5]
Other apps  → separate users (isolated)

Comparison with vulnerable setups:

Some tutorials add www-data to the dovecot group to allow password generation. This is problematic on mixed-use servers where multiple web applications share the www-data user. See this security concern for details. Our approach avoids this by:

  • Using dedicated users per application
  • Not requiring dovecot group membership
  • Maintaining proper isolation between services

Note: The install.sh script created the templates_c directory but suggested using www-data ownership. We ignore that advice and use the postfixadmin user instead for better security isolation. If the directory doesn’t exist for some reason, create it manually:

mkdir -p /var/www/postfixadmin/templates_c
chown postfixadmin:postfixadmin /var/www/postfixadmin/templates_c
chmod 750 /var/www/postfixadmin/templates_c

Security Benefits:

  • Application files owned by postfixadmin, not www-data
  • If PHP-FPM is compromised, attacker gets postfixadmin user, not www-data
  • Limits blast radius of potential security issues
  • Follows principle of least privilege

8.9: Configure PHP-FPM Pool for PostfixAdmin

Create a dedicated PHP-FPM pool running as the postfixadmin user:

cat > /etc/php/8.4/fpm/pool.d/postfixadmin.conf << 'EOF'
[postfixadmin]
user = postfixadmin
group = postfixadmin
listen = /run/php/php8.4-fpm-postfixadmin.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500

php_admin_value[error_log] = /var/log/php8.4-fpm-postfixadmin.log
php_admin_flag[log_errors] = on
EOF

# Restart PHP-FPM
systemctl restart php8.4-fpm

Why a dedicated pool?

  • Runs as postfixadmin user (not www-data)
  • Independent resource limits
  • Isolated error logging
  • Can’t interfere with other PHP applications

8.10: Update Apache Virtual Host for PostfixAdmin with PHP-FPM

Now that PostfixAdmin is installed, update the virtual host to:

  1. Point DocumentRoot to PostfixAdmin (instead of /var/www/html)
  2. Use the dedicated PHP-FPM pool
source /root/mail-server-vars.sh

cat > /etc/apache2/sites-available/postfixadmin.conf << EOF
<VirtualHost *:25443>
    ServerName ${MAIL_FQDN}
    
    # Now point to PostfixAdmin
    DocumentRoot /var/www/postfixadmin/public
    
    <Directory /var/www/postfixadmin/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
        
        # Use dedicated PHP-FPM pool
        <FilesMatch \.php$>
            SetHandler "proxy:unix:/run/php/php8.4-fpm-postfixadmin.sock|fcgi://localhost"
        </FilesMatch>
    </Directory>
    
    ErrorLog \${APACHE_LOG_DIR}/postfixadmin_error.log
    CustomLog \${APACHE_LOG_DIR}/postfixadmin_access.log combined
    
    # SSL Configuration
    SSLEngine on
    SSLCertificateFile ${SSL_FULLCHAIN}
    SSLCertificateKeyFile ${SSL_KEY}
    
    # Modern SSL Configuration
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder off
    SSLSessionTickets off
</VirtualHost>
EOF

# Reload Apache
systemctl reload apache2

Key Changes from Step 7.2:

  • DocumentRoot changed from /var/www/html (Apache test page) to /var/www/postfixadmin/public
  • Added PHP-FPM handler using dedicated socket: /run/php/php8.4-fpm-postfixadmin.sock
  • PHP processes run as postfixadmin user, not www-data
  • Better security isolation

8.11: Initialize Database and Create Superadmin

PostfixAdmin requires the database schema to be created before adding admin accounts.

Create database tables:

cd /var/www/postfixadmin

# Run upgrade.php to create all database tables
php public/upgrade.php

This creates all necessary tables: admin, alias, domain, mailbox, quota, quota2, log, config, domain_admins, and more.

Verify tables were created:

mariadb -e "SHOW TABLES FROM postfixadmin;"

Should show all PostfixAdmin tables.

Why Use CLI Instead of Web-Based setup.php?

PostfixAdmin traditionally uses a web-based setup wizard (setup.php), but we’re using the CLI method instead for several important reasons:

CLI Advantages:

  1. No setup password needed – The web setup requires generating a complex $CONF['setup_password'] hash like: $CONF['setup_password'] = '$2y$10$.tW14352cmjVC3N07u9MQOphugNCl5tIOAdwhYLs21XT7/TwR7Lo.'; This is confusing, error-prone, and unnecessary with CLI.
  2. Fully scriptable – Can be automated in deployment scripts or Ansible playbooks
  3. More secure – Requires SSH access instead of web access
  4. No web exposure – The setup interface isn’t accessible via browser
  5. Faster – Direct command execution, no clicking through web forms
  6. Professional – Production environments rarely use web-based setup wizards
  7. Cleaner – No need to manage setup.php files or setup passwords

Traditional Web Setup Process (avoided):

1. Generate setup_password hash with: php -r "echo password_hash('your-password', PASSWORD_DEFAULT);"
2. Add $CONF['setup_password'] to config.local.php
3. Access https://mail.example.com:25443/setup.php in browser
4. Enter setup password
5. Create admin account through web form
6. Remove setup.php file
7. Remove setup_password from config

Our CLI Approach (much simpler):

1. Run: scripts/postfixadmin-cli admin add admin@example.com --password 'YourPassword' --superadmin 1 --active 1
2. Done!

The CLI method is cleaner, more secure, and avoids the complexity of managing setup passwords entirely.

Create the superadmin account:

source /root/mail-server-vars.sh

cd /var/www/postfixadmin

# Create superadmin user (postfixadmin-cli is a shell script, not PHP)
scripts/postfixadmin-cli admin add admin@${DOMAIN} \
  --password 'YourStrongPasswordHere' \
  --password2 'YourStrongPasswordHere' \
  --superadmin 1 \
  --active 1

Important:

  • Replace YourStrongPasswordHere with a strong password and save it securely!
  • Note: postfixadmin-cli is a shell script, so run it directly without php prefix

This command will:

  1. Create the superadmin account in the existing database tables
  2. Grant full privileges (superadmin = 1)
  3. Activate the account immediately
  4. Hash the password using ARGON2I

Verify the admin was created:

scripts/postfixadmin-cli admin view admin@${DOMAIN}

Should show your admin account details with super_admin: 1.

8.12: Remove setup.php (Security)

Even though we used the CLI setup method, the setup.php file still exists and should be removed to prevent potential security issues:

rm -f /var/www/postfixadmin/public/setup.php

Security Note: The setup.php file can be used to create additional admin accounts if left accessible. Always remove it after initial setup is complete.

8.13: Managing Admin Accounts via CLI

PostfixAdmin’s CLI tool allows you to manage admin accounts from the command line, which is useful for adding additional admins or recovering from a lost password.

View existing admin:

cd /var/www/postfixadmin
scripts/postfixadmin-cli admin view admin@example.com

Shows admin details including superadmin status.

Add an additional superadmin:

source /root/mail-server-vars.sh

cd /var/www/postfixadmin
scripts/postfixadmin-cli admin add secondadmin@${DOMAIN} \
  --password 'StrongPassword123' \
  --password2 'StrongPassword123' \
  --superadmin 1 \
  --active 1

Add a regular (non-super) admin for a specific domain:

scripts/postfixadmin-cli admin add domainadmin@example.com \
  --password 'StrongPassword123' \
  --password2 'StrongPassword123' \
  --superadmin 0 \
  --active 1

Then assign them to a domain via the web interface.

Change admin password (lost password recovery):

cd /var/www/postfixadmin
scripts/postfixadmin-cli admin password admin@example.com \
  --password 'NewStrongPassword' \
  --password2 'NewStrongPassword'

This is useful if you’ve lost access to an admin account.

List all admins:

mariadb -e "SELECT username, active, superadmin, created, modified FROM postfixadmin.admin;"

This shows all admin accounts with their superadmin status and timestamps.

Note: Using direct database name (postfixadmin.admin) instead of variables makes it easier to run without sourcing the variables file first.

Delete an admin:

scripts/postfixadmin-cli admin delete admin@example.com

Tip: The CLI method is more secure than web-based password resets because it requires direct server access (SSH). Keep these commands documented in a secure location for emergency access recovery.

Security Architecture Summary

The PostfixAdmin setup follows security best practices:

Process Isolation:

  • ✅ Dedicated postfixadmin system user
  • ✅ PHP-FPM pool running as postfixadmin (not www-data)
  • ✅ Independent from other web applications

File Permissions:

  • ✅ Application owned by postfixadmin:postfixadmin
  • ✅ Restricted permissions (755/750)
  • ✅ No write access from web server

Attack Surface Reduction:

  • ✅ No mod_php (avoids running as privileged www-data)
  • ✅ No php-imap (discontinued, security concerns)
  • ✅ setup.php removed after initial setup
  • ✅ install.sh removed after initial setup
  • ✅ Non-standard port (25443) reduces discoverability

Modern Security:

  • ✅ ARGON2I password hashing
  • ✅ TOTP 2FA support
  • ✅ TLS 1.2+ only
  • ✅ CLI-based administration

Defense in Depth: Each component runs under its own user (Postfix, Dovecot, PostfixAdmin). If PostfixAdmin is compromised, attacker gains only the postfixadmin user with limited privileges, not broader system access. This limits lateral movement between services.

Process Isolation Benefits:

  • ✅ Each service runs as a dedicated user
  • ✅ Compromise of one service doesn’t affect others
  • ✅ Follows principle of least privilege
  • ✅ Professional production architecture
  • ✅ Ready for future expansion (e.g., Roundcube webmail in Part 5)

Comparison to Traditional Setups: Some tutorials run all web applications as www-data and add this user to the dovecot group for password generation. This creates a security risk on mixed-use servers. Our approach uses dedicated users per application, requiring no group membership and maintaining proper isolation.

Step 9: Create Test Domain and Mailbox

9.1: Access PostfixAdmin

Open your browser and navigate to:

https://mail.example.com

(Replace with your $MAIL_FQDN)

Login with the admin credentials you created in Step 8.11.

9.2: Create a Virtual Domain

  1. Go to Domain List
  2. Click New Domain
  3. Enter your domain (e.g., example.com)
  4. Set the following:
    • Aliases: 10
    • Mailboxes: 10
    • Max Quota (MB): 10240 (10 GB – maximum quota per mailbox in this domain)
  5. Click Add Domain

Note: The “Max Quota” for the domain sets the ceiling for individual mailbox quotas. Each mailbox can be assigned up to this limit when created.

9.3: Create a Test Mailbox

  1. Go to Virtual List
  2. Select your domain from the dropdown
  3. Click Add Mailbox
  4. Fill in the form:
    • Username: test
    • Password: (strong password – save this!)
    • Name: Test User
    • Quota: 1024 MB (1 GB – or set as needed per user)
  5. Click Add Mailbox

The mailbox test@example.com is now created.

Note about quotas: The maxquota setting (10240 MB = 10 GB) is the maximum quota you can assign to any mailbox. The actual quota is set individually when creating each mailbox, allowing you to allocate different storage limits per user based on their needs.

Note: Passwords are now hashed using ARGON2I, which is more secure than SHA512-CRYPT. The hash starts with $argon2i$.

9.4: (Optional) Enable TOTP 2FA

If you want to enable Two-Factor Authentication for your admin account:

  1. Log in to PostfixAdmin
  2. Go to Profile or My Account
  3. Look for TOTP Two-Factor Authentication
  4. Scan the QR code with an authenticator app (Google Authenticator, Authy, etc.)
  5. Enter the verification code
  6. Save your backup codes securely

9.5: Verify Database Entries

source /root/mail-server-vars.sh

# Check domain
mariadb -u ${DB_USER} -p${DB_PASSWORD} -e "SELECT * FROM ${DB_NAME}.domain;"

# Check mailbox (verify ARGON2I hash)
mariadb -u ${DB_USER} -p${DB_PASSWORD} -e "SELECT username, password, active FROM ${DB_NAME}.mailbox;"

The password should start with $argon2i$, confirming ARGON2I encryption.

Step 10: Testing and Verification

10.1: Test Postfix MySQL Lookups

source /root/mail-server-vars.sh

# Test domain lookup
postmap -q ${DOMAIN} mysql:/etc/postfix/sql/virtual-domains.cf

# Test mailbox lookup (replace with your actual test email)
postmap -q test@${DOMAIN} mysql:/etc/postfix/sql/virtual-mailboxes.cf

Should return:

  • Domain lookup: example.com
  • Mailbox lookup: example.com/test/ (the maildir path)

10.2: Test Dovecot Authentication

doveadm auth test test@example.com

Enter the password when prompted. Should show:

passdb: test@example.com auth succeeded
extra fields:
  user=test@example.com

10.3: Test SMTP Connection

telnet localhost 25

Type:

EHLO test
QUIT

Should show Postfix responding with capabilities including:

  • 250-STARTTLS
  • 250-AUTH PLAIN LOGIN
  • 250-SIZE 52428800

10.4: Test IMAPS Connection

openssl s_client -connect localhost:993 -crlf

Should connect successfully. Type:

a1 LOGIN test@example.com your_password
a2 LIST "" "*"
a3 LOGOUT

Should show successful login and mailbox listing (Drafts, Sent, Trash, Junk).

10.5: Send Test Email

apt install -y mailutils

# Source variables
source /root/mail-server-vars.sh

# Send test email
echo "This is a test email" | mail -s "Test" -a "From: test@${DOMAIN}" test@${DOMAIN}

Check mail log:

journalctl -u postfix -n 50

Should show:

  • Postfix accepting the message
  • LMTP delivery to Dovecot
  • Message saved to mailbox

Check mailbox on disk:

ls -la /var/vmail/example.com/test/Maildir/new/

Should show the delivered message file.

10.6: Verify Email via IMAP

openssl s_client -connect localhost:993 -crlf

Login and check inbox:

a1 LOGIN test@example.com your_password
a2 SELECT INBOX
a3 FETCH 1 BODY[]
a4 LOGOUT

Should display the test email content.

10.7: Test Submission Port (587)

openssl s_client -connect localhost:587 -starttls smtp -crlf

Should connect and show:

  • 220 mail.example.com ESMTP Postfix
  • TLS negotiation successful

Type:

EHLO test
AUTH PLAIN

(Press Ctrl+C to exit)

This confirms authenticated submission is working.

Core Mail Server Complete!

Your core mail server is now fully operational with all components tested:

MariaDB installed with unix_socket authentication
Virtual mail user (vmail) created and configured
Postfix configured for virtual domains with MySQL backend
Dovecot 2.4 configured with correct new syntax for IMAP/LMTP
PostfixAdmin 4.0.1 web interface with ARGON2I encryption and TOTP 2FA support
PHP 8.4 from Sury repository with all required extensions
SSL/TLS enabled for all services
Test domain and mailbox created via PostfixAdmin
Complete mail flow tested and verified

What’s working:

  • ✅ Send and receive email locally
  • ✅ SMTP authentication (ports 587/465 for authenticated users)
  • ✅ IMAPS access (port 993, encrypted only)
  • ✅ LMTP delivery from Postfix to Dovecot
  • ✅ Web-based domain/mailbox management
  • ✅ ARGON2I password hashing (more secure than SHA512-CRYPT)
  • ✅ TOTP Two-Factor Authentication support
  • ✅ SSL/TLS encryption throughout
  • ✅ MySQL integration for virtual domains/users

Not yet configured:

  • Intrusion prevention (Part 3: CrowdSec) – Do this next for security!
  • Spam filtering and DKIM (Part 4: Rspamd)
  • Webmail interface (Part 5: Roundcube)
  • Monitoring and maintenance (Part 6)

Important DNS Records for External Email

Before your mail server can send and receive external email, you must configure these DNS records:

MX Record

example.com.  IN  MX  10  mx1.example.com.

A/AAAA Records

mx1.example.com.     IN  A     your.server.ip.address
mail.example.com.    IN  CNAME mx1.example.com.
imap.example.com.    IN  CNAME mx1.example.com.
smtp.example.com.    IN  CNAME mx1.example.com.

SPF Record

example.com.  IN  TXT  "v=spf1 mx -all"

DMARC Record (basic, monitoring mode)

_dmarc.example.com.  IN  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com"

Note: DKIM signing will be configured in Part 4 (Rspamd).

Dovecot 2.4 Configuration Structure

Minimal Production Setup

Our configuration uses only essential files for a secure, production-ready mail server. This minimal approach:

  • Simplifies troubleshooting
  • Reduces attack surface
  • Makes maintenance easier
  • Is production-proven
/etc/dovecot/
├── dovecot.conf                    # Main file with version declarations
├── conf.d/
│   ├── 10-auth.conf               # Authentication mechanisms and settings
│   ├── 10-logging.conf            # Logging configuration
│   ├── 10-mail.conf               # Mail storage location (Dovecot 2.4 syntax)
│   ├── 10-master.conf             # Service definitions and sockets
│   ├── 10-ssl.conf                # SSL/TLS configuration (new parameter names)
│   ├── 15-mailboxes.conf          # Special folders (Drafts, Sent, Trash, Junk)
│   ├── 20-imap.conf               # IMAP protocol settings
│   ├── 20-lmtp.conf               # LMTP mail delivery protocol
│   ├── 90-quota.conf              # Quota configuration
│   └── auth-sql.conf.ext          # SQL authentication (inline database config)
└── dovecot.orig/                   # Backup of original Debian config

Security Highlights

Encrypted connections only:

  • Port 993 (IMAPS) – Encrypted from first packet
  • Port 143 (IMAP) disabled – No plaintext exposure
  • auth_allow_cleartext = no – Prevents passwords over unencrypted connections

Modern authentication:

  • auth_username_format = %{user | lower} – Normalizes usernames
  • ARGON2I password hashing via PostfixAdmin (more secure)
  • SQL-based virtual users (no system users)

Dovecot 2.4 specific:

  • Version declarations properly set
  • New SSL parameter names (ssl_server_cert_file, etc.)
  • Inline SQL driver configuration
  • Updated variable syntax (%{user | domain} instead of %d)

Configuration Files Summary

FilePurposeKey Settings
/root/mail-server-vars.shCentral configurationAll domains, paths, credentials
/etc/postfix/main.cfPostfix main configVirtual domains, TLS, SASL
/etc/postfix/master.cfPostfix servicesSubmission (587), SMTPS (465)
/etc/postfix/sql/*.cfMySQL lookupsDomain, mailbox, alias queries
/etc/dovecot/dovecot.confDovecot mainVersion declarations
/etc/dovecot/conf.d/10-mail.confMail storageMaildir location (2.4 syntax)
/etc/dovecot/conf.d/10-ssl.confDovecot SSLCertificate paths (2.4 names)
/etc/dovecot/conf.d/auth-sql.conf.extAuthenticationInline SQL config (2.4 syntax)
/var/www/postfixadmin/config.local.phpPostfixAdminDatabase, domain defaults, TOTP 2FA

Next Steps

Critical next step: Proceed immediately with Part 3: CrowdSec Intrusion Prevention to protect your exposed services:

  • SSH brute force protection
  • PostfixAdmin web interface protection
  • Postfix SMTP authentication protection
  • Automated IP blocking with crowd-sourced threat intelligence

Your mail server is functional but currently vulnerable to attacks. CrowdSec provides essential security before you start using the server in production.

Return to: Mail Server Series Index