Skip to content

Ansible playbook for deploying a complete, secure mail server (Postfix, Dovecot, Rspamd) with SPF/DKIM/DMARC, MTA-STS, and Let's Encrypt

Notifications You must be signed in to change notification settings

calebikhuohon/mail-server

Repository files navigation

Mail Server Automation

This repository contains an Ansible playbook that provisions a complete, production-ready mail platform on Ubuntu 24.04. It follows the Pieter Hollander mail server guide while wrapping the workflow in reusable roles, helpful defaults, and guard rails for modern DNS, TLS, and spam-hardening standards.

For a terminal-friendly reference of the most common commands see CHEATSHEET.md.

Features

Core Mail Services

  • Postfix - SMTP server for sending and receiving email
  • Dovecot - IMAP server with Sieve filtering and ManageSieve support
  • Rspamd - Advanced spam filtering with DKIM signing
  • PostgreSQL - Database for virtual domains, mailboxes, and aliases
  • Redis - Backend for Rspamd learning data

Security & Standards

  • ✅ SPF, DKIM, and DMARC support
  • ✅ DNSSEC validation
  • ✅ DANE/TLSA for SMTP server validation
  • ✅ MTA-STS for secure mail transport
  • ✅ TLS 1.2+ with strong ciphers
  • ✅ Let's Encrypt certificates with auto-renewal
  • ✅ Fail2ban protection against brute force attacks
  • ✅ UFW firewall configuration

Additional Features

  • Self-learning Bayesian spam filter
  • Address tagging (e.g., support+tag@example.com)
  • Email aliases and forwarding
  • Server-side Sieve filtering
  • Automatic mailbox folders (Sent, Drafts, Spam, etc.)
  • Autoconfig for easy client setup
  • Web-based Rspamd admin interface

Requirements

Target Server

  • Ubuntu 24.04 LTS
  • 2 vCPU / 2GB RAM minimum (4GB recommended)
  • 20GB+ disk (depends on mailbox volume)
  • Public IPv4 (IPv6 optional but supported)
  • Root or passwordless sudo access

Local Machine

  • Ansible 2.10 or later
  • SSH access to the target server
  • Python 3.6+ on both local machine and server

DNS Requirements

You should have already configured:

  • A record: mail.example.com → Your server IP
  • MX record: example.commail.example.com (priority 10)

Additional DNS records will be provided after the playbook runs.

Quick Start

Prefer a guided workflow? Run ./setup.sh — it installs collections, checks your configuration, encrypts the vault, and runs the playbook for you. The steps below mirror what the script does so you can run things manually or in CI.

1. Install dependencies

# macOS
brew install ansible

# Ubuntu/Debian
sudo apt update && sudo apt install ansible

First, copy the domain configuration template:

```bash
cp group_vars/all/config.yml.example group_vars/all/config.yml

Edit group_vars/all/config.yml with your domain settings:

mail_domain: "example.com"
mail_hostname: "mail.example.com"

email_accounts:
  - email: "support@example.com"
    name: "Support Team"
  - email: "admin@example.com"
    name: "Admin"

Then edit group_vars/all/vars.yml:

# Update these values
mail_server_ip: "YOUR_SERVER_IP"
mail_server_ipv4: "YOUR_SERVER_IPV4"
mail_server_ipv6: ""  # Leave empty if not using IPv6
mail_server_user: "root"

3. Configure Secrets

Copy the example vault file:

cp group_vars/all/vault.yml.example group_vars/all/vault.yml

Edit group_vars/all/vault.yml and set strong passwords:

postgresql_vmail_password: "STRONG_PASSWORD_HERE"
postgresql_postgres_password: "STRONG_PASSWORD_HERE"

# Generate with: docker run --rm rspamd/rspamd rspamadm pw
# Or on server after install: rspamadm pw
rspamd_controller_password: "$2$..."

email_passwords:
  "support@example.com": "STRONG_PASSWORD_HERE"
  "admin@example.com": "STRONG_PASSWORD_HERE"

rspamd_web_user: "admin"
rspamd_web_password: "STRONG_PASSWORD_HERE"

Encrypt the vault file:

ansible-vault encrypt group_vars/all/vault.yml

4. Update Inventory

Edit inventory.yml so the host name matches your real mail server (replace the placeholder mail.example.com shown in the file comment). The rest of the connection details can stay templated to pull from group_vars/all/vars.yml:

all:
  children:
    mailservers:
      hosts:
        mail.example.com:  # must match your DNS A record
          ansible_host: "{{ mail_server_ip | default(mail_server_ipv4) }}"
          ansible_user: "{{ mail_server_user | default('root') }}"
          ansible_python_interpreter: "{{ ansible_python_interpreter | default('/usr/bin/python3') }}"

5. Run the Playbook

ansible-playbook -i inventory.yml playbook.yml --ask-vault-pass

Enter your vault password when prompted.

Configuration Reference

File Purpose Key values
group_vars/all/config.yml Mailbox + alias definitions mail_domain, mail_hostname, email_accounts, email_aliases, optional additional_domains
group_vars/all/vars.yml Server connectivity + feature toggles mail_server_ip, mail_server_user, rspamd_spam_threshold, fail2ban_whitelist, etc.
group_vars/all/vault.yml Secrets encrypted via Ansible Vault DB passwords, mailbox passwords, DKIM/Rspamd credentials
inventory.yml Host definitions Ensure hostnames line up with DNS records

Whenever you change config.yml or vault.yml, re-run the relevant role(s). Example: mailbox updates only need --tags postgresql.

DNS Checklist

After a successful run the playbook prints the records you need, but the full set is below for planning:

Type Name Value
A mail.example.com Server IPv4
A smtp.example.com Server IPv4
A imap.example.com Server IPv4
A autoconfig.example.com Server IPv4
A mta-sts.example.com Server IPv4
A rspamd.example.com Server IPv4
MX example.com mail.example.com priority 10
TXT example.com "v=spf1 mx ~all"
TXT _dmarc.example.com "v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com"
TXT _mta-sts.example.com "v=STSv1; id=<timestamp>"
TXT default._domainkey.example.com DKIM public key generated by Rspamd
TXT _smtp._tls.example.com (Optional) SMTP TLS reporting
TLSA _25._tcp.mail.example.com 3 1 1 <hash>
TLSA _587._tcp.mail.example.com 3 1 1 <hash>

Generate the TLSA hash on the server:

echo "3 1 1 $(openssl x509 -in /etc/lego/certificates/mail.example.com.crt -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary | xxd -p -u -c 32)"

After Deployment

  • Rspamd UI: https://rspamd.example.com (credentials from the vault)
  • Autoconfig endpoint: https://autoconfig.example.com/mail/config-v1.1.xml
  • MTA-STS policy: https://mta-sts.example.com/.well-known/mta-sts.txt

Email client settings

Protocol Host Port Security Notes
IMAP mail.example.com 993 SSL/TLS Username = full email
IMAP mail.example.com 143 STARTTLS
SMTP submission mail.example.com 587 STARTTLS Auth required
ManageSieve mail.example.com 4190 STARTTLS Use for Sieve rules

Health checks

systemctl status postfix dovecot rspamd redis-server postgresql fail2ban
tail -f /var/log/mail.log
journalctl -u postfix -f
journalctl -u dovecot -f
journalctl -u rspamd -f

Send a real-world test:

echo "Test email body" | mail -s "Test Subject" you@example.net

Ongoing Management

Add a mailbox

# group_vars/all/config.yml
email_accounts:
  - email: "newuser@example.com"
    name: "New User"
# group_vars/all/vault.yml
email_passwords:
  "newuser@example.com": "STRONG_PASSWORD"
ansible-playbook -i inventory.yml playbook.yml --tags postgresql --ask-vault-pass

Add an alias

email_aliases:
  - alias: "sales@example.com"
    destination: "team@example.com"

Re-run with --tags postgresql.

Change a password directly on the server

doveadm pw -s SHA512-CRYPT               # produce hash
sudo -u postgres psql vmail \
  -c "UPDATE accounts SET password='HASH' WHERE email='user@example.com';"

Renew certificates manually

/usr/local/bin/renew-certificates.sh

Train the spam filter

  • Move spam into the "Spam" IMAP folder to learn spam
  • Move false positives out of "Spam" to teach ham

Troubleshooting

Cannot send mail

systemctl status postfix
journalctl -u postfix -n 50
telnet mail.example.com 587
doveadm auth test user@example.com

Verify DNS + reverse DNS, then check the Postfix queue (mailq, postqueue -p).

Cannot receive mail

dig example.com MX
ss -tlnp | grep :25
tail -f /var/log/mail.log

Look for firewall blocks or upstream providers blocking port 25.

Spam filter issues

systemctl status rspamd
rspamc stat
rspamc < sample.eml
journalctl -u rspamd -n 100

Adjust thresholds in group_vars/all/vars.yml (rspamd_spam_threshold) and redeploy rspamd.

Certificate problems

openssl x509 -in /etc/lego/certificates/mail.example.com.crt -text -noout
/usr/local/bin/renew-certificates.sh

Ensure DNS A/AAAA records resolve before requesting certificates.

Database problems

sudo -u postgres psql vmail
\dt
SELECT email, enabled FROM accounts;

Repair permissions:

GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO vmail;

Security & Maintenance

  • Firewall: UFW allows only SSH, SMTP (25/587), IMAP (143/993), LMTP, ManageSieve, HTTPS for ACME endpoints. Layer it with your provider’s security groups.
  • Fail2ban: fail2ban-client status / fail2ban-client status postfix
  • Monitoring: Track disk usage (/var/vmail), certificate expiry, service uptime, and login failures.
  • Backups: Keep copies of /var/vmail, /var/lib/rspamd, /var/lib/redis, /etc/lego, /etc/postfix, /etc/dovecot, and the vmail PostgreSQL DB (pg_dump vmail).
  • Performance: Use htop, ss -s, and mailq to monitor load, sockets, and queues.

Testing & Online Tools

Contributing & Support

For issues with this automation:

Issues and PRs are welcome! If you hit deployment problems:

  • Re-run with ansible-playbook ... -vvv
  • Inspect the generated /tmp/ansible-*.log files
  • Check the service logs listed above

License

This project is provided as-is for operating your own mail server. See the repository license for details.

Acknowledgments

Based on the outstanding research and documentation by Pieter Hollander: https://pieterhollander.nl/post/mailserver/

About

Ansible playbook for deploying a complete, secure mail server (Postfix, Dovecot, Rspamd) with SPF/DKIM/DMARC, MTA-STS, and Let's Encrypt

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published