6.6 KiB

name description version author license metadata
mail-server-setup Set up Postfix+Dovecot+DKIM mail server on Ubuntu. 1.0.0 Hermes Agent MIT
hermes
tags
mail
postfix
dovecot
dkim
smtp
imap
letsencrypt
email

Mail Server Setup

Complete mail server on Ubuntu: Postfix (SMTP/SMTPS/Submission), Dovecot (IMAPS), OpenDKIM signing, and Let's Encrypt TLS certificate.

Prerequisites

  • Ubuntu 22.04+ with root/sudo access
  • DNS managed externally (Aliyun DNS, Cloudflare, etc.)
  • Port 25 may need manual unblock from cloud provider (Aliyun requires ticket)
  • A domain with MX record pointing to the server

DNS Records Required

Before starting, ensure these DNS records exist:

Type Host Value Priority
A mail.domain.com <server_ip>
MX @ mail.domain.com 1
TXT @ v=spf1 mx ~all
TXT _dmarc v=DMARC1; p=none; rua=mailto:admin@domain.com
TXT default._domainkey (generated below)

Cloud Firewall Ports

Open these TCP ports in the cloud security group:

Port Purpose
25 SMTP (requires manual unblock ticket on Aliyun)
465 SMTPS (SSL-encrypted submission)
587 Submission (STARTTLS)
993 IMAPS (SSL-encrypted retrieval)
80 HTTP (certbot renewal)

No local firewall needed if UFW is inactive (default on many Ubuntu images).

Install Packages

sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
  postfix dovecot-core dovecot-imapd opendkim opendkim-tools

Debconf preseed avoids interactive postfix configuration prompts.

Configure Postfix

postconf -e myhostname=mail.domain.com
postconf -e mydomain=domain.com
postconf -e myorigin=/etc/mailname
postconf -e mydestination=localhost
postconf -e mynetworks=127.0.0.0/8 [::1]/128
postconf -e home_mailbox=Maildir/
postconf -e virtual_alias_maps=hash:/etc/postfix/virtual
postconf -e smtpd_tls_security_level=may
postconf -e smtp_tls_security_level=may
postconf -e smtpd_sasl_type=dovecot
postconf -e smtpd_sasl_path=private/auth
postconf -e smtpd_sasl_auth_enable=yes
postconf -e smtpd_tls_auth_only=yes
postconf -e smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination

CRITICAL: use key=value syntax (no spaces around =) to avoid shell quoting issues.

Enable submission (587) and smtps (465) in master.cf:

sed -i 's/^#submission/submission/' /etc/postfix/master.cf
sed -i 's/^#smtps/smtps/' /etc/postfix/master.cf
# Also uncomment the -o lines under each

Set up virtual alias and Maildir:

echo "domain.com" > /etc/mailname
echo "admin@domain.com username" > /etc/postfix/virtual
postmap /etc/postfix/virtual
mkdir -p ~/Maildir/{cur,new,tmp} && chmod -R 700 ~/Maildir

Configure Dovecot

Mail location → Maildir:

sed -i 's|^mail_location = .*|mail_location = maildir:~/Maildir|' /etc/dovecot/conf.d/10-mail.conf

Auth config (SASL for Postfix):

cat > /etc/dovecot/conf.d/10-auth.conf << 'EOF'
disable_plaintext_auth = yes
auth_mechanisms = plain login
!include auth-system.conf.ext
EOF

Master config (IMAPS only, SASL socket):

cat > /etc/dovecot/conf.d/10-master.conf << 'EOF'
service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
  unix_listener auth-userdb {
    mode = 0600
    user = USERNAME
  }
}
service imap-login {
  inet_listener imap { port = 0 }
  inet_listener imaps { port = 993; ssl = yes }
}
service pop3-login {
  inet_listener pop3 { port = 0 }
  inet_listener pop3s { port = 0 }
}
EOF

SSL (initially snakeoil):

cat > /etc/dovecot/conf.d/10-ssl.conf << 'EOF'
ssl = yes
ssl_cert = </etc/dovecot/private/dovecot.pem
ssl_key = </etc/dovecot/private/dovecot.key
ssl_min_protocol = TLSv1.2
EOF

Let's Encrypt Certificate

sudo certbot certonly --webroot -w /var/www/html \
  -d mail.domain.com --non-interactive --agree-tos \
  --email admin@domain.com

Apply to Postfix:

CERT=/etc/letsencrypt/live/mail.domain.com/fullchain.pem
KEY=/etc/letsencrypt/live/mail.domain.com/privkey.pem
postconf -e smtpd_tls_cert_file=$CERT
postconf -e smtpd_tls_key_file=$KEY
postconf -e smtp_tls_cert_file=$CERT
postconf -e smtp_tls_key_file=$KEY
postconf -e smtpd_tls_protocols='!SSLv2, !SSLv3, !TLSv1, !TLSv1.1'

Apply to Dovecot:

cat > /etc/dovecot/conf.d/10-ssl.conf << EOF
ssl = yes
ssl_cert = <$CERT
ssl_key = <$KEY
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
EOF

Configure DKIM (OpenDKIM)

Generate keys:

mkdir -p /etc/opendkim/keys/domain.com && cd /etc/opendkim/keys/domain.com
opendkim-genkey -s default -d domain.com
chown root:root default.private && chmod 600 default.private

OpenDKIM config:

cat > /etc/opendkim.conf << 'EOF'
Syslog yes
UMask 002
Domain domain.com
KeyFile /etc/opendkim/keys/domain.com/default.private
Selector default
Mode sv
Canonicalization relaxed/simple
Socket inet:8891@localhost
EOF

Add milter to Postfix and restart:

postconf -e milter_default_action=accept
postconf -e milter_protocol=6
postconf -e smtpd_milters=inet:localhost:8891
postconf -e non_smtpd_milters=inet:localhost:8891
systemctl restart opendkim postfix dovecot

Get the DKIM DNS record:

cat /etc/opendkim/keys/domain.com/default.txt

Verification

# Check ports
sudo ss -tlnp | grep -E '25|465|587|993'

# Test TLS (should show "Verify return code: 0 (ok)")
echo "QUIT" | openssl s_client -starttls smtp -connect localhost:25 2>&1 | grep "Verify return code"
echo "Q" | openssl s_client -connect localhost:993 2>&1 | grep "Verify return code"

# Test local delivery
echo "TEST" | mail -s "Test" username@localhost
ls ~/Maildir/new/

Pitfalls

  1. SSH quoting breaks config commands: When running through SSH, use base64 encoding to transmit complex scripts. See socks-proxy-download skill → shell-quoting-workaround.md.
  2. postconf whitespace: postconf -e "key = value" fails (shell consumes =). Always use postconf -e key=value (no spaces).
  3. opendkim key ownership: Must match the running uid. If no UserID in config, opendkim runs as root → key must be owned by root. Error: "not owned by executing uid".
  4. Aliyun blocks port 25: Requires manual ticket to unblock.
  5. DNS propagation delay: Let's Encrypt validation may fail for newly added DNS records. Use --dry-run first. Retry after propagation.
  6. Dovecot SASL socket: Must be at /var/spool/postfix/private/auth with mode 0660, group postfix — otherwise postfix can't authenticate users.