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

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.