Andrew Mercer
on this page

Installing Postfix

RHEL / Rocky / Alma / Fedora:

sudo dnf install -y postfix s-nail       # s-nail provides the `mail` command
sudo systemctl enable --now postfix

Debian / Ubuntu:

sudo apt install -y postfix mailutils

On Debian-family systems the package asks interactively for a "general mail configuration type" (No configuration, Internet Site, Internet with smarthost, Satellite system, Local only) and the system mail name. Pre-seed the answers for scripted or unattended installs:

sudo debconf-set-selections <<< "postfix postfix/main_mailer_type select Satellite system"
sudo debconf-set-selections <<< "postfix postfix/mailname string $(hostname -f)"
sudo debconf-set-selections <<< "postfix postfix/relayhost string [smtp.provider.tld]:587"
sudo DEBIAN_FRONTEND=noninteractive apt install -y postfix mailutils

"Satellite system" is Debian's name for a null client that relays everything (see Relaying Outbound Mail). To re-run the questions later: sudo dpkg-reconfigure postfix.

Making Postfix the system MTA

Anything that sends mail through /usr/sbin/sendmail (cron, mail, monitoring tools, applications) uses whichever MTA owns that path.

RHEL-family systems manage it with alternatives:

alternatives --display mta
sudo alternatives --set mta /usr/sbin/sendmail.postfix

If Postfix is the only MTA installed, this is already set. Debian-family packages conflict with each other, so installing Postfix removes Exim or another MTA.

Make sure only one MTA is running. A leftover Sendmail, Exim or a previously installed suite (Zimbra, for example) still bound to port 25 causes baffling symptoms:

sudo ss -tlnp | grep ':25 '
sudo systemctl disable --now sendmail exim4 2>/dev/null

A minimal main.cf for a standalone server

# /etc/postfix/main.cf
compatibility_level = 3.6          # leave whatever your install set; see the overview doc
myhostname     = mail.domain.tld   # check with: hostname -f
mydomain       = domain.tld
myorigin       = $mydomain
mydestination  = $myhostname, localhost.$mydomain, localhost, $mydomain
inet_interfaces = all
inet_protocols  = ipv4             # or all, if IPv6 works end to end (including PTR)
mynetworks      = 127.0.0.0/8 [::1]/128
home_mailbox    = Maildir/         # deliver local users to ~/Maildir/ instead of /var/mail mbox
mailbox_size_limit = 0
recipient_delimiter = +
smtpd_banner    = $myhostname ESMTP

Then validate and apply:

sudo postfix check && sudo systemctl reload postfix

Notes on the less obvious settings:

  • mynetworks: list explicitly what you trust. The old mynetworks_style = subnet trusts every host on the server's local subnets, which on a cloud VM can mean other customers' machines. Prefer host (just this machine) or an explicit list.
  • recipient_delimiter = + enables plus-addressing ([email protected] delivers to user), which is handy for filtering and tracking.
  • mailbox_size_limit = 0 removes Postfix's per-mailbox cap. Enforce quotas in the MDA (Dovecot) instead.
  • smtpd_banner: the default includes ($mail_name). Leaving the version and distribution out gives scanners a little less information.

Firewall

# firewalld (RHEL-family)
sudo firewall-cmd --permanent --add-service=smtp            # 25
sudo firewall-cmd --permanent --add-service=smtp-submission # 587
sudo firewall-cmd --permanent --add-service=smtps           # 465
sudo firewall-cmd --reload

# ufw (Debian-family)
sudo ufw allow 25/tcp && sudo ufw allow 587/tcp && sudo ufw allow 465/tcp

Open only what this host actually serves. A null client needs no inbound ports at all.

Testing mail flow

From the command line:

echo "Test body" | mail -s "Test from $(hostname -s)" [email protected]

By hand, over raw SMTP. This removes every layer between you and exactly what Postfix sees:

$ telnet localhost 25
220 mail.domain.tld ESMTP
EHLO test.local
250-mail.domain.tld
...
MAIL FROM:<[email protected]>
250 2.1.0 Ok
RCPT TO:<[email protected]>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: test message

This is a test message
.
250 2.0.0 Ok: queued as E06041CF26
QUIT

For this local test, the envelope sender doesn't need to be deliverable. You're only confirming Postfix accepts and queues the message.

With swaks. It's scriptable, handles TLS and AUTH, and prints a transcript:

swaks --to [email protected] --server localhost
swaks --to [email protected] --server mail.domain.tld --port 587 --tls --auth LOGIN --auth-user [email protected]

Watch the log in another terminal while you test:

sudo tail -F /var/log/maillog          # RHEL-family
sudo tail -F /var/log/mail.log         # Debian-family with rsyslog
sudo journalctl -f -t postfix/smtpd -t postfix/smtp -t postfix/qmgr -t postfix/cleanup   # journald only

Recent Debian releases don't install rsyslog by default, so there's no /var/log/mail.log until you either install rsyslog or set maillog_file = /var/log/postfix.log (Postfix 3.4+; see Administration and Queue Management).

Building an AUTH PLAIN token for a manual authentication test (only ever over TLS):

printf '\0%s\0%s' 'username' 'password' | base64

A first alias or redirect

The most common first customization, forwarding one address to another, uses virtual_alias_maps:

# /etc/postfix/main.cf
virtual_alias_maps = hash:/etc/postfix/virtual
sudo postmap /etc/postfix/virtual
sudo postfix reload

This pattern (lookup table, then postmap, then reload) recurs throughout Postfix configuration. See the overview's lookup-table section and Virtual Domains, Mailing Lists and Virtual Mailboxes for the fuller version.