Server

Postfix as a Null Client: Send-Only Mail Done Right

By Prabath Thalangama· October 24, 2026· 3 min read
#postfix#email#security

Introduction

Most servers don't need a full mail server — they need to send: cron output, monitoring alerts, "your export is ready" notifications. A null client is Postfix configured to relay everything through an authenticated smarthost (your provider, Gmail, SES, a company relay) and accept nothing. It's a handful of lines and it's the right amount of mail server for the job.

Concepts

A null client:

  • Listens only on 127.0.0.1 (or not at all as a network service).
  • Forwards all mail to a relayhost — it never does its own MX lookups or direct delivery.
  • Authenticates to the relay with SASL over TLS.
  • Rewrites the envelope/header From so [email protected] becomes something the relay and recipients will accept.

Hands-on

Install and pick "Internet Site" or "No configuration" — you'll overwrite it.

/etc/postfix/main.cf

# Identity
myhostname = web01.example.com
myorigin = example.com
mydestination =                       # empty: accept mail for NO domain
inet_interfaces = loopback-only
inet_protocols = ipv4

# Relay everything through the smarthost
relayhost = [smtp.example-relay.com]:587

# SASL auth to the relay
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_mechanism_filter = plain, login

# TLS to the relay (required)
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_loglevel = 1

# Rewrite sender addresses
sender_canonical_maps = regexp:/etc/postfix/sender_canonical
smtp_header_checks = regexp:/etc/postfix/header_checks

# Don't relay for anyone else
smtpd_relay_restrictions = permit_mynetworks, reject

Credentials — /etc/postfix/sasl_passwd

[smtp.example-relay.com]:587    apikey:s3cr3t-token
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db

Rewrite the From address

/etc/postfix/sender_canonical — turn every local sender into one address:

/etc/postfix/header_checks — fix the visible From: header too:

/^From:.*/  REPLACE From: server alerts <[email protected]>

Apply

sudo systemctl restart postfix

Verification and troubleshooting

echo "test body" | mail -s "null client test" [email protected]
sudo tail -f /var/log/mail.log            # watch the relay handshake
mailq                                     # anything stuck?
sudo postqueue -f                         # flush the queue now

A healthy send in the log ends with status=sent (250 2.0.0 OK ...).

  • status=bounced ... Relay access denied — you're hitting the relay without auth, or SASL failed. Check sasl_passwd maps the exact relayhost string (brackets and port included), and postmap was run.
  • SASL authentication failed; server ... said: 535 — wrong username/token, or the relay wants LOGIN not PLAIN (smtp_sasl_mechanism_filter), or the token has expired.
  • Cannot start TLS: handshake failure — smtp_tls_CAfile path wrong, or the relay needs smtp_tls_security_level = may on an internal relay with a self-signed cert (prefer encrypt + a proper CA).
  • Mail sent but lands in spam — the relay isn't authorised in your domain's SPF, or the From: you rewrote to isn't DKIM-signed by the relay. See SPF, DKIM, DMARC end to end.
  • status=deferred ... Connection timed out — outbound 587 (or 465) blocked by a firewall/cloud security group. Many providers block 25; use 587.
  • Postfix accepting inbound connections — inet_interfaces isn't loopback-only, or mydestination isn't empty. sudo ss -tlnp | grep :25 should show 127.0.0.1 only.
  • App mail works, cron mail doesn't — cron sends to root@localhost; make sure sender_canonical catches it and /etc/aliases routes root somewhere (newaliases after editing).
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.