--- name: mail-server-deployment description: "Deploy Postfix+Dovecot+DKIM mail server with Let's Encrypt." tags: [mail, postfix, dovecot, dkim, letsencrypt, smtp, imap, devops] --- # Mail Server Deployment ## Overview Deploy a complete mail server stack on Ubuntu 22.04: - Postfix (SMTP/SMTPS/Submission) - Dovecot (IMAPS) - OpenDKIM (email signing) - Let's Encrypt (SSL certificates) - SASL authentication (system users) ## Prerequisites - Ubuntu 22.04 server with root/sudo access - Domain with DNS managed externally (e.g., Aliyun DNS) - Cloud firewall (security group) capable of opening ports - Port 25 typically blocked by cloud providers — requires support ticket to unblock ## Step 1: DNS Records Create these five DNS records before starting: | Type | Host | Value | |------|------|-------| | MX | @ | `1 mail.` | | A | mail | `` | | TXT | @ | `v=spf1 mx ~all` | | TXT | _dmarc | `v=DMARC1; p=none; rua=mailto:admin@` | DKIM record is generated later (Step 5). ## Step 2: Install Packages ```bash sudo DEBIAN_FRONTEND=noninteractive apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ postfix dovecot-core dovecot-imapd dovecot-pop3d \ opendkim opendkim-tools ``` ## Step 3: Postfix Configuration See `references/postfix-config.md` for the complete `main.cf` and `master.cf` setup. Key settings: ```bash postconf -e "myhostname = mail." postconf -e "mydomain = " postconf -e "mydestination = localhost, mail., " postconf -e "home_mailbox = Maildir/" 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 "transport_maps = hash:/etc/postfix/transport" ``` Enable SMTPS (465) and Submission (587) by uncommenting the `submission` and `smtps` sections in `/etc/postfix/master.cf`. ## Step 4: Dovecot Configuration See `references/dovecot-config.md` for the complete configuration. Critical settings: - `mail_location = maildir:~/Maildir` - SASL auth socket at `/var/spool/postfix/private/auth` (mode 0660, user postfix) - IMAP on port 0 (disabled), IMAPS on port 993 - POP3/POP3S disabled entirely ## Step 5: Let's Encrypt SSL ```bash sudo certbot certonly --webroot -w /var/www/html \ -d mail. --non-interactive --agree-tos \ --email admin@ ``` Apply cert to postfix: ```bash postconf -e "smtpd_tls_cert_file = /etc/letsencrypt/live/mail./fullchain.pem" postconf -e "smtpd_tls_key_file = /etc/letsencrypt/live/mail./privkey.pem" ``` Apply cert to dovecot — see `references/dovecot-config.md`. Verify TLS on all ports: ```bash echo Q | openssl s_client -starttls smtp -connect localhost:25 | grep "Verify return code" echo Q | openssl s_client -connect localhost:465 | grep "Verify return code" echo Q | openssl s_client -connect localhost:993 | grep "Verify return code" ``` All should show `Verify return code: 0 (ok)`. ## Step 6: DKIM (OpenDKIM) Generate key: ```bash mkdir -p /etc/opendkim/keys/ opendkim-genkey -s default -d -D /etc/opendkim/keys// chown root:root /etc/opendkim/keys//default.private chmod 600 /etc/opendkim/keys//default.private ``` Configure `/etc/opendkim.conf`: ``` Domain Selector default KeyFile /etc/opendkim/keys//default.private Socket inet:8891@localhost Mode sv Canonicalization relaxed/simple ``` Create `/etc/opendkim/TrustedHosts`: ``` 127.0.0.1 ::1 localhost mail. ``` Fix PID directory: ```bash mkdir -p /var/run/opendkim chown opendkim:opendkim /var/run/opendkim ``` Connect to postfix: ```bash 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" ``` ### DKIM DNS Record Extract the public key from `/etc/opendkim/keys//default.txt` and add as a TXT record: | Type | Host | Value | |------|------|-------| | TXT | `default._domainkey` | `v=DKIM1; h=sha256; k=rsa; p=` | Verify: `dig +short TXT default._domainkey.` ## Step 7: Firewall (Cloud Security Group) Open these TCP ports in the cloud security group (e.g., Aliyun): | Port | Protocol | Purpose | |------|----------|---------| | 25 | TCP | SMTP (external receive) | | 465 | TCP | SMTPS (SSL send) | | 587 | TCP | Submission (TLS submit) | | 993 | TCP | IMAPS (SSL receive) | | 80 | TCP | Certbot renewal | Port 25 is usually blocked by cloud providers — submit a support ticket to unblock. ## Step 8: Test ### Local delivery test ```bash echo "Test" | mail -s "Test subject" @localhost ls ~/Maildir/new/ ``` ### DKIM signing test (via SMTP — this is the path that triggers DKIM) ```bash echo -e "EHLO mail.\nMAIL FROM:<@>\nRCPT TO:<@localhost>\nDATA\nSubject: DKIM test\nFrom: @\n\nTest body\n.\nQUIT" \ | timeout 10 openssl s_client -connect localhost:465 -quiet 2>/dev/null ``` Check the delivered email for `DKIM-Signature:` header. **Important:** DKIM signing only works through the SMTP path (ports 25/465/587). The `mail` command and `sendmail` use the `non_smtpd_milters` path which may not trigger signing for all configurations. Always test DKIM via SMTP. ### Queue check ```bash mailq # Should be empty ``` ## Pitfalls 1. **Port 25 blocked by cloud provider**: Mail to `user@domain` will queue and fail with "Connection timed out" because postfix tries to deliver via public IP on port 25. Fix: add `transport_maps` entry `domain local:` to force local delivery, AND open port 25 on the security group. 2. **DKIM signing only via SMTP**: The `mail` command and `sendmail` go through `non_smtpd_milters` which may or may not trigger DKIM signing depending on configuration. Always test DKIM via SMTP submission (port 465 or 587). 3. **OpenDKIM PID file**: On Ubuntu 22.04, opendkim may fail with "Can't open PID file /run/opendkim/opendkim.pid". Fix: `mkdir -p /var/run/opendkim && chown opendkim:opendkim /var/run/opendkim` and add `PidFile /var/run/opendkim/opendkim.pid` to opendkim.conf. 4. **OpenDKIM key ownership**: opendkim runs as root (uid 0) on Ubuntu 22.04 by default. If the key file is owned by `opendkim` user, opendkim will reject it. Set key ownership to `root:root`. 5. **Shell quoting breaks postconf**: `postconf -e "key = value"` fails through SSH because the `=` gets consumed. Use `postconf -e key=value` (no spaces around `=`) or the base64-encoding pattern (see `references/shell-quoting-workaround.md` under socks-proxy-download skill). 6. **Let's Encrypt DNS requirement**: Certbot webroot validation requires the domain to resolve publicly. If DNS is misconfigured, certbot will fail with "SERVFAIL". Run `--dry-run` first to verify. 7. **mydestination must include the domain**: Without `` in `mydestination`, postfix will try to deliver `user@domain` externally via SMTP (which fails if port 25 is blocked). Add a `transport_maps` entry as backup: `echo " local:" > /etc/postfix/transport && postmap /etc/postfix/transport`. ## References | File | Content | |------|---------| | `references/postfix-config.md` | Complete postfix main.cf and master.cf | | `references/dovecot-config.md` | Complete dovecot configuration files | | `references/dns-records.md` | DNS record reference with examples |