
Bulk-Update BIND Zone Files and Reload Safely with a Script
Before you run this
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.
It needs root — it writes into /etc/bind and calls rndc, so run it with sudo. It changes and, briefly, could disrupt DNS: an edited zone that reloads with a wrong record answers wrongly for everything that depends on it. The edits themselves are reversible — the script copies every zone file into a timestamped backup directory first, and I show the restore command at the end — but a bad answer served in the meantime is not something you can un-serve to a resolver that already cached it.
Read the script before you run it. Test it on a lab or hidden-master server, or point ZONE_DIR at a copy of your zone files first and inspect the result, before you run it against production. Keep the default DRY_RUN=1 for the first pass so it only shows you what it would change.
This assumes plain (unsigned) zones, or the unsigned source files of inline-signed zones. If you edit DNSSEC-signed zone files directly, or manage zones with dynamic updates (nsupdate), do not hand-edit them — those have their own tooling (nsdiff/nsupdate, rndc signing). Check the BIND ARM for those cases.
What I'm assuming
Debian 12 / Ubuntu 22.04, package
bind9, utilities frombind9-utils(named-checkconf,named-checkzonein$PATH).Zone files live in one directory, named
db.<zone>— e.g./etc/bind/zones/db.example.com. The script derives the zone name from that filename, so this convention matters.Each zone's SOA serial is written in
YYYYMMDDnnform on a line carrying a; serialcomment, which is the Debian default and the common convention:2024031501 ; serialrndcis already working (Debian sets uprndc.keyon install; test withsudo rndc status).
If your service unit is named.service rather than the bind9.service alias, that only matters for start/stop — we reload through rndc, not systemd.
The script
Save this as bulk-zone-update.sh, edit the four settings at the top, and run it once with DRY_RUN=1 before flipping it to 0.
#!/usr/bin/env bash
#
# bulk-zone-update.sh — apply a literal find/replace across BIND zone files,
# bump each changed zone's SOA serial, validate, and reload only if all pass.
#
set -euo pipefail
### ---- edit these ----
ZONE_DIR="/etc/bind/zones" # directory holding your db.<zone> files
OLD="10.0.0.5" # text to find (treated as a sed pattern)
NEW="10.0.0.6" # text to write (treated as a sed replacement)
DRY_RUN=1 # 1 = preview only; 0 = actually change + reload
### --------------------
BACKUP_DIR="/var/backups/bind-zones/$(date +%Y%m%d-%H%M%S)"
CHANGED=()
# db.example.com -> example.com
zone_name() { basename "$1" | sed -E 's/^db\.//'; }
bump_serial() {
local file="$1" today current new
today=$(date +%Y%m%d)
# serial must be a 10-digit number on a line marked "; serial"
current=$(grep -iE ';[[:space:]]*serial' "$file" | grep -oE '[0-9]{10}' | head -n1)
if [ -z "$current" ]; then
echo " ! no 'NNNNNNNNNN ; serial' line in $file — bump it by hand" >&2
return 1
fi
if [ "$current" -ge "${today}00" ]; then
new=$((current + 1)) # already bumped today: just increment
else
new="${today}00" # first change today
fi
sed -i -E "s/\b${current}\b([[:space:]]*;[[:space:]]*[Ss]erial)/${new}\1/" "$file"
echo " serial $current -> $new"
}
mkdir -p "$BACKUP_DIR"
cp -a "$ZONE_DIR"/db.* "$BACKUP_DIR"/
echo "Backup: $BACKUP_DIR"
# find every zone that contains OLD
for f in "$ZONE_DIR"/db.*; do
[ -f "$f" ] || continue
if grep -qF "$OLD" "$f"; then
echo "Match in $(basename "$f"):"
grep -nF "$OLD" "$f" | sed 's/^/ /'
CHANGED+=("$f")
fi
done
if [ "${#CHANGED[@]}" -eq 0 ]; then
echo "No files contain '$OLD'. Nothing to do."; exit 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY RUN — nothing changed, BIND not reloaded."; exit 0
fi
# apply the change and bump serials
for f in "${CHANGED[@]}"; do
echo "Updating $(basename "$f")"
sed -i "s|${OLD}|${NEW}|g" "$f"
bump_serial "$f"
done
# validate before touching the running server
echo "Checking named.conf..."
named-checkconf
for f in "${CHANGED[@]}"; do
z=$(zone_name "$f")
echo " named-checkzone $z"
if ! named-checkzone "$z" "$f" >/dev/null; then
echo "VALIDATION FAILED for $z — restoring backups, NOT reloading." >&2
cp -a "$BACKUP_DIR"/db.* "$ZONE_DIR"/
exit 1
fi
done
echo "All zones valid. Reloading BIND..."
rndc reload
echo "Done. Backup kept at $BACKUP_DIR"
Two honest cautions about the matching. OLD and NEW go straight into sed, so OLD is a regular expression and NEW is a replacement: an IP like 10.0.0.5 works because the literal dots also match dots, but if your search text contains ., /, &, or \, escape it or the change can over-match. When in doubt, keep DRY_RUN=1 and read what it reports before committing. The script only touches files that already contain OLD, so unrelated zones are left alone.
Verify it worked
First, confirm BIND actually loaded the new serial for a changed zone. rndc zonestatus shows the loaded serial and the last load time:
sudo rndc zonestatus example.com
Then query the local server directly and check both the record and the SOA serial:
dig @127.0.0.1 example.com SOA +short # serial should be the new value
dig @127.0.0.1 host.example.com A +short # should return NEW, not OLD
If anything looks wrong, check the log for load errors:
sudo journalctl -u named --since "5 min ago" # or bind9.service, or /var/log/syslog
A silently unchanged answer usually means the serial didn't advance, so BIND kept the old copy — check that the ; serial line matched the pattern.
Undo
Because every zone file was copied first, rolling back is a file copy and a reload. Use the timestamped directory the script printed:
sudo cp -a /var/backups/bind-zones/YYYYMMDD-HHMMSS/db.* /etc/bind/zones/
sudo named-checkconf
sudo rndc reload
Replace YYYYMMDD-HHMMSS with your actual backup directory. Restoring reverts the serials too, which is fine on your master, but if slaves already pulled the newer (higher) serial they will not step backwards to a lower one — you would need to bump the serial forward again after restoring the old records. That's the one place a "rollback" isn't purely mechanical, so keep it in mind on a zone with live secondaries.
For the exact meaning of rndc reload, named-checkzone, and serial handling, see the BIND 9 Administrator Reference Manual (ISC's official documentation) rather than trusting any single flag from memory.
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.
