| | |

Mail Server Preparation: Essential Setup Before Installing Postfix

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

Introduction

This is the first article in a comprehensive series on building a production-ready mail server from scratch. If you’re wondering why you should self-host email or why we’re not using all-in-one solutions like Mailcow or iRedMail, please read the series introduction first.

This guide covers the essential preparation steps you need to complete before installing Postfix and Dovecot. Proper groundwork prevents common issues and ensures your mail server has a solid foundation.

What this guide covers:

  1. Server naming conventions and hostname setup
  2. Configuration variables file for the entire series
  3. Timezone and time synchronization
  4. Unbound DNS resolver installation
  5. SSL certificate setup with acme.sh

For detailed explanations about Unbound DNS configuration, see: Setting Up Unbound Recursive DNS Resolver on Debian 13

Step 1: Server Naming and Hostname Setup

Why Proper Naming Matters

Mail servers require careful naming because:

  • MX records identify your mail servers
  • Users connect to user-friendly names like mail.example.com
  • SSL certificates must match all service names
  • Proper PTR (reverse DNS) records prevent spam classification

Recommended Naming Convention

Server hostnames: mx1.example.com, mx2.example.com (technical infrastructure)
User-facing services: mail.example.com (CNAME to mx1)

Note: This tutorial series covers setting up mx1 only (primary mail server). The mx2 naming is shown for reference as best practice for future expansion.

This approach gives you:

  • Clear technical naming (mx1/mx2)
  • User-friendly access (mail.example.com)
  • Easy scalability (add mx3 later without renaming)
  • Professional appearance

Option 1: Using CNAMEs (Recommended for flexibility)

# MX Records (must point to A/AAAA records, never CNAMEs)
example.com.     MX  10 mx1.example.com.
example.com.     MX  20 mx2.example.com.

# A Records (actual servers)
mx1.example.com. A   YOUR_IPv4_ADDRESS

# AAAA Records (if using IPv6)
mx1.example.com. AAAA YOUR_IPv6_ADDRESS

# CNAMEs (user-facing services point to actual server)
mail.example.com.    CNAME mx1.example.com.
imap.example.com.    CNAME mx1.example.com.
smtp.example.com.    CNAME mx1.example.com.

Note about mx2: The MX record for mx2.example.com (backup mail server) is shown above as best practice, but this series only covers setting up mx1.example.com (primary mail server). Setting up a backup/secondary mail server is beyond the scope of this tutorial series. For now, you can either:

  • Configure only the mx1 MX record and omit mx2 entirely
  • Keep the mx2 MX record in DNS (it will be ignored if the server doesn’t exist)
  • Set up mx2 later as an advanced topic

Benefits:

  • ✅ Change server IP once (mx1.example.com), all services follow automatically
  • ✅ Easy to move services between servers (just update CNAME target)
  • ✅ Clear separation between technical infrastructure (mx1) and user-facing names (mail/imap/smtp)
  • ✅ Simpler certificate management with fewer SANs needed

Option 2: Using Direct A/AAAA Records (Alternative)

# MX Records
example.com.     MX  10 mx1.example.com.
example.com.     MX  20 mx2.example.com.

# A Records for all names
mx1.example.com.  A   YOUR_IPv4_ADDRESS
mail.example.com. A   YOUR_IPv4_ADDRESS
imap.example.com. A   YOUR_IPv4_ADDRESS
smtp.example.com. A   YOUR_IPv4_ADDRESS

# AAAA Records (if using IPv6)
mx1.example.com.  AAAA YOUR_IPv6_ADDRESS
mail.example.com. AAAA YOUR_IPv6_ADDRESS
imap.example.com. AAAA YOUR_IPv6_ADDRESS
smtp.example.com. AAAA YOUR_IPv6_ADDRESS

Note about mx2: Same as Option 1 – the mx2 MX record is shown for completeness, but not configured in this series. See Option 1 note above for details.

Benefits:

  • ✅ One fewer DNS lookup (direct A/AAAA instead of CNAME → A/AAAA)
  • ✅ Preferred by some mail administrators for “purity”
  • ✅ Slightly better compatibility with very old mail servers
  • ❌ Must update 4 records when changing IP addresses
  • ❌ More records to maintain

The CNAME Debate:

Some mail administrators avoid CNAMEs for mail services, citing:

  • Extra DNS lookup overhead (minimal in practice)
  • Preference for “direct” records
  • Historical issues with older mail servers (rare today)

However, CNAMEs are:

  • ✅ Widely used in production mail systems
  • ✅ Fully supported by modern mail clients and servers
  • ✅ Easier to manage at scale
  • ✅ Standard practice at many large organizations

Important: MX records must NEVER point to CNAMEs; they must point to A/AAAA records. In both options above, the MX record correctly points to mx1.example.com which has direct A/AAAA records.

Recommendation: Use Option 1 (CNAMEs) unless you have a specific reason to use direct A/AAAA records. Both work perfectly fine; this is a matter of preference and management style.

Set the Hostname

Set your server’s hostname to match your primary MX record:

hostnamectl set-hostname mx1.example.com

Verify:

hostname -f

Should return: mx1.example.com

Update /etc/hosts

Edit /etc/hosts:

vi /etc/hosts

Add or modify these lines (replace with your actual IP addresses):

127.0.0.1       localhost
YOUR_IPv4       mx1.example.com mx1
YOUR_IPv6       mx1.example.com mx1

# The following lines are desirable for IPv6 capable hosts
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

Important: Use your container’s private/local IP address (e.g., 10.0.0.50 for Incus bridged network), NOT your public IP. The public IP is configured in DNS records only.

Create Configuration Variables File

Now that you understand the naming convention, create a central configuration file that will be used throughout this series. This file stores all your domain-specific variables in one place.

Create /root/mail-server-vars.sh

cat > /root/mail-server-vars.sh << 'EOF'
#!/bin/bash
# /root/mail-server-vars.sh
# Mail Server Configuration Variables

#=============================================================================
# EDIT THESE VALUES FOR YOUR DOMAIN
#=============================================================================
export DOMAIN="example.com"              # Replace with your actual domain
export ACME_EMAIL="admin@example.com"    # For Let's Encrypt notifications

#=============================================================================
# Derived Hostnames (automatically generated from DOMAIN)
#=============================================================================
export MX1_FQDN="mx1.${DOMAIN}"
export MAIL_FQDN="mail.${DOMAIN}"
export IMAP_FQDN="imap.${DOMAIN}"
export SMTP_FQDN="smtp.${DOMAIN}"

#=============================================================================
# Database Configuration (passwords added in Part 2)
#=============================================================================
export DB_NAME="postfixadmin"
export DB_USER="postfixadmin"
export DB_PASSWORD=""  # Will be generated in Part 2

#=============================================================================
# Virtual Mail User
#=============================================================================
export VMAIL_UID=5000
export VMAIL_GID=5000
export VMAIL_HOME="/var/vmail"

#=============================================================================
# SSL/TLS Paths (acme.sh with DNS challenge)
#=============================================================================
export SSL_CERT="/etc/ssl/acme/${MX1_FQDN}.cert"
export SSL_KEY="/etc/ssl/acme/${MX1_FQDN}.key"
export SSL_FULLCHAIN="/etc/ssl/acme/${MX1_FQDN}.fullchain"
EOF

chmod 700 /root/mail-server-vars.sh

Edit the file to set your actual domain:

vi /root/mail-server-vars.sh

Change these two lines:

  • export DOMAIN="example.com" → Your actual domain
  • export ACME_EMAIL="admin@example.com" → Your actual email address

Save and exit (Ctrl+X, Y, Enter).

Test that it works:

source /root/mail-server-vars.sh
echo "MX1 hostname: $MX1_FQDN"
echo "Mail hostname: $MAIL_FQDN"

Should display your configured hostnames with your domain.

Why this file is important:

  • Single place to define your domain
  • All hostnames are derived automatically
  • Used throughout the tutorial series
  • Easy to update if anything changes
  • Future scripts can source this file

Step 2: Timezone and Time Configuration

Why Accurate Time Matters

Mail servers require accurate time for:

  • Email timestamp headers
  • TLS/SSL certificate validation
  • Log file accuracy
  • DKIM signature timestamps
  • SPF record validation

Set Your Timezone

List available timezones:

timedatectl list-timezones

Set your timezone (example for Brussels/Belgium):

timedatectl set-timezone Europe/Brussels

Verify:

timedatectl

Time Synchronization: Container vs Bare Metal

For Containers (Incus/LXC):

Containers inherit time from the host system. You don’t need to configure NTP inside the container.

Verify time is correct:

date
timedatectl status

The output should show:

System clock synchronized: yes

Even without an NTP service running in the container, time sync comes from the host.

For Bare Metal Servers:

Check which NTP service is available:

systemctl status systemd-timesyncd 2>/dev/null || \
systemctl status chronyd 2>/dev/null || \
systemctl status chrony 2>/dev/null || \
echo "No NTP service found - install one"

If you need to install NTP:

# Option 1: systemd-timesyncd (lightweight, good for most servers)
apt install systemd-timesyncd
systemctl enable systemd-timesyncd
systemctl start systemd-timesyncd

# Option 2: chrony (more accurate, better for complex scenarios)
apt install chrony
systemctl enable chrony
systemctl start chrony

Enable NTP synchronization:

timedatectl set-ntp true

Verify synchronization status:

timedatectl timesync-status  # for systemd-timesyncd
# or
chronyc tracking              # for chrony

Step 3: Unbound DNS Resolver Setup

Why Unbound is Essential

Mail servers using spam filtering with DNS-based Blackhole Lists (DNSBLs) generate thousands of DNS queries per hour. Public DNS services like Google (8.8.8.8) or Cloudflare (1.1.1.1) will rate-limit or block your queries, breaking spam filtering.

Unbound provides:

  • Unlimited DNS queries (no rate limiting)
  • Direct recursive resolution to authoritative nameservers
  • DNSSEC validation for security
  • Local caching for performance

3.1: Install Packages

apt update
apt install unbound dnsutils curl -y

3.2: Download Root Hints

curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
chown unbound:unbound /var/lib/unbound/root.hints

3.3: Configure Unbound

Create /etc/unbound/unbound.conf.d/local.conf:

cat > /etc/unbound/unbound.conf.d/local.conf << 'EOF'
server:
    module-config: "validator iterator"
    
    # Listen on localhost only
    interface: 127.0.0.1
    interface: ::1
    port: 53
    
    # Override IPv6 default to prevent Spamhaus DNSBL false positives
    # Spamhaus RBL queries over IPv6 can trigger "open resolver" errors
    do-ip6: no
    
    # Access control - only allow localhost queries
    access-control: 127.0.0.0/8 allow
    access-control: ::1 allow
    access-control: 0.0.0.0/0 refuse
    access-control: ::/0 refuse
    
    root-hints: "/var/lib/unbound/root.hints"
    auto-trust-anchor-file: "/var/lib/unbound/root.key"
    
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: yes
    
    cache-min-ttl: 3600
    cache-max-ttl: 86400
    prefetch: yes
    num-threads: 2
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4
    rrset-cache-size: 100m
    msg-cache-size: 50m
    so-rcvbuf: 1m
EOF

Important: The do-ip6: no setting is critical for mail servers. Spamhaus and other blacklists have difficulty processing IPv6 queries and may classify your server as an open resolver, leading to blacklisting.

Remove duplicate configuration if present:

rm -f /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf

3.4: Create DAEMON_OPTS File

cat > /etc/default/unbound << 'EOF'
DAEMON_OPTS=""
EOF

3.5: Disable systemd-resolved

Critical for mail servers: systemd-resolved conflicts with Postfix’s chrooted environment.

systemctl stop systemd-resolved
systemctl disable systemd-resolved
systemctl mask systemd-resolved
rm -f /etc/resolv.conf

cat > /etc/resolv.conf << 'EOF'
nameserver 127.0.0.1
EOF

chmod 644 /etc/resolv.conf

3.6: Start Unbound

unbound-checkconf
systemctl enable unbound
systemctl start unbound

3.7: Verification

Test basic resolution:

dig @127.0.0.1 google.com

Test DNSSEC validation:

dig @127.0.0.1 sigok.verteiltesysteme.net +dnssec | grep "ad;"

Should show the ad (authenticated data) flag.

Test system resolution:

dig google.com

Should resolve through Unbound (127.0.0.1).

3.8: Setup Automatic Updates

Root Hints Update (Every 6 Months)

Create systemd service:

cat > /etc/systemd/system/unbound-roothints-update.service << 'EOF'
[Unit]
Description=Update Unbound Root Hints
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
ExecStart=/bin/chown unbound:unbound /var/lib/unbound/root.hints
ExecStartPost=/bin/systemctl reload unbound
EOF

Create systemd timer:

cat > /etc/systemd/system/unbound-roothints-update.timer << 'EOF'
[Unit]
Description=Update Unbound Root Hints Every 6 Months

[Timer]
OnCalendar=*-01,07-01 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

Root Key Update (Monthly)

Create systemd service:

cat > /etc/systemd/system/unbound-rootkey-update.service << 'EOF'
[Unit]
Description=Update Unbound Root Trust Anchor
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/unbound-anchor -a /var/lib/unbound/root.key
ExecStartPost=/bin/systemctl reload unbound
EOF

Create systemd timer:

cat > /etc/systemd/system/unbound-rootkey-update.timer << 'EOF'
[Unit]
Description=Update Unbound Root Trust Anchor Monthly

[Timer]
OnCalendar=monthly
Persistent=true

[Install]
WantedBy=timers.target
EOF

Enable All Timers

systemctl daemon-reload
systemctl enable unbound-roothints-update.timer
systemctl start unbound-roothints-update.timer
systemctl enable unbound-rootkey-update.timer
systemctl start unbound-rootkey-update.timer

Verify timers are active:

systemctl list-timers | grep unbound

Verify PTR Records (Reverse DNS)

Now that Unbound is operational, verify your PTR (reverse DNS) records. This is critical for mail server reputation.

Important: Your hosting provider must configure PTR records. Test them by looking up your public IPs from DNS and checking their reverse records:

# Get FQDN from system
FQDN=$(hostname -f)
echo "Testing PTR for: $FQDN"

# Get public IPv4 from DNS (now using Unbound)
IP4=$(dig +short A $FQDN | head -1)
echo "Your public IPv4: $IP4"
dig +short -x $IP4

Should return: mx1.example.com.

# Get public IPv6 from DNS (if configured)
IP6=$(dig +short AAAA $FQDN | head -1)
if [ -n "$IP6" ]; then
  echo "Your public IPv6: $IP6"
  dig +short -x $IP6
else
  echo "No IPv6 configured"
fi

Should also return: mx1.example.com. (if IPv6 is configured).

If PTR records don’t match your hostname: Contact your hosting provider to fix them. Incorrect PTR records will cause your emails to be rejected or marked as spam.

Note: This test requires Unbound to be working, which is why we perform it here after DNS resolver setup is complete.

Preparation Complete!

Your server is now properly prepared for Postfix installation:

  • Hostname configured as mx1.example.com
  • The configuration variables file was created at /root/mail-server-vars.sh
  • PTR records verified for both IPv4 and IPv6
  • Timezone set with time synchronization verified
  • Unbound DNS resolver operational on port 53
  • systemd-resolved disabled (prevents Postfix chroot conflicts)
  • Automatic DNS updates configured via systemd timers
  • SSL certificate obtained covering all service names (SAN)

Important: Throughout the rest of this series, you’ll use the variables from /root/mail-server-vars.sh. Always source this file at the beginning of each configuration session:

source /root/mail-server-vars.sh

This ensures all your domain-specific settings are available for use in commands and configuration files.

Step 4: SSL Certificate Setup

Before installing the mail server components, obtain an SSL certificate that covers all your service names. We’ll use acme.sh with the DNS challenge method since we’re not running a web server on ports 80/443 yet.

Install acme.sh

# install required packages
apt install cron socat -y

source /root/mail-server-vars.sh

curl https://get.acme.sh | sh
source ~/.bashrc

Register with Let’s Encrypt

# Register account
acme.sh --register-account -m $ACME_EMAIL

# Set Let's Encrypt as default CA
acme.sh --set-default-ca --server letsencrypt

Issue SAN Certificate Using DNS Challenge

Request a certificate covering all service names:

acme.sh --issue --dns \
  -d $MX1_FQDN \
  -d $MAIL_FQDN \
  -d $IMAP_FQDN \
  -d $SMTP_FQDN \
  --yes-I-know-dns-manual-mode-enough-go-ahead-please

The output will show TXT records you need to add:

Add the following TXT record:
Domain: '_acme-challenge.mx1.example.com'
TXT value: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

Domain: '_acme-challenge.mail.example.com'
TXT value: 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'

Domain: '_acme-challenge.imap.example.com'
TXT value: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'

Domain: '_acme-challenge.smtp.example.com'
TXT value: 'wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww'

Add DNS TXT Records

Add all four TXT records to your DNS zone:

_acme-challenge.mx1.example.com.   IN  TXT  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
_acme-challenge.mail.example.com.  IN  TXT  "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
_acme-challenge.imap.example.com.  IN  TXT  "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
_acme-challenge.smtp.example.com.  IN  TXT  "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"

Wait for DNS propagation (typically 1-5 minutes):

dig +short TXT _acme-challenge.$MX1_FQDN
dig +short TXT _acme-challenge.$MAIL_FQDN
dig +short TXT _acme-challenge.$IMAP_FQDN
dig +short TXT _acme-challenge.$SMTP_FQDN

Once you see all TXT records, continue.

Complete Certificate Issuance

acme.sh --renew \
  -d $MX1_FQDN \
  -d $MAIL_FQDN \
  -d $IMAP_FQDN \
  -d $SMTP_FQDN \
  --yes-I-know-dns-manual-mode-enough-go-ahead-please

Install Certificate

# Create certificate directory
mkdir -p /etc/ssl/acme

# Install certificate (services will be configured to use these paths)
acme.sh --install-cert -d $MX1_FQDN \
  --cert-file /etc/ssl/acme/${MX1_FQDN}.cert \
  --key-file /etc/ssl/acme/${MX1_FQDN}.key \
  --fullchain-file /etc/ssl/acme/${MX1_FQDN}.fullchain

Note: We use $MAIL_FQDN as the primary domain for the certificate filename. The certificate covers all four domains (SAN), but we reference it by the main mail hostname.

Set Permissions

chmod 644 /etc/ssl/acme/${MX1_FQDN}.cert
chmod 644 /etc/ssl/acme/${MX1_FQDN}.fullchain
chmod 600 /etc/ssl/acme/${MX1_FQDN}.key
chown root:root /etc/ssl/acme/${MX1_FQDN}.*

Verify Certificate

openssl x509 -in /etc/ssl/acme/${MX1_FQDN}.fullchain -noout -subject -dates -text | grep "DNS:"

Should show all four domains:

DNS:mx1.example.com, DNS:mail.example.com, DNS:imap.example.com, DNS:smtp.example.com

Certificate Coverage:

  • mx1.example.com – Server hostname
  • mail.example.com – User-facing mail service
  • imap.example.com – IMAP access
  • smtp.example.com – SMTP submission

Automatic Renewal: acme.sh automatically installs a cron job for certificate renewal. In Part 2, we’ll configure the reload commands so Postfix and Dovecot automatically reload when certificates are renewed.

Next Steps

You’re now ready to proceed with Part 2: Core Mail Server Setup, where we’ll install and configure MariaDB, PostfixAdmin, Postfix, and Dovecot 2.4 to create a complete working mail server.

Return to: Mail Server Series Index

Similar Posts