Find DMARC DKIM SPF Nameservers With One Script
Published: Aug 28, 2025
If you are setting up Microsoft Office or Google Workspace, you’re constantly having to check DMARC, DKIM and SPF values and then check if the edited values have propagated. Here’s a script that does just that. It even saves main zone records in a new folder with a symlink of the latest output. All output is saved.
This script was developed with a large language model.
Features
- Displays DKIM, SPF, DMARC and nameserver values.
- Polls against Cloudflare and Google servers.
- Saves a little bit more output to a folder with the domain name.
- Symlnks the latest file with ’latest’ in the name.
This script is tested on WSL running Ubuntu 24. Please run it at your own discretion.
How to run
- Make your script executable. (chmod +x scriptname)
- Enter like this on the terminal: ./dns-check.sh example.com
#!/usr/bin/env bash
# dns-check.sh (summary + zone snapshot only)
# Usage:
# ./dns-check.sh [-r 1.1.1.1] domain.com
# Env:
# RESOLVER=1.1.1.1
# RESOLVERS=1.1.1.1,8.8.8.8,9.9.9.9
set -euo pipefail
# ----- args -----
resolver_cli=""
if [[ "${1:-}" == "-r" || "${1:-}" == "--resolver" ]]; then
resolver_cli="${2:-}"; shift 2 || true
fi
domain="${1:-}"
if [[ -z "${domain}" ]]; then
echo "Usage: $0 [-r 1.1.1.1] domain.com"; exit 1
fi
# ----- resolvers -----
if [[ -n "${resolver_cli}" ]]; then
resolvers=("${resolver_cli}")
elif [[ -n "${RESOLVER:-}" ]]; then
resolvers=("${RESOLVER}")
elif [[ -n "${RESOLVERS:-}" ]]; then
IFS=',' read -r -a resolvers <<< "$RESOLVERS"
else
resolvers=(1.1.1.1 8.8.8.8 9.9.9.9)
fi
# ----- deps -----
if ! command -v dig >/dev/null 2>&1; then
echo "Installing dnsutils..."; sudo apt update -y && sudo apt install -y dnsutils
fi
# ----- dig helpers (caching + single active resolver) -----
dig_quick_opts="+short +time=1 +tries=1"
dig_dump_opts="+noall +answer +time=1 +tries=1"
ACTIVE_RESOLVER=""
choose_resolver() {
for r in "${resolvers[@]}"; do
if dig ${dig_quick_opts} @"$r" NS . >/dev/null 2>&1; then
ACTIVE_RESOLVER="$r"; return
fi
done
ACTIVE_RESOLVER="${resolvers[0]}"
}
choose_resolver
declare -A CACHE_Q # key "TYPE|NAME" -> +short text
declare -A CACHE_F # key "TYPE|NAME" -> +answer text
_q_raw() { # raw +short
local rr="${1-}" name="${2-}"
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
dig ${dig_quick_opts} @"$ACTIVE_RESOLVER" "$rr" "$name" 2>/dev/null || true
}
_f_raw() { # raw +answer
local rr="${1-}" name="${2-}"
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
dig ${dig_dump_opts} @"$ACTIVE_RESOLVER" "$rr" "$name" 2>/dev/null || true
}
q() { # cached +short
local rr="${1-}" name="${2-}" k
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
k="${rr}|${name}"
if [[ -z "${CACHE_Q[$k]+x}" ]]; then
CACHE_Q[$k]="$(_q_raw "$rr" "$name")"
fi
printf "%s" "${CACHE_Q[$k]}"
}
f() { # cached +answer
local rr="${1-}" name="${2-}" k
[[ -z "$rr" || -z "$name" ]] && { printf ""; return 0; }
k="${rr}|${name}"
if [[ -z "${CACHE_F[$k]+x}" ]]; then
CACHE_F[$k]="$(_f_raw "$rr" "$name")"
fi
printf "%s" "${CACHE_F[$k]}"
}
# ----- TXT helpers -----
get_txt_raw_lines() { q TXT "$1" || true; }
get_txt_records() {
local raw line
raw="$(get_txt_raw_lines "$1")"
[[ -z "$raw" ]] && return 0
while IFS= read -r line; do
printf "%s\n" "$line" | sed 's/^"//; s/"$//' | tr -d '"'
done <<< "$raw"
}
wrap_txt() { fold -s -w 70 | sed 's/^/ /'; }
ttl_of() { # parse TTL from cached full answer
local rr="${1-}" name="${2-}" ttl
[[ -z "$rr" || -z "$name" ]] && { printf "Auto"; return 0; }
ttl="$(f "$rr" "$name" | awk 'NR==1{print $2}')"
[[ -n "$ttl" ]] && printf "%s" "$ttl" || printf "Auto"
}
# ----- paths -----
mkdir -p "$domain" "names"
ts="$(date +"%Y-%m-%d_%H-%M-%S")"
zonesfile="$domain/${domain}_${ts}.zones"
zones_latest="$domain/latest.zones"
# ====== HUMAN READABLE SUMMARY ======
echo "Checking DNS records for: $domain"
echo "Resolver: $ACTIVE_RESOLVER (chosen from: ${resolvers[*]})"
echo "--------------------------------------------------"
# SPF
echo "[SPF]"
mapfile -t spf_list < <(get_txt_records "$domain" | awk 'BEGIN{IGNORECASE=1} /^v=spf1/ {print}')
if (( ${#spf_list[@]} == 0 )); then echo "No SPF record found"
elif (( ${#spf_list[@]} == 1 )); then echo "${spf_list[0]}"
else
echo "⚠️ Multiple SPF records found (${#spf_list[@]}). Merge into one:"
for i in "${!spf_list[@]}"; do printf " [%d] %s\n" "$((i+1))" "${spf_list[$i]}"; done
fi
echo
# DMARC
echo "[DMARC]"
mapfile -t dmarc_list < <(get_txt_records "_dmarc.$domain" | awk 'BEGIN{IGNORECASE=1} /^v=DMARC1/ {print}')
if (( ${#dmarc_list[@]} == 0 )); then echo "No DMARC record found"
elif (( ${#dmarc_list[@]} == 1 )); then echo "${dmarc_list[0]}"
else
echo "⚠️ Multiple DMARC records found (${#dmarc_list[@]}). Merge into a single record."
for i in "${!dmarc_list[@]}"; do printf " [%d] %s\n" "$((i+1))" "${dmarc_list[$i]}"; done
fi
echo
# MX
echo "[MX]"
mx="$(q MX "$domain" || true)"
[[ -n "$mx" ]] && echo "$mx" | sort -n -k1 || echo "No MX records found"
echo
# NS
echo "[Nameservers]"
ns="$(q NS "$domain" || true)"
[[ -n "$ns" ]] && echo "$ns" | sort || echo "No NS records found"
echo
# DKIM
echo "[DKIM]"
policy_recs="$(get_txt_records "_domainkey.$domain" || true)"
if [[ -n "$policy_recs" ]]; then
echo "Policy (_domainkey.$domain):"
while IFS= read -r pline; do echo " $pline"; done <<< "$policy_recs"
echo
fi
selectors=(s1 s2 selector1 selector2 default google mail k1 k2 mx dkim)
found_txt=0; found_cname=0; any_found=false
dkim_found_names=()
for sel in "${selectors[@]}"; do
rec="$sel._domainkey.$domain"
txt_lines="$(get_txt_records "$rec" || true)"
cname="$(q CNAME "$rec" || true)"
if [[ -n "$cname" ]]; then
any_found=true; ((found_cname++)) || true
echo "Selector: $sel"; echo " Record: $rec"; echo " CNAME → $cname"; echo
dkim_found_names+=("$rec")
fi
if [[ -n "$txt_lines" ]]; then
while IFS= read -r tline; do
if echo "$tline" | grep -qi '^v=DKIM1'; then
any_found=true; ((found_txt++)) || true
echo "Selector: $sel"; echo " Record: $rec"; echo " TXT →"
echo "$tline" | wrap_txt; echo
dkim_found_names+=("$rec")
fi
done <<< "$txt_lines"
fi
done
[[ "$any_found" != true ]] && echo "No DKIM records found with common selectors."
echo "Summary: DKIM TXT=$found_txt, CNAME=$found_cname"
echo "--------------------------------------------------"
# ===== ZONE SNAPSHOT (full answers; uses cached f()) =====
{
echo "; Zone snapshot for $domain"
echo "; Generated: $(date) | Resolver: $ACTIVE_RESOLVER"
echo
types=(SOA NS A AAAA CNAME MX TXT SRV CAA DS DNSKEY)
names=("$domain" "_dmarc.$domain" "_domainkey.$domain" "www.$domain" \
"autodiscover.$domain" "enterpriseenrollment.$domain" "enterpriseregistration.$domain" \
"lyncdiscover.$domain" "sip.$domain" "_sip._tls.$domain" "_sipfederationtls._tcp.$domain" \
"s1._domainkey.$domain" "s2._domainkey.$domain" "selector1._domainkey.$domain" "selector2._domainkey.$domain" \
"apple-domain-verification.$domain")
if [[ -f "names/$domain.txt" ]]; then
while IFS= read -r lbl; do
[[ -z "$lbl" || "$lbl" =~ ^# ]] && continue
names+=("$lbl.$domain")
done < "names/$domain.txt"
fi
if (( ${#dkim_found_names[@]} )); then names+=("${dkim_found_names[@]}"); fi
readarray -t names < <(printf "%s\n" "${names[@]}" | awk '!seen[$0]++')
for n in "${names[@]}"; do
echo ";; ===== $n ====="
for rr in "${types[@]}"; do
ans="$(f "$rr" "$n" || true)"
[[ -n "$ans" ]] && { echo ";; -- $rr"; echo "$ans"; }
done
echo
done
} > "$zonesfile"
# latest pointer for zones
if ln -sfn "$(basename "$zonesfile")" "$zones_latest" 2>/dev/null; then :; else cp -f "$zonesfile" "$zones_latest"; fi
echo "Zone snapshot written to: $zonesfile"
echo "Updated latest → $zones_latest"
🔵