Configure Postfix SMTP Relay on RHEL 9/10

Configure Postfix as a null client or internal SMTP relay on RHEL 9 and 10, Rocky Linux, AlmaLinux, CentOS Stream, and Oracle Linux using relayhost, SASL authentication, STARTTLS, SWAKS, and queue troubleshooting.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

Postfix SMTP relay forwarding system email through an authenticated mail server

Your Linux servers send more email than you might expect—cron reports, monitoring alerts, app notifications, and messages from system services. If Postfix tries to deliver that mail directly to the Internet, it often fails. Cloud hosts block outbound port 25, and remote mail servers reject senders with no reputation.

The practical fix is a relay: Postfix accepts mail locally (or from trusted hosts on your LAN) and hands it to your company SMTP server or a hosted provider like Microsoft 365. This guide shows that setup on RHEL 9, RHEL 10, Rocky Linux, AlmaLinux, CentOS Stream, and Oracle Linux.

You will configure either a null client (one server forwarding its own mail) or an internal LAN relay (several hosts sharing one outbound path). This is not a guide to running a full incoming mail server—no public MX records, Maildir mailboxes, or BIND DNS zones here.

Validation environment: Rocky Linux 10.2 (Red Quartz), Postfix 3.8.5, SWAKS from EPEL. I verified package install, null-client settings, password maps, service startup, loopback binding, SWAKS and mail tests, deferred mail when using documentation hostnames, and successful forwarding (status=sent) to a local demo listener on 127.0.0.1:1025. Real provider SMTP auth and STARTTLS were not tested.

IMPORTANT
On RHEL 10, Postfix no longer uses Berkeley DB for lookup maps. The default is LMDB (default_database_type = lmdb). If you copy an old EL8 example with hash:, run postconf -m on your system first and use whatever backend it actually lists.

Red Hat splits these jobs in the docs: Postfix as a relay/null client on RHEL 9 and the same on RHEL 10 are separate from building a host that receives mail for a public domain. The settings below—empty mydestination, bracketed relayhost, inet_interfaces = loopback-only, no local delivery, loopback-only mynetworks—follow that null-client model.


Choose the Correct Postfix Relay Mode

Decide which pattern you need before you touch main.cf. Mixing null-client and internal-relay settings on one host is how you end up with open relays or mail loops back to myself errors.

Mode When to use it Remote clients can connect? Receives Internet mail?
Null client One server sends its own alerts through a relay No No
Internal SMTP relay Many LAN hosts share one outbound Postfix server Yes, from networks you trust No
Full mail server You receive mail for @yourdomain.com Yes Yes

A null client listens only on localhost and sends everything to relayhost. An internal relay listens on the LAN and trusts only the subnets you list in mynetworks. A full mail server needs MX, PTR, SPF, DKIM, DMARC, TLS, spam filtering, and usually Dovecot—that is a different project.

One thing that trips people up: mynetworks lists who may send mail through this Postfix box. It is not a list of remote servers Postfix dials out to.


Lab Setup and Prerequisites

The examples below use documentation addresses from RFC 5737—safe placeholders, not a real home network:

Piece Example value
Postfix host relay1.example.com
Postfix IP 192.0.2.10
Upstream relay smtp.example.net:587
Trusted LAN 192.0.2.0/24
Test recipient [email protected]

Before you start, gather:

  • A RHEL-family machine and sudo access
  • Your relay hostname, port (usually 587 for submission), and whether login is required
  • SMTP username and password—or whatever your provider documents for OAuth or app passwords
  • Outbound network access to that port (587 for most hosted relays, or port 25 if your org runs its own relay)

A null client does not need an MX record for your domain. It only needs to resolve and reach the relay in relayhost.


Install Postfix and Required Tools

RHEL expects only one mail daemon on port 25. If Sendmail is still installed, remove it first.

Check whether Sendmail is present:

bash
rpm -q sendmail

Sample output:

output
package sendmail is not installed

If you get a package name back instead of “not installed”, remove it:

bash
sudo dnf remove -y sendmail

Install Postfix, SASL support for plain-text auth, and the command-line mail client:

bash
sudo dnf install -y postfix cyrus-sasl-plain s-nail

The s-nail package is what provides the mail command on current RHEL-family releases.

SWAKS—the SMTP test tool used later—lives in EPEL, not the base repos. After enabling EPEL for your version, install it:

bash
sudo dnf install -y swaks

Confirm everything landed:

bash
rpm -q postfix cyrus-sasl-plain s-nail swaks

Sample output:

output
postfix-3.8.5-10.el10_2.x86_64
cyrus-sasl-plain-2.1.28-30.el10_2.x86_64
swaks-20240103.0-2.el10_1.noarch
s-nail-14.9.24-12.el10.x86_64

Check the Postfix version:

bash
postconf mail_version

Sample output:

output
mail_version = 3.8.5

Before you build a password file, see which map types your Postfix build supports:

bash
postconf -m

Sample output:

output
cidr environ fail inline internal lmdb memcache pipemap proxy randmap regexp socketmap static tcp texthash unionmap unix

On RHEL 10 you will see lmdb and not hash. Use whatever your host supports—not what an old blog post says.

Grab the default map type and save it in a shell variable for the next steps:

bash
postconf default_database_type

On RHEL 10 you should see:

output
default_database_type = lmdb

Store that value:

bash
MAP_TYPE=$(postconf -h default_database_type)

Configure Postfix as a Null Client

This is the setup most people want: one server that forwards cron mail, alerts, and app messages through an existing relay. Nothing is delivered to local mailboxes; everything goes out through relayhost.

Tell Postfix where to send mail. Square brackets mean “connect to this host directly—do not look up MX records”:

bash
sudo postconf -e 'relayhost = [smtp.example.net]:587'

Listen on localhost only so other machines cannot dump mail on you:

bash
sudo postconf -e 'inet_interfaces = loopback-only'

Turn off local delivery for domain names:

bash
sudo postconf -e 'mydestination ='

If something still tries local delivery, bounce it with a clear error:

bash
sudo postconf -e 'local_transport = error: local delivery disabled'

Only local processes on this host may submit mail:

bash
sudo postconf -e 'mynetworks = 127.0.0.0/8, [::1]/128'

Set the domain Postfix appends to bare usernames in the From address:

bash
sudo postconf -e 'myorigin = example.com'

Do not set inet_interfaces = all on a single-host null client. You do not need a public SMTP listener, and it makes abuse easier.


Add SMTP Authentication and STARTTLS

Most hosted relays want port 587, STARTTLS, and a username/password. Here is how to wire that in.

Turn on SMTP authentication when Postfix talks to the relay:

bash
sudo postconf -e 'smtp_sasl_auth_enable = yes'

Point Postfix at a password file, using the map type from earlier:

bash
sudo postconf -e "smtp_sasl_password_maps = ${MAP_TYPE}:/etc/postfix/sasl_passwd"

Do not allow anonymous auth:

bash
sudo postconf -e 'smtp_sasl_security_options = noanonymous'

Require TLS to the relay:

bash
sudo postconf -e 'smtp_tls_security_level = encrypt'

RHEL already trusts public CAs via /etc/pki/tls/certs/ca-bundle.crt—you usually do not need to change smtp_tls_CAfile.

Create the password file. One line per relay; format is host:port username:password:

bash
sudo tee /etc/postfix/sasl_passwd <<'EOF'
[smtp.example.net]:587    relayuser:relaypass
EOF

Lock it down:

bash
sudo chmod 600 /etc/postfix/sasl_passwd

Compile it into the map format Postfix reads:

bash
sudo postmap "${MAP_TYPE}:/etc/postfix/sasl_passwd"

You should see the source file and the compiled map:

bash
ls -l /etc/postfix/sasl_passwd*

Sample output on RHEL 10:

output
-rw-------. 1 root root    40 Jul 11 20:06 /etc/postfix/sasl_passwd
-rw-------. 1 root root 12288 Jul 11 20:06 /etc/postfix/sasl_passwd.lmdb

Postfix picks up this map when you start it in the next section. If you change the password later while Postfix is already running, rebuild the map and reload:

bash
sudo postmap "${MAP_TYPE}:/etc/postfix/sasl_passwd"
sudo systemctl reload postfix
WARNING
Do not assume your normal Microsoft 365 or Gmail password still works for SMTP. Microsoft and Google change auth rules often—OAuth, connectors, and app passwords are common now. For Gmail specifically, see the Gmail SMTP relay with Postfix guide instead of guessing.

Configure Postfix as an Internal LAN SMTP Relay

Use this when several servers on your network should share one outbound relay—monitoring boxes, app servers, printers, and so on. Treat it as a separate layout from the null client; do not blindly merge both inet_interfaces settings.

Listen on your LAN interfaces:

bash
sudo postconf -e 'inet_interfaces = all'

Trust localhost plus only the subnets that should relay through this host:

bash
sudo postconf -e 'mynetworks = 127.0.0.0/8, [::1]/128, 192.0.2.0/24'

Keep local delivery off if this box is relay-only:

bash
sudo postconf -e 'mydestination ='

Forward everything upstream:

bash
sudo postconf -e 'relayhost = [smtp.example.net]:587'

If the upstream relay needs auth, keep the SASL and TLS settings from the previous section.

Lock down who may relay. This matches the Postfix default Red Hat documents:

bash
sudo postconf -e 'smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination'

Trusted mynetworks clients may relay. Authenticated clients may relay when you enable inbound SMTP auth. Everyone else gets blocked from relaying to arbitrary Internet domains.

Restrict firewalld so SMTP is not open to the world. First, see which zone your NIC uses:

bash
sudo firewall-cmd --get-active-zones

Sample output:

output
public (default)
  interfaces: enp0s3 enp0s8

Set a variable to that zone name:

bash
ZONE=public

Change public if your output shows something else.

Allow SMTP only from the trusted subnet:

bash
sudo firewall-cmd --zone="$ZONE" --permanent --add-rich-rule='rule family="ipv4" source address="192.0.2.0/24" service name="smtp" accept'

Apply the rule and confirm it stuck:

bash
sudo firewall-cmd --reload
sudo firewall-cmd --zone="$ZONE" --list-rich-rules

In a lab where a client can actually reach port 25, try relaying from an IP outside mynetworks—Postfix should refuse. In production, firewalld should stop strangers before they hit Postfix at all. For finer control, see restrict Postfix relay by source network.

Never put an unauthenticated internal relay on the public Internet. Open relays get abused within hours and blacklisted just as fast.


Validate, Start, and Test Postfix

Run a quick syntax check—no output means the config parsed cleanly:

bash
sudo postfix check

See only the settings you changed (easier to spot typos):

bash
sudo postconf -n

For a null client, the relay-related lines look like this:

output
inet_interfaces = loopback-only
local_transport = error: local delivery disabled
mydestination =
mynetworks = 127.0.0.0/8, [::1]/128
myorigin = example.com
relayhost = [smtp.example.net]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = lmdb:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt
smtp_tls_security_level = encrypt

An internal LAN relay adds inet_interfaces = all, a wider mynetworks, and:

output
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination

Start Postfix and enable it at boot:

bash
sudo systemctl enable --now postfix

Check that it is running:

bash
sudo systemctl status postfix --no-pager

Sample output:

output
● postfix.service - Postfix Mail Transport Agent
     Loaded: loaded (/usr/lib/systemd/system/postfix.service; enabled; preset: disabled)
     Active: active (running) since Sat 2026-07-11 20:06:38 IST; 2s ago
   Main PID: 79572 (master)

On a null client, SMTP should listen only on localhost:

bash
sudo ss -lntp 'sport = :25'

Sample output:

output
LISTEN 0 100 127.0.0.1:25 0.0.0.0:* users:(("master",pid=79572,fd=13))

Send a test message with SWAKS. If SWAKS complains about a missing HELO string, add --helo relay1.example.com as shown:

bash
swaks --server 127.0.0.1 --helo relay1.example.com --from [email protected] --to [email protected] --header 'Subject: Postfix relay test' --body 'SWAKS null-client test'

Sample output:

output
=== Trying 127.0.0.1:25...
=== Connected to 127.0.0.1.
<-  220 rocky1.localdomain ESMTP Postfix
 -> EHLO relay1.example.com
<-  250-STARTTLS
<-  250 CHUNKING
 -> 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>
<-  250 2.0.0 Ok: queued as C7D48828AB
 -> QUIT
<-  221 2.0.0 Bye

Note the queue ID after queued as—you will grep for it in the logs. On an internal relay, run the same command from a trusted client against 192.0.2.10 instead of 127.0.0.1.

Optional: confirm relay delivery with a local demo server

Want proof that relayhost forwarding works before you point at production? Spin up a tiny SMTP listener on the same machine and aim Postfix at it briefly.

Install Python pip if you do not have it:

bash
sudo dnf install -y python3-pip

Install the listener package for your user:

bash
python3 -m pip install --user aiosmtpd

Start it in the background and remember the PID:

bash
python3 -c "
from aiosmtpd.controller import Controller
class H:
    async def handle_DATA(self, server, session, envelope):
        return '250 Message accepted for relay test'
Controller(H(), hostname='127.0.0.1', port=1025).start()
import time
while True:
    time.sleep(60)
" &

DEMO_SMTP_PID=$!

Point Postfix at the demo listener:

bash
sudo postconf -e 'relayhost = [127.0.0.1]:1025'
sudo postconf -e 'smtp_sasl_auth_enable = no'
sudo postconf -e 'smtp_tls_security_level = may'
sudo systemctl reload postfix

Run the SWAKS test again. In the log you want relay=127.0.0.1[127.0.0.1]:1025 and status=sent.

Put production settings back when you are done:

bash
sudo postconf -e 'relayhost = [smtp.example.net]:587'
sudo postconf -e 'smtp_sasl_auth_enable = yes'
sudo postconf -e 'smtp_tls_security_level = encrypt'
sudo systemctl reload postfix

Stop the demo listener:

bash
kill "$DEMO_SMTP_PID"

Confirm nothing is still bound to port 1025:

bash
sudo ss -lntp 'sport = :1025'

When you are ready to test the real relay, SWAKS can walk through STARTTLS and login. It prompts for the password and can hide it from both your shell history and the printed SMTP transcript:

bash
swaks \
  --server smtp.example.net:587 \
  --tls \
  --auth LOGIN \
  --auth-user relayuser \
  --auth-password \
  --protect-prompt \
  --auth-hide-password \
  --from [email protected] \
  --to [email protected]

--protect-prompt hides the password while you type it. --auth-hide-password keeps the encoded credential out of the SMTP transcript SWAKS prints—important for AUTH LOGIN and AUTH PLAIN, where the exchange can otherwise be decoded from the terminal output.

You can also pipe a quick message through the local MTA:

bash
echo 'Local MTA test' | mail -s 'Postfix null client' [email protected]

Verify Logs, Queue, and Delivery Status

After you send mail, check what Postfix did:

bash
sudo journalctl -u postfix --no-pager -n 8

When the relay accepts the message, you will see status=sent:

output
Jul 11 20:06:43 rocky1 postfix/smtpd[79712]: C7D48828AB: client=localhost[127.0.0.1]
Jul 11 20:06:43 rocky1 postfix/qmgr[79574]: C7D48828AB: from=<[email protected]>, size=424, nrcpt=1 (queue active)
Jul 11 20:06:43 rocky1 postfix/smtp[79578]: C7D48828AB: to=<[email protected]>, relay=127.0.0.1[127.0.0.1]:1025, delay=0.08, delays=0.06/0/0/0.02, dsn=2.0.0, status=sent (250 Message accepted for relay test)
Jul 11 20:06:43 rocky1 postfix/qmgr[79574]: C7D48828AB: removed

If relayhost points at a placeholder like smtp.example.net with no real DNS, mail sits in the queue as deferred:

output
Jul 11 20:06:58 rocky1 postfix/smtp[80085]: A28D8828AB: to=<[email protected]>, relay=none, delay=0.24, dsn=4.3.5, status=deferred (Host or domain name not found. Name service error for name=smtp.example.net type=A: Host found but no data record of requested type)

Some hosts also log to /var/log/maillog:

bash
sudo tail -3 /var/log/maillog

See what is waiting in the queue:

bash
sudo postqueue -p

Sample output when DNS for the relay hostname fails:

output
-Queue ID-  --Size-- ----Arrival Time---- -Sender/Recipient-------
A28D8828AB      433 Sat Jul 11 20:06:58  [email protected]
(Host or domain name not found. Name service error for name=smtp.example.net type=A: Host found but no data record of requested type)
                                         [email protected]

-- 0 Kbytes in 1 Request.

That deferred state is normal for documentation hostnames in a lab. After a successful demo-relay test, postqueue -p should say the queue is empty. Once DNS, credentials, or firewall issues are fixed, nudge Postfix to retry:

bash
sudo postqueue -f
What you see in the log What it usually means
status=sent The next server took the message
status=deferred Temporary problem—Postfix will retry
status=bounced Permanent failure
Relay access denied Client or destination not allowed
SASL authentication failed Bad password or wrong auth method
certificate verify failed TLS trust or hostname mismatch
Connection timed out Firewall, ISP block, or wrong network path

status=sent only means the next hop accepted the mail—not that it landed in someone's inbox.


Troubleshoot Common Postfix Relay Errors

Symptom Likely cause What to try
SASL authentication failed Wrong credentials or missing SASL package Verify provider settings; confirm cyrus-sasl-plain is installed
Must issue a STARTTLS command first Auth before TLS Use port 587 and smtp_tls_security_level = encrypt
certificate verify failed CA bundle or hostname mismatch Check smtp_tls_CAfile and the name in relayhost
Connection timed out Port blocked upstream timeout 5 bash -c 'cat < /dev/null > /dev/tcp/smtp.example.net/587'
Connection refused Wrong host or port Double-check relayhost and provider docs
mail for ... loops back to myself relayhost points at this same server Fix relayhost, DNS, and destination settings
unsupported dictionary type: hash Old map type on RHEL 10 postconf default_database_type and postconf -m; rebuild the map
Mail stays deferred DNS, network, TLS, or upstream issue Read postqueue -p and matching log lines
Server became an open relay mynetworks too wide Narrow trusted networks immediately
Sender rejected Provider does not allow that From address Use an authorized sender or rewrite rules
IPv6 failures Broken IPv6 route or DNS Fix IPv6 or set inet_protocols = ipv4 if you intend IPv4 only

References


Summary

Most RHEL-family setups need one of these two patterns:

Single-host null client

  • Listen on loopback only (inet_interfaces = loopback-only)
  • Turn off local delivery (mydestination =, local_transport = error:...)
  • Send everything through relayhost
  • Add SASL and TLS when your provider requires them
  • No inbound SMTP firewall hole needed

Internal SMTP relay

  • Listen on the LAN (inet_interfaces = all)
  • Trust only explicit subnets in mynetworks
  • Lock down firewalld by source
  • Forward through an authenticated upstream relay
  • Confirm strangers cannot relay—via firewalld and smtpd_relay_restrictions

For Gmail or Microsoft 365 specifics, use a dedicated provider guide such as Gmail SMTP relay with Postfix instead of assuming a normal account password still works in 2026.


Frequently Asked Questions

1. Does a Postfix null client need an MX record for my domain?

No. A null client only has to reach the relay you put in relayhost. MX records matter when this same machine is supposed to receive mail for your domain from the Internet.

2. What is the difference between a null client and an internal SMTP relay?

A null client runs on one server, listens on localhost only, and forwards mail from local jobs like cron. An internal relay listens on the LAN and accepts mail from other hosts you trust before forwarding everything upstream.

3. Why does Postfix on RHEL 10 fail with unsupported dictionary type hash?

RHEL 10 dropped Berkeley DB. Run postconf default_database_type and postconf -m, then rebuild your maps with the backend your system supports—usually lmdb on RHEL 10.

4. What does status=sent mean in the Postfix logs?

The next mail server accepted the message. That is not a promise it reached the recipient inbox—only that the relay hop took it.

5. Can I use a personal Gmail or Microsoft 365 password for relay authentication?

Only if your provider still allows it for your account. Policies change often—use a dedicated guide for OAuth, connectors, or app passwords instead of assuming your normal login password works.

6. How do I test a Postfix SMTP relay without Telnet?

Use SWAKS. One command can walk through EHLO, STARTTLS, authentication, sender, recipient, and the message body against localhost or your relay hostname.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …