Introduction
Postfix is the most popular open-source mail transfer agent (MTA) — secure, fast, and easy to configure. Ansible automates the full email infrastructure: install Postfix, configure SMTP relay or full mail server, set up TLS encryption, deploy SPF/DKIM/DMARC for deliverability, manage virtual domains, and integrate spam filtering.
Install Postfix
---
- name: Deploy Postfix mail server
hosts: mail_servers
become: true
vars:
postfix_hostname: mail.example.com
postfix_domain: example.com
postfix_mynetworks:
- 127.0.0.0/8
- 10.0.0.0/8
tasks:
- name: Install Postfix
ansible.builtin.package:
name:
- postfix
- mailutils
- libsasl2-modules
state: present
- name: Deploy main.cf
ansible.builtin.template:
src: main.cf.j2
dest: /etc/postfix/main.cf
mode: '0644'
notify: restart postfix
- name: Allow SMTP through firewall
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
state: enabled
immediate: true
loop: [smtp, smtps, smtp-submission]
- name: Start Postfix
ansible.builtin.service:
name: postfix
state: started
enabled: true
handlers:
- name: restart postfix
ansible.builtin.service:
name: postfix
state: restarted
main.cf Template
# templates/main.cf.j2
# Managed by Ansible
smtpd_banner = $myhostname ESMTP
biff = no
append_dot_mydomain = no
myhostname = {{ postfix_hostname }}
mydomain = {{ postfix_domain }}
myorigin = $mydomain
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
mynetworks = {{ postfix_mynetworks | join(', ') }}
# TLS
smtpd_tls_cert_file = /etc/ssl/certs/{{ postfix_domain }}.crt
smtpd_tls_key_file = /etc/ssl/private/{{ postfix_domain }}.key
smtpd_use_tls = yes
smtpd_tls_security_level = may
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_security_level = may
smtp_tls_loglevel = 1
# SASL Authentication
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
# Restrictions
smtpd_helo_required = yes
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
reject_invalid_hostname,
reject_non_fqdn_hostname,
reject_non_fqdn_sender,
reject_non_fqdn_recipient,
reject_unknown_sender_domain,
reject_rbl_client zen.spamhaus.org
# Size limits
message_size_limit = 52428800
mailbox_size_limit = 0
# Virtual domains
{% if postfix_virtual_domains is defined %}
virtual_mailbox_domains = {{ postfix_virtual_domains | join(', ') }}
virtual_mailbox_maps = hash:/etc/postfix/vmailbox
virtual_alias_maps = hash:/etc/postfix/virtual
virtual_mailbox_base = /var/mail/vhosts
virtual_minimum_uid = 100
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
{% endif %}
SMTP Relay (Satellite System)
For servers that only send mail through a relay:
- name: Configure as SMTP relay client
hosts: all
become: true
vars:
smtp_relay: smtp.example.com
smtp_relay_port: 587
tasks:
- name: Install Postfix
ansible.builtin.package:
name: postfix
state: present
- name: Configure relay
ansible.builtin.copy:
dest: /etc/postfix/main.cf
content: |
myhostname = {{ ansible_fqdn }}
relayhost = [{{ smtp_relay }}]:{{ smtp_relay_port }}
smtp_use_tls = yes
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
inet_interfaces = loopback-only
mynetworks = 127.0.0.0/8
mode: '0644'
notify: restart postfix
- name: Configure relay credentials
ansible.builtin.copy:
dest: /etc/postfix/sasl_passwd
content: "[{{ smtp_relay }}]:{{ smtp_relay_port }} {{ smtp_user }}:{{ vault_smtp_password }}"
mode: '0600'
no_log: true
notify: postmap sasl
handlers:
- name: postmap sasl
ansible.builtin.command: postmap /etc/postfix/sasl_passwd
- name: restart postfix
ansible.builtin.service:
name: postfix
state: restarted
DKIM Signing
- name: Install OpenDKIM
ansible.builtin.package:
name: [opendkim, opendkim-tools]
state: present
- name: Generate DKIM key
ansible.builtin.command: >
opendkim-genkey -s mail -d {{ postfix_domain }} -D /etc/opendkim/keys/{{ postfix_domain }}/
args:
creates: /etc/opendkim/keys/{{ postfix_domain }}/mail.private
- name: Configure OpenDKIM
ansible.builtin.copy:
dest: /etc/opendkim.conf
content: |
Syslog yes
Domain {{ postfix_domain }}
Selector mail
KeyFile /etc/opendkim/keys/{{ postfix_domain }}/mail.private
Socket inet:8891@localhost
Canonicalization relaxed/simple
Mode sv
SubDomains no
mode: '0644'
notify: restart opendkim
- name: Add DKIM milter to Postfix
ansible.builtin.blockinfile:
path: /etc/postfix/main.cf
block: |
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
notify: restart postfix
- name: Display DKIM DNS record
ansible.builtin.command: cat /etc/opendkim/keys/{{ postfix_domain }}/mail.txt
register: dkim_record
changed_when: false
- name: Show DKIM record to add to DNS
ansible.builtin.debug:
var: dkim_record.stdout
Virtual Domains
- name: Create virtual mailbox maps
ansible.builtin.template:
src: vmailbox.j2
dest: /etc/postfix/vmailbox
mode: '0644'
notify: postmap vmailbox
- name: Create virtual alias maps
ansible.builtin.template:
src: virtual.j2
dest: /etc/postfix/virtual
mode: '0644'
notify: postmap virtual
handlers:
- name: postmap vmailbox
ansible.builtin.command: postmap /etc/postfix/vmailbox
- name: postmap virtual
ansible.builtin.command: postmap /etc/postfix/virtual
# templates/vmailbox.j2
{% for user in mail_users %}
{{ user.email }} {{ user.email.split('@')[1] }}/{{ user.email.split('@')[0] }}/
{% endfor %}
# templates/virtual.j2
{% for alias in mail_aliases %}
{{ alias.from }} {{ alias.to }}
{% endfor %}
Monitoring
- name: Check mail queue
ansible.builtin.command: mailq
register: mail_queue
changed_when: false
- name: Alert on large queue
ansible.builtin.debug:
msg: "WARNING: {{ mail_queue.stdout_lines | length }} messages in queue on {{ inventory_hostname }}"
when: mail_queue.stdout_lines | length > 100
- name: Check Postfix status
ansible.builtin.command: postfix status
register: postfix_status
changed_when: false
failed_when: false
Troubleshooting
Test Mail Delivery
- name: Send test email
ansible.builtin.command: >
echo "Test from Ansible on {{ inventory_hostname }}" |
mail -s "Ansible Mail Test" {{ test_email }}
changed_when: true
- name: Check mail logs
ansible.builtin.command: tail -50 /var/log/mail.log
register: mail_logs
changed_when: false
Related Articles
Conclusion
Ansible automates the full Postfix lifecycle — from simple relay clients (one config file pointing to a smarthost) to full mail servers with virtual domains, DKIM signing, TLS, and spam filtering. Template main.cf from variables, deploy DKIM keys, configure SPF/DMARC via DNS, and monitor queue depth. Email infrastructure as code means reproducible, auditable mail servers.