My domains are currently hosted on 1984 FreeDNS nameservers. Even though this service is free to use, zones are hosted on 5 servers.

I've been monitoring that, whenever I update one of my zones, changes are properly propagated on their side.

For that, I've been using the following script, and Monit.

checksoa.sh

This script will automatically find authoritative nameservers for a zone, and compare the serial values for all of them. A hidden nameserver can also be declared to compare its serial with the public nameservers as well.

This script can also be found as a gist on Github. I'm more likely to remember to keep it up-to-date over there than here.

#!/bin/bash

# Variables
MINNS=1
declare -a AUTHNS

# Functions

# Name: usage
# Description: shows how the script should be used and exists
usage() {
  echo "Usage: $0 -z <zone> (-h <hidden_ns>)"
  exit 2
}

# Name: getns
# Description: add hidden nameserver to an array if declared;
#              then retrives authoritative nameservers for the zone to add them to the same array
getns() {
  [[ -n "$HIDDENNS" ]] && AUTHNS+=("$HIDDENNS")
  for PUBNS in $(/usr/bin/retry -t 5 -d 1,2,3 -- /usr/bin/dig -4 "$ZONE" NS +short +tcp +tries=1 +timeout=1); do
    AUTHNS+=("$PUBNS")
  done
}

# Name: getsoa
# Description: retrieve zone serial from all discovered authoritative nameservers
getsoa() {
  local i=0
  for NS in "${AUTHNS[@]}"; do
    SOA[i]=$(/usr/bin/retry -t 5 -d 1,2,3 -- /usr/bin/dig -4 @"$NS" "$ZONE" SOA +short +tcp +tries=1 +timeout=1 | /usr/bin/awk '{print $3}')
    i=$(( i + 1 ))
  done
}

# Name: soaoutput
# Description: generates script output
soaoutput() {
  local i=0
  for OUTPUT in "${AUTHNS[@]}"; do
    echo "$OUTPUT: ${SOA[i]}"
    i=$(( i + 1 ))
  done
}

# Main
while getopts ":z:h:" o; do
  case "${o}" in
    z)
      ZONE="${OPTARG}"
    ;;
    h)
      HIDDENNS="${OPTARG}"
    ;;
    :)
      echo "ERROR: Option -${OPTARG} requires an argument"
      usage
    ;;
    *)
      echo "ERROR: Invalid option -${OPTARG}"
      usage
    ;;
  esac
done

# -z is required
[[ -z "$ZONE" ]] && usage
# If the user declares a hidden, we expect at least 2 nameservers
[[ -n "$HIDDENNS" ]] && MINNS=2

getns

# Error out if we failed to retrive nameservers for the zone
if [[ ${#AUTHNS[@]} -lt $MINNS ]]; then
  echo "CRITICAL: Could not retrive authoritative NS for zone $ZONE"
  exit 2
fi

getsoa

for ALLSOA in "${SOA[@]}"; do
  # Error out if we failed to retrive one or more serials for the zone
  if ! [[ $ALLSOA =~ [[:digit:]]+ ]]; then
    echo "CRITICAL: Could not fetch SOA on at least one DNS server for zone $ZONE"
    exit 2
  fi
  # Compare all serials to the first one we retrived
  if [[ $ALLSOA != "${SOA[0]}" ]]; then
    INCONSISTENTSERIAL="true"
  fi
done

if [[ -n $INCONSISTENTSERIAL ]]; then
  echo "WARNING: Serials are inconsistent for zone $ZONE"
  soaoutput
  exit 1
else
  echo "OK: Serials are consistent for zone $ZONE"
  soaoutput
  exit 0
fi

Here's what its output looks like :

# zone serials are consistent
./checksoa.sh -z cloudflare.com
OK: Serials are consistent for zone cloudflare.com
ns5.cloudflare.com.: 2363241910
ns4.cloudflare.com.: 2363241910
ns7.cloudflare.com.: 2363241910
ns6.cloudflare.com.: 2363241910
ns3.cloudflare.com.: 2363241910
# zone serials aren't consistent
./checksoa.sh -z google.com
WARNING: Serials are inconsistent for zone google.com
ns1.google.com.: 718790660
ns3.google.com.: 718790660
ns2.google.com.: 719232139
ns4.google.com.: 718790660

No idea why google.com serials aren't the same between all their nameservers, but you get the idea.

Integration with monit

This is the configuration I use in monit :

check program captainarkdotnet
  with path "/usr/local/bin/checksoa.sh -z captainark.net"
  every 10 cycles
  if status != 0 then alert

Note that I run the script only once every 10 monit cycles, as it generates quite a few DNS queries, and zone transfers can take a bit of time. For me, a monit cycle being 30 secondes, the script only runs once every 5 minutes.

Conclusion

This script should be usable as-is with NRPE as well, and any other monitoring solutions I'm not aware of as long as they rely on return codes and stdout messages.

Note that it won't fully work if one or more of the declared nameservers for the tested zone uses round-robin DNS (multiple A/AAAA records for a single name), as it will only check one of them.

Feel free to let me know on any of my socials if you end up using it, or if you improve upon it!