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.shconfigured - 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 mariadb2.2: Secure MariaDB
mariadb-secure-installationAnswer 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;
EOFNote: 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_HOME3.2: Verify
id vmail
ls -ld $VMAIL_HOMEShould show:
uid=5000(vmail) gid=5000(vmail) groups=5000(vmail)
drwxrwx--- 2 vmail vmail 4096 Nov 27 12:00 /var/vmailStep 4: Postfix Installation and Configuration
4.1: Install Postfix
apt install -y postfix postfix-mysqlDuring 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.orig4.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
EOF4.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
EOF4.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.confto its chroot postfix checkupdates it with correct ownership (root:root)- With systemd-resolved disabled, this happens automatically
4.7: Verify Postfix Configuration
postfix checkShould 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 postfixCheck logs for errors:
journalctl -u postfix -n 50Step 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_versionanddovecot_storage_version - SSL parameter names changed –
ssl_server_cert_fileinstead ofssl_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_cleartextinstead ofdisable_plaintext_auth
5.1: Install Dovecot
apt install -y dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysqlNote: 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.orig5.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
EOF5.4: Create Configuration Directory
mkdir -p /etc/dovecot/conf.d5.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
EOF5.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
EOF5.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 = +
EOF5.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
}
}
EOF5.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
}
}
EOF5.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
}
EOF5.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}
}
EOF5.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
EOF5.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'
}
EOF5.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/dovecot5.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 dovecot5.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 dovecotCommon 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.comStep 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 apache26.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://localhostShould 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.conf7.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 apache2Test Apache Access:
You should now be able to access the default Apache page at:
https://mail.example.com:25443This 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:25443Or using the mx hostname:
https://mx1.example.com:25443Security 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 ofwww-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 wgetAdd 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 updateNote: 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 zipImportant packages:
php8.4-gd– Required for TOTP 2FA QR code generationphp8.4-sqlite3andzip– 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" postfixadmin8.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 apache2Note: 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/postfixadminNote: 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.shExpected 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:
- Composer superuser warning – Safe to ignore. The
install.shscript must run as root to install system packages. Composer disables plugins automatically for safety when running as root, which is the correct behavior. - templates_c ownership warning – Expected. We’ll set the correct ownership (
postfixadminuser, notwww-data) in Step 8.8 for better security isolation. - 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.shSecurity: 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';
?>
EOFKey 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)
maxquotaset 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/dovecotTest that doveadm works with postfixadmin user:
# Test as postfixadmin user
sudo -u postfixadmin /usr/bin/doveadm pw -s ARGON2I -p testpasswordShould 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:
- Future-proofing: If you later add Roundcube webmail (Part 5 of this series), it would run under its own
roundcubeuser with a separate PHP-FPM pool - Defense in depth: Each component runs under its own user (postfix, dovecot, postfixadmin, roundcube)
- Best practice: Professional mail servers use process isolation regardless of what else runs on the system
- Limited blast radius: If PostfixAdmin has a security vulnerability, the attacker only gets
postfixadminuser, not system-wide access
Traditional setup (single www-data user):
All web apps → www-data user → shared access to everythingOur 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_cSecurity Benefits:
- Application files owned by
postfixadmin, notwww-data - If PHP-FPM is compromised, attacker gets
postfixadminuser, notwww-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-fpmWhy a dedicated pool?
- Runs as
postfixadminuser (notwww-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:
- Point DocumentRoot to PostfixAdmin (instead of /var/www/html)
- 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 apache2Key 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
postfixadminuser, notwww-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.phpThis 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:
- 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. - Fully scriptable – Can be automated in deployment scripts or Ansible playbooks
- More secure – Requires SSH access instead of web access
- No web exposure – The setup interface isn’t accessible via browser
- Faster – Direct command execution, no clicking through web forms
- Professional – Production environments rarely use web-based setup wizards
- 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 configOur 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 1Important:
- Replace
YourStrongPasswordHerewith a strong password and save it securely! - Note:
postfixadmin-cliis a shell script, so run it directly withoutphpprefix
This command will:
- Create the superadmin account in the existing database tables
- Grant full privileges (superadmin = 1)
- Activate the account immediately
- 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.phpSecurity 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.comShows 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 1Add a regular (non-super) admin for a specific domain:
scripts/postfixadmin-cli admin add domainadmin@example.com \
--password 'StrongPassword123' \
--password2 'StrongPassword123' \
--superadmin 0 \
--active 1Then 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.comTip: 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
postfixadminsystem user - ✅ PHP-FPM pool running as
postfixadmin(notwww-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
- Go to Domain List
- Click New Domain
- Enter your domain (e.g.,
example.com) - Set the following:
- Aliases: 10
- Mailboxes: 10
- Max Quota (MB): 10240 (10 GB – maximum quota per mailbox in this domain)
- 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
- Go to Virtual List
- Select your domain from the dropdown
- Click Add Mailbox
- 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)
- Username:
- 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:
- Log in to PostfixAdmin
- Go to Profile or My Account
- Look for TOTP Two-Factor Authentication
- Scan the QR code with an authenticator app (Google Authenticator, Authy, etc.)
- Enter the verification code
- 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.cfShould return:
- Domain lookup:
example.com - Mailbox lookup:
example.com/test/(the maildir path)
10.2: Test Dovecot Authentication
doveadm auth test test@example.comEnter the password when prompted. Should show:
passdb: test@example.com auth succeeded
extra fields:
user=test@example.com10.3: Test SMTP Connection
telnet localhost 25Type:
EHLO test
QUITShould show Postfix responding with capabilities including:
250-STARTTLS250-AUTH PLAIN LOGIN250-SIZE 52428800
10.4: Test IMAPS Connection
openssl s_client -connect localhost:993 -crlfShould connect successfully. Type:
a1 LOGIN test@example.com your_password
a2 LIST "" "*"
a3 LOGOUTShould 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 50Should 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 -crlfLogin and check inbox:
a1 LOGIN test@example.com your_password
a2 SELECT INBOX
a3 FETCH 1 BODY[]
a4 LOGOUTShould display the test email content.
10.7: Test Submission Port (587)
openssl s_client -connect localhost:587 -starttls smtp -crlfShould 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 configSecurity 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
| File | Purpose | Key Settings |
|---|---|---|
/root/mail-server-vars.sh | Central configuration | All domains, paths, credentials |
/etc/postfix/main.cf | Postfix main config | Virtual domains, TLS, SASL |
/etc/postfix/master.cf | Postfix services | Submission (587), SMTPS (465) |
/etc/postfix/sql/*.cf | MySQL lookups | Domain, mailbox, alias queries |
/etc/dovecot/dovecot.conf | Dovecot main | Version declarations |
/etc/dovecot/conf.d/10-mail.conf | Mail storage | Maildir location (2.4 syntax) |
/etc/dovecot/conf.d/10-ssl.conf | Dovecot SSL | Certificate paths (2.4 names) |
/etc/dovecot/conf.d/auth-sql.conf.ext | Authentication | Inline SQL config (2.4 syntax) |
/var/www/postfixadmin/config.local.php | PostfixAdmin | Database, 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
