Fix Postfix Relay Access Denied on Debian 13

updated 11 min read

The mail did not arrive. The app says it sent the mail, the user says Outlook showed an error, and the Postfix log has one of four lines in it. This article covers the Postfix Relay access denied error on Debian 13, plus the three other errors that usually turn up alongside it: deferred port 25 timeouts, Domain not found sender rejections and SASL authentication failures. For each one you get the exact log line, what causes it, commands to confirm the cause, and a fix that does not turn your server into an open relay.

Debian 13 (trixie) ships Postfix 3.10. At the time of writing, the stable suite has 3.10.11-0+deb13u1 and stable-security has 3.10.13-0+deb13u1, according to tracker.debian.org. Install the security update before you start debugging.

The error messages, exactly as logged

These are typical forms of the four lines. Hostnames, IPs and queue IDs will be different on your server:

postfix/smtpd[2817]: NOQUEUE: reject: RCPT from client.example.net[198.51.100.20]: 454 4.7.1 <someone@gmail.com>: Relay access denied; from=<alice@example.org> to=<someone@gmail.com> proto=ESMTP helo=<laptop>
postfix/smtp[3104]: 4F2A11C0F3A: to=<someone@gmail.com>, relay=none, delay=30, delays=0.01/0/30/0, dsn=4.4.1, status=deferred (connect to gmail-smtp-in.l.google.com[203.0.113.25]:25: Connection timed out)
postfix/smtpd[2817]: NOQUEUE: reject: RCPT from localhost[127.0.0.1]: 450 4.1.8 <www-data@web01.lan>: Sender address rejected: Domain not found; from=<www-data@web01.lan> to=<ops@example.org> proto=ESMTP helo=<web01>
postfix/smtpd[2817]: warning: unknown[192.0.2.44]: SASL LOGIN authentication failed: UGFzc3dvcmQ6

Where Debian 13 puts these lines

Debian has not installed rsyslog by default since bookworm, so on a fresh Debian 13 system /var/log/mail.log often does not exist. Postfix still logs through syslog, and journald collects those messages. One more change: Debian removed the old postfix@- unit in package version 3.9.1-7 and replaced it with a plain postfix.service. Commands from Debian 12 guides that use journalctl -u postfix@- return nothing on trixie.

journalctl -u postfix --since today
journalctl -u postfix -f
journalctl -t postfix/smtpd --since '1 hour ago'
journalctl -u postfix --since today | grep -E 'reject:|status=deferred|SASL'

If you installed rsyslog yourself, use grep -E 'reject:|status=deferred|SASL' /var/log/mail.log as well.

Log lineWhich sideUsual causeFix
454 4.7.1 ... Relay access deniedYour smtpd rejecting a clientClient not in mynetworks and not SASL-authenticatedSubmission port 587 with SASL. Do not widen mynetworks
status=deferred (connect to ...:25: Connection timed out)Your smtp client sending outOutbound port 25 blocked by provider or firewallRelay through a smarthost on 587, or get the port unblocked
Sender address rejected: Domain not foundWhoever applies reject_unknown_sender_domainSender domain has no MX or A record (*.lan, localhost)Fix myorigin/sender address, check DNS
SASL LOGIN authentication failedYour smtpdWrong credentials, broken Dovecot socket, or brute forceCheck the auth backend and rate-limit

Quick fix for Postfix Relay access denied

The most common case is a laptop, phone or app on the internet that tries to send mail to an outside address through your server on port 25 without authenticating. Postfix is correct to refuse it. Many guides tell you to add the client's network to mynetworks. Don't. Every IP in mynetworks can relay to anywhere without a password, and if you add a dynamic or shared range, you have built an open relay.

The safe fix is an authenticated submission service on port 587. First check that your Postfix build supports Dovecot SASL and that the Dovecot auth socket exists:

postconf -a
ls -l /var/spool/postfix/private/auth

postconf -a should list dovecot. Setting up the socket in Dovecot is covered in the Dovecot documentation listed in the sources. It has to be owned by postfix with mode 0660. Then point Postfix at it:

postconf -e 'smtpd_sasl_type = dovecot'
postconf -e 'smtpd_sasl_path = private/auth'

Enable the submission service in /etc/postfix/master.cf. Debian ships it commented out. Copy the chroot column from your existing smtp line:

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject

SASL stays off on port 25 (smtpd_sasl_auth_enable defaults to no), and port 587 refuses AUTH until STARTTLS has run. You need a working certificate for that. If you use Let's Encrypt, see Certbot renewals with a deploy hook on Debian 13 so Postfix picks up renewed certificates.

Warning: a syntax error in master.cf can stop Postfix from accepting mail. Run the check first and reload only if it prints nothing:

postfix check
postconf -M submission/inet
systemctl reload postfix
ss -ltnp | grep ':587'

Configure the client for port 587 with STARTTLS and normal password authentication, then test it as shown in the open-relay section below.

How relay restrictions are evaluated

You need to know the evaluation order to understand why a change opens or closes relaying. With compatibility_level 3.6 or higher, when a client sends RCPT TO, Postfix checks smtpd_relay_restrictions first and then smtpd_recipient_restrictions. The parameter smtpd_relay_before_recipient_restrictions controls this order. On a main.cf carried over with a lower compatibility_level, it defaults to no and the order is reversed. Check both values with postconf compatibility_level smtpd_relay_before_recipient_restrictions. Within each list, restrictions run left to right and the first permit or reject ends that list. Both lists must pass.

postconf compatibility_level smtpd_relay_before_recipient_restrictions
postconf -d smtpd_relay_restrictions
postconf mynetworks mynetworks_style smtpd_relay_restrictions smtpd_recipient_restrictions relay_domains
postconf -n

The upstream default for the relay list is:

smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination

In words: allow relaying from $mynetworks, then from SASL-authenticated clients, then refuse anything not addressed to a domain this server is responsible for. defer_unauth_destination rejects with a temporary code, which is why you see 454. reject_unauth_destination uses relay_domains_reject_code, which defaults to 554, a permanent error. The postconf(5) manual also says that one of the two lists must contain reject_unauth_destination, defer_unauth_destination, reject or defer, or Postfix refuses to receive mail.

These are the mistakes that create open relays:

  • A permit rule placed before defer_unauth_destination/reject_unauth_destination, such as a broad check_client_access map that returns OK.
  • mynetworks containing a public or provider-shared range, or 0.0.0.0/0.
  • Something in front of Postfix (a TCP proxy or port-forwarding helper) that makes every client appear with a local address that matches mynetworks.
  • An old main.cf that sets only smtpd_recipient_restrictions with a permissive order, copied from a pre-2.10 guide.

Keep mynetworks to loopback plus hosts you actually control, and check it after every change:

postconf mynetworks

Diagnosing deferred mail: Connection timed out on port 25

This line comes from Postfix's outbound smtp client. The mail is queued and Postfix will keep retrying. Look at the queue and one message:

postqueue -p
postcat -e -q 4F2A11C0F3A
postcat -h -q 4F2A11C0F3A

Then test whether you can reach any remote MX on port 25:

dig +short MX gmail.com
swaks --server gmail-smtp-in.l.google.com --port 25 --quit-after BANNER
nft list ruleset | grep -n 'dport 25'

If the banner never arrives and your own nftables ruleset doesn't block outbound 25, your provider is blocking it. Many cloud providers do this. Hetzner, for example, documents that it blocks ports 25 and 465 by default on all cloud servers and only unblocks them on request after the first month and first paid invoice. Port 587 is not blocked.

You have two options. You can ask the provider to unblock the port, or you can relay through a smarthost on 587. For the smarthost, put the credentials in a root-only file and set relayhost:

[smtp.relay.example]:587 user@example.org:app-password
chmod 600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
postconf -e 'relayhost = [smtp.relay.example]:587'
postconf -e 'smtp_sasl_auth_enable = yes'
postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
postconf -e 'smtp_sasl_security_options = noanonymous'
postconf -e 'smtp_tls_security_level = encrypt'
systemctl reload postfix
postqueue -i 4F2A11C0F3A
journalctl -u postfix -f

A successful retry logs status=sent with relay=smtp.relay.example.

Port 25 works, but mail still bounces or lands in spam

If the remote side answers but rejects or junks your mail, check reverse DNS and authentication. Google's sender guidelines require all senders to have SPF or DKIM and valid forward and reverse DNS (PTR) for the sending IP:

dig +short -x 203.0.113.10
dig +short A mail.example.org
dig +short TXT example.org
dig +short TXT default._domainkey.example.org

The PTR should return your mail hostname, and that hostname should resolve back to the same IP. You set the PTR in your hosting provider's panel, not in your own zone. Replace default with your DKIM selector.

Diagnosing “Sender address rejected: Domain not found”

This comes from reject_unknown_sender_domain. According to postconf(5), it rejects when Postfix is not the final destination for the sender address and the MAIL FROM domain has no DNS MX and no A record, or a malformed MX. The default unknown_address_reject_code is 450, so you see a 4xx code and the sender keeps retrying.

The usual cause is a server sending as www-data@web01.lan or root@localhost.localdomain because myorigin or the application's sender is set to the machine name. Check:

postconf myhostname myorigin mydomain
dig +short MX web01.lan
dig +short A web01.lan

No MX and no A means the domain is the problem. Fix the sender in the application, or set myorigin to a real domain you control:

postconf -e 'myorigin = example.org'
systemctl reload postfix
swaks --server 127.0.0.1 --from app@example.org --to ops@example.org --quit-after RCPT

If real domains also get this rejection now and then, your resolver is the problem rather than the sender. A local resolver that is failing or out of date can produce temporary lookup failures, and the BIND9 and Unbound security updates on Debian 13 article covers keeping that resolver patched.

Diagnosing SASL authentication failed

The string after the colon is base64 of the server's prompt, not the password:

echo 'UGFzc3dvcmQ6' | base64 -d
Password:

One failure from your user's IP is a wrong password. Hundreds from IPs you don't recognise are a brute-force attempt. Postfix only passes the credentials on, and Dovecot makes the decision, so read both logs:

journalctl -u postfix --since today | grep -c 'authentication failed'
journalctl -u postfix --since today | grep 'authentication failed' | grep -oE '\[[0-9a-f.:]+\]' | sort | uniq -c | sort -rn | head
journalctl -u dovecot --since '10 min ago'

Test a known-good account yourself. Leave out --auth-password so swaks prompts for it and the password stays out of your shell history:

swaks --server mail.example.org --port 587 --tls --auth LOGIN --auth-user alice@example.org --quit-after AUTH

If this fails as well, check the socket path from smtpd_sasl_path, its owner and mode, and whether Dovecot expects alice or alice@example.org. If the failures are attacks, the postfix-sasl jail described in Fail2ban on Debian 13 with nftables blocks the offending IPs.

Prove the server is not an open relay

Run this from a host outside mynetworks, such as another VPS or your laptop on mobile data, and only against servers you run. --quit-after RCPT ends the session before any message is sent.

swaks --server mail.example.org --port 25 --from test@example.net --to someone@example.com --quit-after RCPT
swaks --server mail.example.org --port 587 --tls --from test@example.net --to someone@example.com --quit-after RCPT

Both commands must fail at RCPT. Port 25 should answer with something like this:

<** 454 4.7.1 <someone@example.com>: Relay access denied

Port 587 should reject the unauthenticated client. Also check that delivery to your own domain still works on port 25, because that is how other servers reach you:

swaks --server mail.example.org --port 25 --from test@example.net --to postmaster@example.org --quit-after RCPT

If the outside-domain test returns 250 on port 25, stop and fix it before anything else. Go back to the evaluation order above and look for an early permit or a mynetworks that is too wide.

Takeaways

  • On Debian 13, read logs with journalctl -u postfix. The postfix@- unit no longer exists, and /var/log/mail.log exists only if you installed rsyslog.
  • Fix Relay access denied with SASL on port 587, never by widening mynetworks.
  • With compatibility_level 3.6 or higher, smtpd_relay_restrictions runs before smtpd_recipient_restrictions, and the first permit wins. No permit should come before defer_unauth_destination.
  • Port 25 timeouts on cloud servers are usually the provider's block. Use a smarthost on 587 or request an unblock.
  • Use real sender domains. Check MX/A with dig, plus PTR, SPF and DKIM for outbound mail.
  • After every change, run postfix check, systemctl reload postfix, the matching swaks test and the open-relay test from an outside host.

Sources

Comments