Shore Up
LinuxMail

Generate and Validate Reverse DNS (PTR) Records for Mail Server IPs

Ketan Aagja9 min read
No ratings yet

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.
  • dig is available from the dnsutils package (sudo apt install dnsutils). On RHEL/Alma the package is bind-utils and the script is otherwise identical.
  • IPv4. IPv6 reverse records use the .ip6.arpa zone 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 the sed strips). head -n1 takes 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 pipefail makes 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.arpa zone file (it must already have its own SOA and NS records), bump the zone serial, and reload. The exact named reload 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).

Written by
Ketan Aagja

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.

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.

10 min read

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.

9 min read

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.

10 min read

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.

9 min read