Generate and Validate Reverse DNS (PTR) Records for Mail Server IPs
A mail server with a missing or mismatched PTR record gets its mail deferred or rejected by the big providers. This guide gives you one small bash script that does two jobs: generate the PTR records you (or your IP owner) need to publish, and validate that what is published is correct — including the forward-confirmed reverse DNS (FCrDNS) check that mail servers actually care about.
Before you run this
This script does two read-only-ish things. In validate mode it only runs DNS queries (dig) and prints a report — it changes nothing. In generate mode it writes BIND-format PTR record lines to standard output (a text file if you redirect it); it does not push anything to any DNS server. So the script itself is safe to run.
Privileges: none. It needs no sudo and no root. Run it as your normal user. It only needs dig installed and outbound DNS.
The risk is not in the script — it is in what you do with the output. Publishing PTR records means editing a reverse zone (in-addr.arpa), which is almost always controlled by whoever owns the IP block: your ISP, your hosting provider, or your own BIND server if you were delegated the zone. If you edit a reverse zone and reload it with a mistake, you break reverse lookups for every address in it, which is hard to spot and directly hurts mail deliverability. That change is not automatic here and not undone by this script — treat the zone edit as the real, production-affecting step.
Read the script before you run it, and run validate against your IPs from a non-production shell first to see what the current state actually is before you change anything.
What I'm assuming
- Debian 12 or Ubuntu 22.04, bash 5.x.
digis available from thednsutilspackage (sudo apt install dnsutils). On RHEL/Alma the package isbind-utilsand the script is otherwise identical.- IPv4. IPv6 reverse records use the
.ip6.arpazone with the fully-expanded, nibble-reversed address, which is error-prone to build in shell — I handle IPv4 here and point you to the right place for IPv6 at the end. - You have a simple list of your mail server IPs and the hostname each one should reverse to.
The input file
Make a plain text file, one IP and its intended hostname per line. Lines starting with # are ignored. Replace these with your own values:
# ip hostname
203.0.113.25 mail.example.com
203.0.113.26 mx2.example.com
The hostname is what the PTR should point to, and — this matters for FCrDNS — that hostname must have an A record pointing back to the same IP.
The script
Save this as rdns.sh and chmod +x rdns.sh.
#!/usr/bin/env bash
# rdns.sh — generate and validate PTR records for mail server IPs (IPv4 only).
# generate: prints BIND PTR lines to stdout (publishes nothing).
# validate: checks each IP's PTR and forward-confirmed reverse DNS.
set -euo pipefail
usage() { echo "Usage: $0 {generate|validate} <hosts-file>" >&2; exit 2; }
[ $# -eq 2 ] || usage
mode="$1"; hosts="$2"
[ -r "$hosts" ] || { echo "Cannot read $hosts" >&2; exit 1; }
reverse_ipv4() { # 203.0.113.25 -> 25.113.0.203.in-addr.arpa
local ip="$1" a b c d
IFS='.' read -r a b c d <<< "$ip"
printf '%s.%s.%s.%s.in-addr.arpa' "$d" "$c" "$b" "$a"
}
generate() {
while read -r ip host _; do
[[ -z "$ip" || "$ip" == \#* ]] && continue
# Fully-qualified PTR record, valid in any reverse zone.
printf '%s.\tIN\tPTR\t%s.\n' "$(reverse_ipv4 "$ip")" "${host%.}"
done < "$hosts"
}
validate() {
local fail=0
while read -r ip host _; do
[[ -z "$ip" || "$ip" == \#* ]] && continue
host="${host%.}" # tolerate a trailing dot
ptr=$(dig +short -x "$ip" | sed 's/\.$//' | head -n1)
if [ -z "$ptr" ]; then
printf 'FAIL %-15s no PTR record\n' "$ip"; fail=1; continue
fi
if [ "${ptr,,}" != "${host,,}" ]; then
printf 'WARN %-15s PTR is %s, expected %s\n' "$ip" "$ptr" "$host"
fi
# FCrDNS: the PTR hostname must resolve forward to this same IP.
if dig +short A "$ptr" | grep -qx "$ip"; then
printf 'OK %-15s %s\n' "$ip" "$ptr"
else
printf 'FAIL %-15s %s does not forward-resolve to %s\n' "$ip" "$ptr" "$ip"
fail=1
fi
done < "$hosts"
return "$fail"
}
case "$mode" in
generate) generate ;;
validate) validate ;;
*) usage ;;
esac
A few notes on the non-obvious parts:
dig +short -x "$ip"does the reverse lookup and prints the PTR target (with a trailing dot, which thesedstrips).head -n1takes the first if there are several.- The FCrDNS check is the point of the whole exercise. A PTR that exists but whose hostname does not have an A record back to the same IP still fails the test that receiving mail servers apply.
grep -qx "$ip"requires an exact full-line match against the A records returned. ${ptr,,}lower-cases for a case-insensitive compare (bash 4+; fine on Debian 12).set -euo pipefailmakes the script stop on unset variables and real errors rather than limping on.
One edge to know: if your hostname sits behind a CNAME, dig +short A prints the chain and the final A, and grep -qx still matches the IP line — so that works, but keep mail PTR targets as plain A records to avoid surprises.
Running it
See the current reality first:
./rdns.sh validate hosts.txt
OK 203.0.113.25 mail.example.com
FAIL 203.0.113.26 no PTR record
Then generate the records to publish:
./rdns.sh generate hosts.txt > ptr-records.zone
cat ptr-records.zone
25.113.0.203.in-addr.arpa. IN PTR mail.example.com.
26.113.0.203.in-addr.arpa. IN PTR mx2.example.com.
What you do with ptr-records.zone depends on who owns the IPs:
- Hosting/VPS provider or ISP: paste the hostname for each IP into their control panel's "reverse DNS" / "PTR" field, or send them these lines. Most providers expose exactly one hostname box per IP.
- You run the delegated reverse zone in BIND: add these records into the matching
in-addr.arpazone file (it must already have its own SOA and NS records), bump the zone serial, and reload. The exactnamedreload step and zone-file layout are in the BIND 9 Administrator Reference Manual — follow it rather than anything I'd guess here.
Reverse DNS has long TTLs and provider-side caching, so give changes time — up to the zone's TTL — before you expect validation to pass.
Verifying and rolling back
Re-run the validator until every line reads OK:
./rdns.sh validate hosts.txt
You can confirm a single record by hand with the standard tools:
dig +short -x 203.0.113.25 # should print mail.example.com.
dig +short A mail.example.com # should print 203.0.113.25
host 203.0.113.25 # human-readable cross-check
Both directions must agree for FCrDNS to pass.
Rolling back: the script leaves nothing to undo — generate only wrote a local text file you can delete, and validate changed nothing. The reversible part is the zone edit. If you set a wrong PTR in a provider panel, change the field back (or blank it). If you edited a BIND reverse zone, remove or correct the line, bump the serial again, and reload. Keep a copy of the zone file before you edit it so the rollback is a straight file restore plus a reload.
For the authoritative query syntax, see the dig man page shipped with dnsutils/bind-utils; for FCrDNS expectations, the receiving-side requirements are documented by the major mailbox providers (for example Google's and Microsoft's sender guidelines).
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
A Bash Script to Test Mail Server Deliverability End to End
This script runs a series of read-only and send checks against a mail domain you control: it looks up MX, SPF, DKIM, and DMARC records with dig , tests the STARTTLS handshake on the submission port with openssl , and then sends one real test message through your server with swaks . Its purpose is to confirm, in one pass, that mail for your domain is configured to leave and arrive correctly.
Bulk-Update BIND Zone Files and Reload Safely with a Script
This script applies one literal find-and-replace across every zone file in a directory (for example, retiring an old IP or NS name), bumps each changed zone's SOA serial so slaves pick up the change, validates every touched zone, and reloads BIND only if all of them pass . If any zone fails validation it restores the backups and aborts before touching the running server.
Automate DNS Record Checks and Alert on Drift with dig
This is a read-only monitor . The script queries DNS with dig , compares each answer against a baseline file you control, and prints (and by cron, emails) a line for every record that no longer matches. It changes no DNS records, no zone files, and no configuration — the worst it can do is send you an email or write to a file you point it at. Because of that there is nothing to roll back on the DNS side.
Automate Cleanup of Stalled Messages in the Postfix Queue
Postfix is good at retrying mail on its own, but over time a busy server collects two kinds of junk in its spool: messages that have been deferred for days and will never deliver, and partial or misplaced files left behind by a crash or an abrupt restart. This guide sets up a safe, scheduled cleanup for both — using Postfix's own tools, not manual file surgery.



