
Automate DNS Record Checks and Alert on Drift with dig
Before you run this
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.
Privileges: none special. dig runs fine as an unprivileged user, and so does this script. The only place root matters is where you choose to store things: if you put the baseline in /etc or install the script into /usr/local/bin, you need write access to those paths (use sudo for the copy, not for running the job). Run the actual check as a normal user or a dedicated service account.
Test first: read the script before you run it, and run it by hand against your own domain a few times before you put it in cron. The one file that carries risk is the baseline (expected-records.txt): if you generate it from a moment when your DNS was already wrong, the script will happily treat "wrong" as "correct" and stay silent. Build the baseline deliberately, eyeball it, and only then trust it.
Assumptions. Debian 12 or Ubuntu 22.04, bash, and dig from the bind9-dnsutils package (on older releases the package is dnsutils; the dig binary is identical). Alerting is done the boring, reliable way — cron mails you any output a job produces — so a working local mail path (MAILTO plus something like msmtp or a smarthost) is assumed. If you don't have mail, the script still works; you just read its output or its log yourself.
Install dig
sudo apt update
sudo apt install bind9-dnsutils # 'dnsutils' on Debian 11/Ubuntu 20.04 and earlier
Confirm it's there:
dig -v
The idea in one paragraph
For each record you care about, you ask a nameserver "what do you answer for this?" and compare that answer to a value you saved earlier. The two decisions worth making up front are (1) who you ask and (2) what counts as the same answer. On who: querying the domain's authoritative nameserver tells you the zone itself changed; querying a public resolver (1.1.1.1, 8.8.8.8) tells you what the outside world currently sees, cache and all. Pick one on purpose. On sameness: a name can return several values (multiple A records, several MX hosts) in any order, so we sort the answers before comparing — otherwise you'll get false drift alerts from ordering alone.
The baseline file
Plain text, one record per line: type, name, then the expected answer as a sorted, comma-separated string. Lines starting with # are comments.
# expected-records.txt — the known-good answers
# TYPE NAME EXPECTED (sorted, comma-separated)
A example.com 203.0.113.10
A www.example.com 203.0.113.10
MX example.com 10 mail.example.com.
TXT example.com "v=spf1 include:_spf.example.com -all"
NS example.com ns1.example.com.,ns2.example.com.
Replace example.com and the IPs with your own. Note that dig +short returns MX with its priority (10 mail.example.com.), names with a trailing dot, and TXT values wrapped in quotes — so your baseline must match that formatting exactly. The easiest way to get it right is to let dig generate it, then read it back.
Generate the baseline from live DNS
Do this only when you're confident the current answers are correct.
RESOLVER=1.1.1.1 # or your authoritative NS, e.g. ns1.example.com
for spec in "A example.com" "A www.example.com" "MX example.com" \
"TXT example.com" "NS example.com"; do
read -r type name <<< "$spec"
# sort so multi-value answers are stable; paste joins them with commas
answer=$(dig +short "@${RESOLVER}" "$name" "$type" | sort | paste -sd, -)
printf '%s\t%s\t%s\n' "$type" "$name" "$answer"
done | tee expected-records.txt
Open expected-records.txt, confirm every line is what you expect, and fix anything that looks wrong at the source (in DNS) before trusting the file.
The check script
#!/usr/bin/env bash
# dns-drift-check.sh — compare live DNS answers to a baseline and report drift.
# Read-only: it queries DNS only. Prints one block per drifted record.
# Exit 0 = no drift, 1 = drift found, 2 = usage/setup error.
set -euo pipefail
RESOLVER="1.1.1.1" # who to ask; use your authoritative NS to catch zone edits
EXPECTED_FILE="${1:-./expected-records.txt}"
[[ -r "$EXPECTED_FILE" ]] || { echo "Cannot read baseline: $EXPECTED_FILE" >&2; exit 2; }
drift=0
# Read TYPE, NAME, and the rest of the line as the expected value.
while read -r type name expected; do
# Skip blank lines and comments.
[[ -z "${type:-}" || "$type" == \#* ]] && continue
# +short returns just the answer data; sort + join to compare stably.
actual=$(dig +short "@${RESOLVER}" "$name" "$type" | sort | paste -sd, -)
if [[ "$actual" != "$expected" ]]; then
echo "DRIFT: $name ($type)"
echo " expected: $expected"
echo " actual: ${actual:-<no answer>}"
echo
drift=1
fi
done < "$EXPECTED_FILE"
exit "$drift"
A few honest caveats about dig +short, none of them exotic:
- It prints answers with the formatting shown above (trailing dots, quoted TXT). Your baseline must match that formatting — which is why generating the baseline with the same tool is the safe path.
- For TXT records split into multiple strings,
digshows them as separate quoted chunks. If you use long DKIM keys, compare those by hand or store the exact+shortoutput. - The script compares the full set of answers. If a name legitimately returns a rotating set (some CDNs), it's a poor candidate for this kind of exact-match monitoring.
Install and schedule it
sudo install -m 0755 dns-drift-check.sh /usr/local/bin/dns-drift-check.sh
sudo install -d -m 0755 /etc/dns-drift
sudo install -m 0644 expected-records.txt /etc/dns-drift/expected-records.txt
Because the script prints nothing when everything matches and prints details when it doesn't, cron's built-in mail behaviour is the alerting: a silent run sends no mail, a drifted run mails you the block. Edit your crontab (crontab -e) and add:
MAILTO=you@example.com
# every 15 minutes; only produces output (and therefore mail) on drift
*/15 * * * * /usr/local/bin/dns-drift-check.sh /etc/dns-drift/expected-records.txt
If you don't have working mail, drop the MAILTO and redirect to a log instead, then watch that log:
*/15 * * * * /usr/local/bin/dns-drift-check.sh /etc/dns-drift/expected-records.txt >> /var/log/dns-drift.log 2>&1
(Make sure the account running cron can write that log file.)
Verify it actually catches drift
Don't take the script's word that "no output" means "working." Prove it detects a mismatch:
# Run it clean — should print nothing and exit 0.
/usr/local/bin/dns-drift-check.sh /etc/dns-drift/expected-records.txt; echo "exit=$?"
# Now break one line on purpose in a COPY, and confirm it's caught.
cp /etc/dns-drift/expected-records.txt /tmp/test-baseline.txt
sed -i 's/203\.0\.113\.10/203.0.113.99/' /tmp/test-baseline.txt # a value that won't match live DNS
/usr/local/bin/dns-drift-check.sh /tmp/test-baseline.txt; echo "exit=$?"
The second run should print a DRIFT: block and report exit=1. Delete the temp file when you're done. That single test confirms both halves: the comparison fires, and the exit code and output are what cron will act on.
Undoing it
There's nothing to reverse in DNS — the script never wrote anything there. To remove the monitor itself:
crontab -e # delete the dns-drift line
sudo rm /usr/local/bin/dns-drift-check.sh
sudo rm -r /etc/dns-drift
sudo rm -f /var/log/dns-drift.log # only if you used the log approach
When your DNS legitimately changes — you move a host, add an MX — you're not undoing anything; you're re-baselining. Re-run the generator, review the new expected-records.txt, and reinstall it. That review step is the whole point: it forces every change to pass through your eyes once.
For the exact behaviour of dig's output modes and query options, see the dig manual page (man dig) and the BIND 9 documentation from ISC.
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.
