maborak.com~/blog/spamhaus-sender-guidelines-helo-fcrdnssoftware & infrastructure engineering
Back to all articles

Spamhaus will enforce HELO and reverse DNS from November 2026: check every sending IP

Spamhaus now requires forward-confirmed reverse DNS and a HELO name that resolves to the sending IP. A stock Postfix container announces EHLO localhost.

By the Maborak engineering team · · 8 min read

Spamhaus published its Sender Guidelines on 21 October 2026 and announced that, starting in November 2026, it will gradually enforce three DNS rules for every sending IP: a reverse DNS (PTR) record, a matching forward A record, and a valid HELO. The guidelines say that failing these can get an IP listed on the CSS (spam sources) or XBL (compromised hosts) zones. Many mail servers already fail one of them without anyone noticing: a stock Postfix install in a Debian container announces itself as localhost.

Checked 29 October 2026. Requirements and dates are from the Spamhaus announcement and guidelines linked under Sources and are marked [vendor]. The DNS cases and the Postfix behaviour were run in containers on this machine and are marked [reproduced].

Background

The rules are not new. RFC 5321 already requires the EHLO argument to be the client’s fully qualified domain name, or an address literal when it has none. Receivers have long distrusted IPs without reverse DNS. What changes is that Spamhaus now states the rules in one place, describes them as aligned with mailbox providers’ existing guidance, and is about to enforce them through its blocklists. [vendor]

The guidelines list five minimum requirements for every sender: [vendor]

  1. Confirmed opt-in for all subscribers.
  2. Forward-confirmed reverse DNS (FCrDNS) for every sending IP.
  3. A HELO/EHLO hostname that resolves to the sending IP and stays the same across connections.
  4. For IPv6, a sending address inside an allocation of /64 or larger, with valid reverse DNS.
  5. SPF and DMARC published for every sending domain, and DKIM on every message, aligned with the From domain.

The November enforcement covers items 2 and 3. The announcement names an invalid HELO, a missing PTR and a missing matching A record as the most common DNS reasons for listings. [vendor]

Mechanics

The two checks are separate, and an IP has to pass both:

FCrDNS:  sending IP ──PTR──▶ mail.example.com ──A/AAAA──▶ must include the sending IP
HELO:    EHLO mail.example.com               ──A/AAAA──▶ must include the sending IP

The PTR must name a fully qualified hostname, and that hostname must resolve back to the same IP. The HELO name is checked on its own: it must resolve to the connecting IP, and it must not change between connections from that IP, because several HELO names pointing at different addresses look like a proxy or malware. The guidelines recommend, but do not require, that the HELO and the PTR name be the same. [vendor]

They also list values to avoid: localhost and other placeholders, the generic names hosting providers assign (static-203-0-113-25.hosting.example.net), a HELO resolving to hosts that do not send mail, and impersonating a mailbox provider’s HELO. A changed IP or HELO name with no sending history is treated as a new asset that needs warming up again. [vendor]

Impact

The rules apply to anyone running a mail server: in-house Postfix or Exchange, application servers that send directly instead of through a relay, and ESPs or shared platforms. Typical ways to fail:

  • A new VPS or cloud instance whose PTR is still the provider’s default, or was never set. Only the owner of the IP space can set the PTR, usually through the provider’s control panel.
  • A PTR that points at a hostname whose A record moved, for example after a migration to a new IP that left the old PTR in place.
  • A HELO taken from the machine’s hostname. Containers and cloud images often have a hostname that is not an FQDN, or one that resolves to nothing.
  • Several IPs sharing one HELO name that resolves to only one of them.

A listing in CSS or XBL affects every receiver that queries Spamhaus, not one mailbox provider. The guidelines also say an IP without rDNS is treated as suspicious in its reputation even before a listing. [vendor]

Probe

Two probes: a DNS checker run against good and broken zones, and a look at what Postfix actually announces.

DNS checker. It needs dig (BIND 9.20.30 here). Run it with a sending IP and the HELO name the server uses; the third argument picks a DNS server, otherwise the system resolver is used.

#!/bin/sh
# usage: fcrdns-check.sh SENDING_IP HELO_NAME [DNS_SERVER]
ip=$1 helo=$2 ns=${3:+@$3}
fail=0
addrs() { { dig +short $ns A "$1"; dig +short $ns AAAA "$1"; } | grep -v '\.$'; }
ptr=$(dig +short $ns -x "$ip" | head -1); ptr=${ptr%.}
if [ -z "$ptr" ]; then echo "  FAIL rDNS: no PTR for $ip"; fail=1
elif addrs "$ptr" | grep -qxF "$ip"; then echo "  ok   rDNS: $ip -> $ptr -> $ip"
else echo "  FAIL rDNS: $ip -> $ptr -> $(addrs "$ptr" | tr '\n' ' ')(not $ip)"; fail=1; fi
case $helo in
  *.*) if addrs "$helo" | grep -qxF "$ip"; then echo "  ok   HELO: $helo -> $ip"
       else echo "  FAIL HELO: $helo -> $(addrs "$helo" | tr '\n' ' ')(not $ip)"; fail=1; fi ;;
  *)   echo "  FAIL HELO: \"$helo\" is not a fully qualified name"; fail=1 ;;
esac
[ -n "$ptr" ] && [ "$ptr" != "$helo" ] && echo "  note HELO differs from PTR (recommended to match)"
exit $fail

To test it, a CoreDNS 1.12.4 container served a forward zone example.test and the reverse zone for 172.30.0.0/24, each case changing one record: [reproduced]

== case A: ip=172.30.0.10 helo=mail.example.test        (PTR and A both correct)
  ok   rDNS: 172.30.0.10 -> mail.example.test -> 172.30.0.10
  ok   HELO: mail.example.test -> 172.30.0.10
== case B: ip=172.30.0.11 helo=mail.example.test        (no PTR)
  FAIL rDNS: no PTR for 172.30.0.11
  FAIL HELO: mail.example.test -> 172.30.0.10 (not 172.30.0.11)
== case C: ip=172.30.0.12 helo=mail2.example.test       (PTR target's A is another IP)
  FAIL rDNS: 172.30.0.12 -> mail2.example.test -> 172.30.0.99 (not 172.30.0.12)
  FAIL HELO: mail2.example.test -> 172.30.0.99 (not 172.30.0.12)
== case D: ip=172.30.0.13 helo=localhost                 (rDNS fine, placeholder HELO)
  ok   rDNS: 172.30.0.13 -> mail3.example.test -> 172.30.0.13
  FAIL HELO: "localhost" is not a fully qualified name
== case E: ip=172.30.0.10 helo=mail3.example.test       (HELO of another server)
  ok   rDNS: 172.30.0.10 -> mail.example.test -> 172.30.0.10
  FAIL HELO: mail3.example.test -> 172.30.0.13 (not 172.30.0.10)

Case E is the shared-HELO mistake: rDNS passes, so a check that looks only at the PTR misses it.

What Postfix announces. A debian:trixie-slim image with apt-get install postfix (Postfix 3.10.13, package 3.10.13-0+deb13u1) relayed one message to a small SMTP sink that logs the EHLO line. The second run sets smtp_helo_name: [reproduced]

== stock config:
postfix 3.10.13 | hostname=f15ebd8a64a2 | myhostname=localhost | smtp_helo_name=$myhostname
== with smtp_helo_name=mail.example.test:
postfix 3.10.13 | hostname=a56e598dcb5f | myhostname=localhost | smtp_helo_name=mail.example.test
== sink log:
172.30.0.10 announced: EHLO localhost
172.30.0.10 announced: EHLO mail.example.test

Postfix sends smtp_helo_name, which defaults to $myhostname. Debian’s postfix.postinst writes myhostname into main.cf from hostname --fqdn when the package is installed, and in this image build that produced localhost. Because the value is written into main.cf, starting the container with --hostname mail.example.test does not change it: postconf -h myhostname still returned localhost. Postfix’s built-in default would have been worse for this purpose: <container-id>.localdomain. The result is specific to this image and install method; other images set the hostname differently, so check yours with the commands below.

Mitigation

  1. Set the PTR for every sending IP to an FQDN on your own domain, through whoever controls the IP space (cloud console, hosting panel or the ISP). Then publish an A (or AAAA) record for that name pointing back to the IP.
  2. Set the HELO explicitly, not from the machine’s hostname. In Postfix, postconf -e 'smtp_helo_name = mail.example.com' (or set myhostname, which also drives the banner), then postfix reload. In Exim, helo_data on the SMTP transport; in an application that speaks SMTP itself, the client’s EHLO/local-hostname setting.
  3. One HELO per IP. With several sending IPs, give each its own name and records, or make sure the shared name resolves to every one of them. Postfix can vary it per transport with -o smtp_helo_name= in master.cf.
  4. IPv6: if the server has a global IPv6 address and a route, it may send over IPv6. Either give that address the same PTR and AAAA treatment and a /64 or larger allocation, or restrict outbound SMTP to IPv4 (inet_protocols = ipv4 in Postfix).
  5. Treat a renamed HELO or a new IP as new. The guidelines ask for a warm-up when either changes without sending history, so change them during a quiet period and ramp volume slowly.

Rollback is the previous main.cf and DNS records; nothing here is irreversible, but DNS TTLs delay both the fix and a revert.

Verification

A correct main.cf is not proof of what goes out. Check what is configured, what DNS says, and what a receiver saw.

# what Postfix will announce
postconf -h smtp_helo_name myhostname

# DNS, from outside your network, for each sending IP
./fcrdns-check.sh 203.0.113.25 mail.example.com 1.1.1.1

Then send a message from the server to a mailbox you control and read the topmost Received: header added by the receiving server: it records the HELO the server announced, usually next to the PTR name and IP. Spamhaus also offers a HELO/DNS checker on the guidelines page and a mail-based check: a message sent to [email protected] is answered with the HELO it saw. [vendor]

Sources

Back to all articles

Technologies we work with

  • Argo CD
  • Kubernetes
  • Docker
  • Podman
  • Ansible
  • Jenkins
  • Chef
  • Proxmox VE
  • Google Cloud
  • Linux
  • Red Hat
  • CentOS
  • NGINX
  • Apache
  • Traefik
  • Prometheus
  • Grafana
  • PostgreSQL
  • MySQL
  • MariaDB
  • Redis
  • etcd
  • Qdrant
  • Elasticsearch
  • Logstash
  • Kibana
  • PagerDuty
  • Go
  • Python
  • FastAPI
  • PHP
  • TypeScript
  • React