Shore Up
A small neighbourhood post box collecting local letters and handing them all to a single trusted courier, who carries the bundle onward to a large central sorting depot.
Linux

Deploy a Postfix Relay with Ansible

Ketan Aagja8 min read
No ratings yet

Before you run this

This playbook installs Postfix on one or more target hosts and configures it as a send-only relay (a "smarthost" client): local mail is handed to an upstream provider over an authenticated, TLS-encrypted SMTP submission connection, and the host does not accept mail from the network. Its purpose is to give servers a reliable way to send notifications, cron output, and application mail without each app talking to your provider directly.

Privileges: the tasks install a package and rewrite files under /etc/postfix, so they run with become: true (sudo/root) on the targets. On the control node you only need Ansible and SSH access to those targets. The upstream SMTP password is a secret — keep it in ansible-vault, not in plain group vars.

Test first: read the playbook, then run it against a single throwaway VM (or with --check --diff) before you touch anything that other systems depend on. Installing Postfix where a mail server already runs will collide with the existing MTA, so never point this at a live mail host.

What it changes, and reversibility: it installs the postfix package, appends a marked block to /etc/postfix/main.cf, and writes /etc/postfix/sasl_passwd (containing your credentials) plus its hashed .db. The main.cf block is fenced with Ansible markers and can be removed cleanly; the credential file must be deleted by hand. Setting inet_interfaces = loopback-only means the host will stop listening for inbound SMTP on external interfaces — deliberate for a relay, but confirm nothing was relying on it receiving mail.

I assume an Ansible 2.15+ control node, Ubuntu 22.04 LTS targets, and an upstream that accepts authenticated submission on port 587. On RHEL/Alma the package is still postfix but you would use ansible.builtin.dnf, and the CA bundle path differs (see the note on smtp_tls_CAfile below).

The variables

Keep non-secret settings in group_vars/relay.yml:

# group_vars/relay.yml
relay_smarthost: "smtp.example.com"   # your provider's submission host
relay_port: 587
relay_username: "postmaster@example.com"

Put the password in an encrypted file so it never lands in git in the clear:

ansible-vault create group_vars/relay_secret.yml

Inside that file:

relay_password: "REPLACE-WITH-REAL-PASSWORD"

Replace smtp.example.com, the username, and the password with your real values. relay here is just the inventory group name — put your target hosts under [relay] in your inventory.

The playbook

---
# postfix-relay.yml
- name: Configure Postfix as a send-only relay
  hosts: relay
  become: true

  tasks:
    # Preseed the debconf answers so apt doesn't open an interactive dialog.
    - name: Preseed Postfix mailer type
      ansible.builtin.debconf:
        name: postfix
        question: postfix/main_mailer_type
        value: "Internet Site"
        vtype: select

    - name: Preseed Postfix system mail name
      ansible.builtin.debconf:
        name: postfix
        question: postfix/mailname
        value: "{{ ansible_fqdn }}"
        vtype: string

    - name: Install Postfix
      ansible.builtin.apt:
        name: postfix
        state: present
        update_cache: true

    # Relay-specific settings appended as a marked block. Directives later in
    # main.cf win, so this block overrides the packaged defaults cleanly and
    # can be removed later with state: absent.
    - name: Apply relay settings to main.cf
      ansible.builtin.blockinfile:
        path: /etc/postfix/main.cf
        marker: "# {mark} ANSIBLE MANAGED RELAY BLOCK"
        block: |
          relayhost = [{{ relay_smarthost }}]:{{ relay_port }}
          smtp_sasl_auth_enable = yes
          smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
          smtp_sasl_security_options = noanonymous
          smtp_tls_security_level = encrypt
          smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
          inet_interfaces = loopback-only
          inet_protocols = ipv4
      notify: Restart Postfix

    # Credential map. 0600 so only root can read the password.
    - name: Write SASL credential map
      ansible.builtin.copy:
        dest: /etc/postfix/sasl_passwd
        content: "[{{ relay_smarthost }}]:{{ relay_port }} {{ relay_username }}:{{ relay_password }}\n"
        owner: root
        group: root
        mode: "0600"
      notify: Rebuild SASL map

  handlers:
    - name: Rebuild SASL map
      ansible.builtin.command:
        cmd: postmap /etc/postfix/sasl_passwd
      notify: Restart Postfix

    - name: Restart Postfix
      ansible.builtin.service:
        name: postfix
        state: restarted

Run it:

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

Do a dry run first — --check --diff shows exactly which lines would change without touching the host.

Why these directives

A few of the settings above are the load-bearing ones, so it's worth being explicit:

  • relayhost = [smtp.example.com]:587 — the square brackets tell Postfix to use the host as given and skip an MX lookup, which is what you want for a fixed submission host.
  • smtp_tls_security_level = encrypt forces TLS on the outbound connection and refuses to send if it can't negotiate it. That is the right posture when you're shipping credentials.
  • smtp_tls_CAfile points at the system CA bundle so the smarthost's certificate is actually verified. /etc/ssl/certs/ca-certificates.crt is the Debian/Ubuntu path (from the ca-certificates package); on RHEL/Alma it is /etc/pki/tls/certs/ca-bundle.crt.
  • inet_interfaces = loopback-only stops Postfix listening for inbound mail on the network — a relay client has no business accepting external connections.

The postmap command turns the plain sasl_passwd file into the hashed sasl_passwd.db that Postfix actually reads; the handler runs it whenever the credential file changes. If your Postfix build was compiled without Berkeley DB support (uncommon on stock Ubuntu) the hash: type won't exist — check postconf -m on the target and, if so, switch both the map type and the postmap invocation to lmdb per the Postfix documentation. Don't guess this; postconf -m lists exactly which map types your binary supports.

Verify it worked

SSH to a target and check the service and the effective configuration:

systemctl status postfix
postconf -n            # shows only non-default settings; confirm relayhost etc.
postconf relayhost     # should print your smarthost

Confirm the hashed map exists:

ls -l /etc/postfix/sasl_passwd.db

Then send a real test message. If mailutils (which provides the mail command) isn't installed, swaks is a good alternative — either way, install it separately:

echo "relay test $(date)" | mail -s "relay test" you@example.com

Watch the log as it goes out:

tail -f /var/log/mail.log

A successful hand-off shows a line ending in status=sent with the smarthost's response. status=deferred with an authentication or TLS error tells you the credentials or port are wrong. Check the queue with postqueue -p; an empty queue means everything was accepted.

Undo

To back the relay settings out, remove the managed block by re-running a task with state: absent on the same blockinfile (Ansible finds it by the marker) and delete the credential files, then restart:

# on the target, manual rollback:
sudo rm -f /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfix

To remove Postfix entirely on a test box, sudo apt purge postfix. Don't purge a host that other services rely on for local mail delivery without confirming what replaces it. Because the config change is a single marked block, reverting it is low-risk — but the credential file is the one thing Ansible won't clean up for you, so remove it by hand when you decommission the relay.

For the official directive reference, see the Postfix documentation for postconf(5) and the "Enabling SASL authentication in the Postfix SMTP client" section on the Postfix project site, and Ansible's own docs for the debconf, blockinfile, and apt modules.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Automate Failover Testing Between Primary and Secondary Mail Servers

Having two MX hosts is not the same as knowing failover works. The only way to trust it is to test it: confirm both hosts answer on port 25 and accept mail for your domain, and once in a while prove that when the primary is down, the secondary still takes the message and holds it. This guide gives you a small probe script you can run on a schedule, and a controlled live drill for the real thing.

10 min read

Audit Postfix for Open Relay Vulnerabilities with Bash

This script reads your Postfix relay-control settings with postconf and flags configurations that could let outsiders relay mail through your server — the classic "open relay" that gets you onto blocklists and turns your box into a spam cannon. It is read-only. It changes nothing, writes nothing, and reloads nothing , so there is no config to back up and nothing to roll back. It simply reports.

9 min read

Automate Postfix Transport Map Updates Without Downtime

This guide gives you a small bash script that rebuilds a Postfix transport map from its flat-file source, validates the configuration, and reloads Postfix so the new routing takes effect. postfix reload re-reads configuration and recycles daemons gracefully — it does not drop in-flight SMTP connections or stop the queue, which is why we use it instead of restart .

8 min read

Detect Spam Relay Abuse from Postfix Mail Logs

This guide gives you a read-only Python script that parses a Postfix mail log and reports two things: authenticated senders (SASL users) who sent an unusually large number of messages or recipients — the classic signature of a compromised mailbox being used to blast spam — and source IPs that keep tripping "Relay access denied", which is relay probing. The script does not change anything : it reads the log, counts, and prints a report. It never touches Postfix config, never disables an account, never blocks an IP.

10 min read