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
status=sent) to a local demo listener on127.0.0.1:1025. Real provider SMTP auth and STARTTLS were not tested.
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:
rpm -q sendmailSample output:
package sendmail is not installedIf you get a package name back instead of “not installed”, remove it:
sudo dnf remove -y sendmailInstall Postfix, SASL support for plain-text auth, and the command-line mail client:
sudo dnf install -y postfix cyrus-sasl-plain s-nailThe 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:
sudo dnf install -y swaksConfirm everything landed:
rpm -q postfix cyrus-sasl-plain s-nail swaksSample 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_64Check the Postfix version:
postconf mail_versionSample output:
mail_version = 3.8.5Before you build a password file, see which map types your Postfix build supports:
postconf -mSample output:
cidr environ fail inline internal lmdb memcache pipemap proxy randmap regexp socketmap static tcp texthash unionmap unixOn 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:
postconf default_database_typeOn RHEL 10 you should see:
default_database_type = lmdbStore that value:
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”:
sudo postconf -e 'relayhost = [smtp.example.net]:587'Listen on localhost only so other machines cannot dump mail on you:
sudo postconf -e 'inet_interfaces = loopback-only'Turn off local delivery for domain names:
sudo postconf -e 'mydestination ='If something still tries local delivery, bounce it with a clear error:
sudo postconf -e 'local_transport = error: local delivery disabled'Only local processes on this host may submit mail:
sudo postconf -e 'mynetworks = 127.0.0.0/8, [::1]/128'Set the domain Postfix appends to bare usernames in the From address:
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:
sudo postconf -e 'smtp_sasl_auth_enable = yes'Point Postfix at a password file, using the map type from earlier:
sudo postconf -e "smtp_sasl_password_maps = ${MAP_TYPE}:/etc/postfix/sasl_passwd"Do not allow anonymous auth:
sudo postconf -e 'smtp_sasl_security_options = noanonymous'Require TLS to the relay:
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:
sudo tee /etc/postfix/sasl_passwd <<'EOF'
[smtp.example.net]:587 relayuser:relaypass
EOFLock it down:
sudo chmod 600 /etc/postfix/sasl_passwdCompile it into the map format Postfix reads:
sudo postmap "${MAP_TYPE}:/etc/postfix/sasl_passwd"You should see the source file and the compiled map:
ls -l /etc/postfix/sasl_passwd*Sample output on RHEL 10:
-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.lmdbPostfix 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:
sudo postmap "${MAP_TYPE}:/etc/postfix/sasl_passwd"
sudo systemctl reload postfixConfigure 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:
sudo postconf -e 'inet_interfaces = all'Trust localhost plus only the subnets that should relay through this host:
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:
sudo postconf -e 'mydestination ='Forward everything upstream:
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:
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:
sudo firewall-cmd --get-active-zonesSample output:
public (default)
interfaces: enp0s3 enp0s8Set a variable to that zone name:
ZONE=publicChange public if your output shows something else.
Allow SMTP only from the trusted subnet:
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:
sudo firewall-cmd --reload
sudo firewall-cmd --zone="$ZONE" --list-rich-rulesIn 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:
sudo postfix checkSee only the settings you changed (easier to spot typos):
sudo postconf -nFor a null client, the relay-related lines look like this:
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 = encryptAn internal LAN relay adds inet_interfaces = all, a wider mynetworks, and:
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destinationStart Postfix and enable it at boot:
sudo systemctl enable --now postfixCheck that it is running:
sudo systemctl status postfix --no-pagerSample 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:
sudo ss -lntp 'sport = :25'Sample 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:
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:
=== 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 ByeNote 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:
sudo dnf install -y python3-pipInstall the listener package for your user:
python3 -m pip install --user aiosmtpdStart it in the background and remember the PID:
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:
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 postfixRun 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:
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 postfixStop the demo listener:
kill "$DEMO_SMTP_PID"Confirm nothing is still bound to port 1025:
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:
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:
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:
sudo journalctl -u postfix --no-pager -n 8When the relay accepts the message, you will see status=sent:
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: removedIf relayhost points at a placeholder like smtp.example.net with no real DNS, mail sits in the queue as deferred:
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:
sudo tail -3 /var/log/maillogSee what is waiting in the queue:
sudo postqueue -pSample output when DNS for the relay hostname fails:
-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:
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
- Red Hat Enterprise Linux 9 — Deploying and configuring a Postfix SMTP server
- Red Hat Enterprise Linux 10 — Deploying and configuring a Postfix SMTP server
- Red Hat Enterprise Linux 10 — Mail server changes (LMDB, Sendmail removal)
- Postfix SASL client authentication
- Postfix TLS support
- SWAKS — Swiss Army Knife for SMTP
- RFC 5737 — Documentation address blocks
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.

