The commands you actually type, grouped by what you are trying to do rather than by tool. Built for 3am: find the section that matches your question, copy the line, move on.
Companion to the DNS track — every command here is explained in depth in one of its nine modules, and the module is cited so you can go and read why when you have time.
🔑
Three rules that make everything below work.
Read status: first. Not the answer, not the IP — the status line. Five outcomes, five different owners
No status: line at all means DNS told you nothing. Nothing responded; you are debugging the network
dig and your application do not take the same path. When they disagree, that is the diagnosis
dig +short example.com # values only - for scripts, but see the WARNINGdig +noall +answer example.com # values + TTL + type - the everyday formdig example.com # everything, when you need the header# a specific typedig +noall +answer example.com MXdig +noall +answer example.com TXTdig +noall +answer example.com AAAA # ALWAYS check this too - see A5
⚠️
Never use dig +short in a check or an alert. An empty result means NXDOMAIN or NODATA or SERVFAIL or REFUSED or a timeout — five different problems, four different teams, one identical empty string.
A2 · Everything this name has, in one sweep
bash
D=example.comfor t in SOA NS A AAAA MX TXT CAA DNSKEY; do out=$(dig +noall +answer "$D" "$t") [ -n "$out" ] && echo "$out" || printf '%-8s (none)\n' "$t"done
🔑
Print the (none) lines. The absences are findings: no AAAA means IPv6-only clients cannot reach you; no CAA means every CA on earth may issue for you; no DNSKEY means the zone is unsigned.
A3 · Does this exact name exist?
bash
N=admin.example.com# 1. wildcard check FIRST - without this, nothing below means anythingdig "zz$RANDOM-probe.example.com" +noall +comments | grep -o 'status: [A-Z]*'# NXDOMAIN -> no wildcard, results are trustworthy# anything else -> a wildcard exists, every name "resolves", stop here# 2. the name itself, with status visibledig "$N" +noall +comments +answer# 3. other types - a name can exist as CNAME or TXT onlydig "$N" AAAA +short ; dig "$N" CNAME +short ; dig "$N" TXT +short# 4. confirm against an AUTHORITATIVE server - no cache involveddig @"$(dig +short example.com NS | head -1)" "$N" +noall +comments +answer
What you see
What it means
NOERROR • ANSWER: 1+
Exists, with that record type
NOERROR • ANSWER: 0
NODATA — name exists, no record of this type. Try another type
NXDOMAIN
No such name at all
SERVFAIL / REFUSED
You learned nothing. Ask elsewhere — see C5
A4 · Follow a CNAME chain to the end
bash
dig +noall +answer www.example.com # shows the CNAME AND the resolved targetdig +short www.example.com # first line a hostname = it is a chain
🔑
If +short's first line is a name rather than an IP, you are looking at a CNAME chain. Each hop is latency on a cache miss and a third party who can break you.
A5 · Reverse lookup, and why it matters
bash
dig +short -x 8.8.8.8 # -x builds the in-addr.arpa name for youdig +short -x 2001:4860:4860::8888 # works for IPv6 too
⚠️
No PTR is a real finding if that host sends mail. Receiving servers check forward-confirmed reverse DNS; a sender with no PTR gets a deliverability penalty or an outright rejection. Cross-check any ip4: addresses in your SPF record.
B1 · Who operates the DNS, and who operates the mail?
bash
dig +short example.com NS # nameserver hostnames name the DNS PROVIDERdig +short example.com MX # MX targets name the MAIL PLATFORM
If the NS records look like
The provider is
ns-135.awsdns-16.com
AWS Route 53
amber.ns.cloudflare.com
Cloudflare
ns1-01.azure-dns.com
Azure DNS
ns-cloud-a1.googledomains.com
Google Cloud DNS
aspmx.l.google.com (MX)
Google Workspace
*.mail.protection.outlook.com (MX)
Microsoft 365
🔑
Two commands and you know which console to log into and which API to script against, on a domain nobody documented. This is the single highest-value lookup on an unfamiliar estate.
B2 · Ask the authoritative server directly (bypass every cache)
bash
D=example.comNS=$(dig +short $D NS | head -1)dig @"$NS" "$D" A +noall +comments +answer
Look for aa in the flags and a full, un-counted-down TTL. That combination is ground truth.
B3 · Do all the authoritative servers agree?
bash
D=example.comfor ns in $(dig +short $D NS); do printf '%-30s %s\n' "$ns" "$(dig @$ns $D A +short +norecurse | head -1)"done# and the serial - the number that reveals a missed bumpfor ns in $(dig +short $D NS); do printf '%-30s serial %s\n' "$ns" \ "$(dig @$ns $D SOA +short +norecurse | awk '{print $3}')"done
🚨
Same serial + different data = somebody edited a zone file without bumping the serial. The secondary compared serials, saw no change, and correctly did nothing. It is not broken. This is the classic silent DNS bug — half your traffic gets the old answer, at random, and it never reproduces on demand.
B4 · Delegation health — is any nameserver lame?
bash
D=example.comfor ns in $(dig +short $D NS); do printf '%-30s ' "$ns" out=$(dig @"$ns" "$D" SOA +norecurse +time=3 +tries=1 2>/dev/null) st=$(echo "$out" | grep -o 'status: [A-Z]*' | head -1 | cut -d' ' -f2) if [ -z "$st" ]; then echo "NO RESPONSE <-- unreachable" elif echo "$out" | grep -q 'flags:.* aa'; then echo "$st aa=yes OK" else echo "$st aa=NO <-- LAME DELEGATION"; fidone
⚠️
Test aa, not just reachability. A lame server answers happily while disclaiming authority, so a ping-style check passes it. One lame server in four means a few per cent of cold lookups fail, non-reproducibly — and it is nearly always a decommissioned server left in the delegation.
B5 · Parent vs child — does the delegation match?
bash
D=example.com ; PARENT="${D#*.}"# what the CHILD saysdig +short $D NS | sort# what the PARENT says - +norecurse is ESSENTIALdig @"$(dig +short $PARENT NS | head -1)" $D NS \ +norecurse +noall +authority | awk '$4=="NS"{print $5}' | sort
🚨
Without +norecurse your resolver helpfully fetches the child's answer and you compare the child against itself — a reassuring "match" that proves nothing. This is the most common mistake in delegation checking, and it fails in the direction of false confidence.
C1 · The four-question triage — start here, always
Diagram source
flowchart TD
S["a DNS incident"] --> Q1{"1. is there a<br>status: line at all?"}
Q1 -->|"no"| NET["NETWORK / FIREWALL<br>timed out = silent drop<br>refused = nothing listening"]
Q1 -->|"yes"| Q2{"2. what RCODE?"}
Q2 -->|"REFUSED"| POL["POLICY, not a fault<br>wrong server, or an ACL"]
Q2 -->|"SERVFAIL"| Q3{"3. does +cd fix it?"}
Q3 -->|"yes"| DS["DNSSEC VALIDATION<br>check RRSIG expiry FIRST"]
Q3 -->|"no"| Q3b["ask a SECOND resolver<br>one fails = that resolver<br>all fail = zone / delegation"]
Q2 -->|"NXDOMAIN / NODATA"| Q4{"4. does dig agree<br>with getent?"}
Q4 -->|"no"| LOCAL["LOCAL<br>/etc/hosts, nsswitch,<br>search list, split-horizon"]
Q4 -->|"yes"| AUTH["ask the AUTHORITATIVE servers<br>do they agree? same serial?"]
style NET fill:#ef4444,color:#fff
style DS fill:#ef4444,color:#fff
style POL fill:#8b5cf6,color:#fff
style LOCAL fill:#f59e0b,color:#fff
N=api.example.comecho "1. what DNS says" ; dig +short "$N"echo "2. what the SYSTEM says" ; getent hosts "$N"echo "3. is it overridden?" ; grep -n "${N%%.*}" /etc/hosts /etc/nsswitch.confecho "4. which resolver?" ; resolvectl query "$N" 2>/dev/null || cat /etc/resolv.conf
Pattern
Diagnosis
1 and 2 agree, app still wrong
Not name resolution. Application cache or connection pool. A JVM caches forever by default. Restart it
1 right, 2 wrong
/etc/hosts override, or an nsswitch.conf order you did not expect
1 empty, 2 works
Search-list expansion — the app queries a longer name. Confirm with dig +search
Right for you, wrong on their host
Different resolvers, or split-horizon. Compare with resolvectl status
C3 · Which resolver am I actually using?
bash
resolvectl status | grep -E 'Link|Current DNS Server|DNS Domain' # systemd hostsgrep -E '^(nameserver|search|options)' /etc/resolv.confgrep '^hosts' /etc/nsswitch.conf# the tiebreaker - configuration describes intent, the capture shows realitysudo timeout 5 tcpdump -n -i any 'udp port 53' &sleep 1 ; getent hosts example.com > /dev/null ; wait
⚠️
nameserver 127.0.0.53 tells you nothing about where your queries go. That is a local systemd-resolved stub; the real upstreams come from DHCP, a VPN or netplan and appear nowhere in the file. Editing it is usually pointless too — it is regenerated. resolvectl status is the command that answers the question.
C4 · Is it DNS, or is it the network?
bash
dig +trace example.com | tail -8 # bypasses EVERY cache, walks from the root
Result
Meaning
+trace shows the wrong value
The zone is wrong. Your change never landed
+trace right, your resolver wrong
A cache, or a broken resolution path. Compare public resolvers
+trace works, normal query fails
Your resolver is the problem — forward-only upstream down, firewall, poisoned entry
C5 · Decoding the failure
You see
Means
Next command
NXDOMAIN
No such name
Check spelling; check for a cached negative against an authoritative server
NOERROR • ANSWER: 0
NODATA — wrong type
Try AAAA, CNAME, TXT
SERVFAIL
The answering side failed
dig @1.1.1.1and@8.8.8.8. One fails = that resolver. All fail = the zone. Then +cd
REFUSED
Policy, not a fault
You are asking the wrong server, or an ACL excludes you. Not an incident
connection timed out
Packet silently dropped
Firewall or routing. Check egress rules for UDP and TCP 53
connection refused
Host reached, port rejected
Nothing listening. ss -ulnp | grep :53 on the server
is not a legal name
dig rejected it locally
No packet was sent. Fix the input
C6 · Big answers hang, small ones work
bash
S=1.1.1.1 ; N=orgdig @$S $N SOA +noall +comments | grep status # baselinedig @$S $N NS +dnssec +ignore +noall +comments | grep flags # tc set? (+ignore = do not retry)dig @$S $N NS +dnssec +tcp +noall +stats | grep -E 'SERVER|MSG SIZE'dig @$S $N SOA +noedns +noall +comments | grep status # EDNS-hostile middlebox?
Pattern
Diagnosis
Small ok · tc set · +tcptimes out
TCP/53 is blocked. By far the most common. Breaks every DNSSEC-signed zone
Small query times out · +noedns works
EDNS-hostile server or middlebox dropping OPT records
All four fine
Transport is healthy. Go back to C1
C7 · Is this SERVFAIL a DNSSEC problem?
bash
N=example.org ; R=1.1.1.1dig @$R $N A +noall +comments | grep status # SERVFAIL?dig @$R $N A +cd +noall +comments | grep status # NOERROR with +cd? -> DNSSECdig $N A +dnssec +noall +answer | awk '/RRSIG/{print "expires:",$9}' # check this FIRSTdig $N DS +noall +answer ; dig $N DNSKEY +noall +answer | head -2delv @$R $N A +rtrace 2>&1 | tail -5
🚨
"Nobody changed anything" is the diagnosis, not an alibi. RRSIGs expire on a wall-clock date — 30 days by default — whether or not the zone changes. A zone nobody has touched for a month is exactly the one that goes dark. Check the expiry field before anything else.
D=example.com ; N=www.example.comdig +noall +answer "$N" | awk '{print "current TTL:",$2}' # how long until it drains?for ns in $(dig +short $D NS); do # baseline: do they agree now? printf '%-30s %s\n' "$ns" "$(dig @$ns $N +short +norecurse | head -1)"donedig "$N" +noall +stats | grep 'MSG SIZE' # size headroom
D2 · Immediately after
bash
D=example.com ; N=www.example.com# 1. did it land, and do ALL authoritative servers agree?for ns in $(dig +short $D NS); do printf '%-30s %-18s serial %s\n' "$ns" \ "$(dig @$ns $N +short +norecurse | head -1)" \ "$(dig @$ns $D SOA +short +norecurse | awk '{print $3}')"done# 2. ground truth, no caches at alldig +trace "$N" | tail -4# 3. how far has it reached, and how much longer?for r in 1.1.1.1 8.8.8.8 9.9.9.9; do printf '%-10s %s\n' "$r" "$(dig @$r "$N" +noall +answer | head -1)"done
🔑
Read the TTLs in step 3 and you can state the remaining wait as a number instead of guessing. That is the difference between "it's propagating" and "every resolver will have it within 41 seconds".
D3 · The ordering rules people break
Rule
Why
Lower the TTL, then wait the OLD TTL, then change
Caches holding the old record never see the new low value until their copy expires. Same-day lowering helps nobody
Publish a DNSSEC DSafter the signed zone is live
DS first means resolvers demand signatures your servers are not serving yet — SERVFAIL everywhere
Remove the DSbefore un-signing
The reverse order takes the domain dark for the parent's TTL, which you do not control
Keep the old endpoint alive a week past the cutover
Serve-stale and application caches keep a tail of clients on the old address long past any TTL
Raise the TTL back up afterwards
A low TTL is a hard dependency on your nameservers being reachable right now
Run these against domains you own or are explicitly authorised to assess. Everything here is read-only and uses public data, but a wide sweep is indistinguishable from reconnaissance in a target's logs.
E1 · The complete list — from the provider, not from DNS
bash
# Route 53 - the ONLY 100% complete sourceZONE=$(aws route53 list-hosted-zones-by-name --dns-name example.com \ --query 'HostedZones[0].Id' --output text)aws route53 list-resource-record-sets --hosted-zone-id "$ZONE" --output table# machine-readable, for a diffaws route53 list-resource-record-sets --hosted-zone-id "$ZONE" \ --query 'ResourceRecordSets[].[Name,Type,TTL,ResourceRecords[0].Value]' --output text
🚨
DNS has no listing operation.dig ANY does not do it — since RFC 8482 most operators return a joke HINFO "RFC8482" record. Everything below E1 is reconstruction, and you must say so when you hand over the list.
E2 · Enumerate subdomains without provider access
bash
D=example.com# 1. wildcard check - if one exists, everything below is meaninglessdig "zz$RANDOM-probe.$D" +noall +comments | grep -o 'status: [A-Z]*'# 2. zone transfer, against EVERY nameserver (the open one is the forgotten secondary)for ns in $(dig +short $D NS); do if dig "@$ns" "$D" AXFR +time=3 +tries=1 2>/dev/null | grep -q "^$D"; then echo "OPEN AXFR on $ns <-- FINDING, report it" else echo "refused/timeout on $ns (expected)"; fidone# 3. Certificate Transparency - finds one-off and historic names no wordlist would guesscurl -s "https://crt.sh/?q=%25.$D&output=json" \ | jq -r '.[].name_value' | sed 's/^\*\.//' | sort -u > ct-names.txt# 4. which of those are still LIVE?while read -r n; do dig +short "$n" A | grep -q . && echo "LIVE $n" || echo "dead $n"done < ct-names.txt# 5. dictionary sweep - use a real tool, not a shell loopdnsx -d "$D" -w /usr/share/seclists/Discovery/DNS/dns-Jhaddix.txt -a -resp -silent
🚨
Re-verify every hit serially before the list leaves your hands. A high-concurrency sweep against a public resolver gets rate-limited, and throttled responses fool any script that tests "did dig print anything?" instead of testing status:. A false positive is worse than a miss — somebody opens a firewall rule or buys a certificate for a host that never existed.
Accept a name only when status: NOERROR AND the answer section contains a real A or CNAME line. Keep concurrency around 8, with +tries=2.
E3 · Name → address inventory, sorted by address
bash
D=example.comNAMES="www mail smtp dev demo jira confluence support crm gw intranet api"for n in $NAMES; do for ip in $(dig +short "$n.$D" A | grep -E '^[0-9]+\.'); do printf '%-16s %s\n' "$ip" "$n.$D" donedone | sort -Vecho "--- reverse DNS of each distinct address ---"for n in $NAMES; do dig +short "$n.$D" A; done | grep -E '^[0-9]+\.' | sort -u \ | while read -r ip; do printf '%-16s %s\n' "$ip" "$(dig +short -x "$ip" | head -1)"; done
🔑
Sort by address, not by name. A list of names is an inventory; a list sorted by address is an architecture diagram. Co-location and single points of failure are invisible one way and obvious the other.
But check before you call it a finding: behind a CDN, reverse proxy or Kubernetes ingress, hundreds of unrelated names legitimately share an address. Empty reverse DNS on a non-CDN range is what makes co-location real.
E4 · The audit findings worth looking for
Check
Why it matters
RFC 1918 addresses in a public zone
Useless externally, and leaks internal topology. A private record that escaped its horizon
No CAA record
Every publicly trusted CA on earth may issue for you. One record fixes it, zero traffic impact
No AAAA anywhere
IPv6-only clients — increasingly common on mobile — cannot reach you without translation
Missing PTR on SPF-listed senders
Reverse-DNS deliverability penalty or outright rejection on outbound mail
p=reject with pct=25 in DMARC
Reads as protected, enforcing on a quarter of failing mail. A rollout nobody finished
More than 10 DNS lookups in SPF
permerror, and mail starts bouncing when a vendor expands their record
Open AXFR on any nameserver
Hands over every internal hostname to anyone who asks
ss -tulnp # every listening TCP+UDP socket, with the processss -tulnp | grep ':53' # is a DNS server actually running here?ss -tnp state established # current connections and their processesss -s # summary counts - quick health glance
🔑
ss replaced netstat years ago and is on every modern system.-t TCP, -u UDP, -l listening, -n numeric (no DNS lookups — important when DNS is what is broken), -p process.
F2 · Can I reach it at all?
bash
ping -c 3 example.com # resolves AND tests reachabilityping -c 3 93.184.216.34 # by IP - removes DNS from the questionnc -zv example.com 443 # is the TCP port open? fast and definitivenc -zvu 8.8.8.8 53 # UDP checkip route get 93.184.216.34 # which interface and gateway will be used?ip -brief addr # my addresses, one line per interfaceip -brief link # interface state
⚠️
ping is not a DNS tool. It collapses NXDOMAIN, an invalid name, a dead resolver and a timeout into one message — Name or service not known — and tells you that resolution failed, never why. Use it to test reachability after you have an address.
F3 · Where does it break along the path?
bash
traceroute -n example.com # -n so a DNS problem does not slow the tracemtr -n --report --report-cycles 20 example.com # traceroute + ping, over time. Better
F4 · Test the service, not just the name
bash
# force a specific IP for a hostname WITHOUT touching /etc/hosts - the safest test there iscurl -sv --resolve example.com:443:93.184.216.34 https://example.com/ -o /dev/null# timing breakdown - where do the milliseconds go?curl -s -o /dev/null -w 'dns:%{time_namelookup}s connect:%{time_connect}s tls:%{time_appconnect}s ttfb:%{time_starttransfer}s total:%{time_total}s\n' https://example.com/# just the status and the address it actually reachedcurl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' https://example.com/
🔑
--resolve is the single most useful flag in this whole cheat sheet. It tests a specific backend through the real hostname — correct SNI, correct Host header, correct certificate validation — without editing /etc/hosts, so there is nothing to forget to undo afterwards. Use it to verify a new server before you cut DNS over to it.
And time_namelookup is your DNS latency, measured through the same path your application uses.
F5 · Certificates
bash
# what does the server actually present, and when does it expire?echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates# every hostname the certificate covers - useful for inventoryecho | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -ext subjectAltName# days until expiry, for monitoringecho | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -checkend $((30*86400)) && echo "OK >30 days" || echo "EXPIRES SOON"
🔑
-servername is mandatory. Without it there is no SNI, so a shared host hands you the wrong certificate and you chase a problem that does not exist. The subjectAltName list is also a free inventory source — it names every hostname that certificate was issued for.
F6 · Watch the packets
bash
sudo tcpdump -n -i any 'udp port 53 or tcp port 53' # all DNS, both transportssudo tcpdump -n -i any 'port 53' -c 20 # stop after 20 packetssudo tcpdump -n -i any 'host 1.1.1.1' # one peersudo tcpdump -n -i any 'port 53' -w /tmp/dns.pcap # save for Wireshark
🔑
This is the tiebreaker no one can argue with. Configuration files describe intent; the capture shows the destination address the query actually went to and the exact name that was in it. It also makes search-list expansion visible — you will see the failed suffixed queries go out before the successful one.
Always -n. Without it, tcpdump does reverse lookups while you are debugging DNS.
F7 · Service and log state
bash
systemctl status named bind9 systemd-resolved # whichever existssystemctl is-active systemd-resolvedjournalctl -u named -n 50 --no-pager # last 50 linesjournalctl -u named -f # followjournalctl -u systemd-resolved --since '10 min ago'resolvectl statistics # cache hits/missesresolvectl flush-caches # empty the local cache
H · The flags worth memorising
H1 · dig — the options you will actually use
Option
Effect
+short
Values only. Never in a check — five different failures look identical
+noall +answer
The everyday form: data with TTL and type, no noise
+noall +comments
Just the header — status: and flags:
@server
Ask a specific server. The feature that makes dig diagnostic rather than a lookup tool
+norecurse
Answer from cache only. Essential when comparing a parent's delegation with a child's NS set
+trace
Walk from the root yourself. Bypasses every cache, including serve-stale
+search
Apply the resolv.conf search list — reproduces what your application actually queries
+cd
Disable DNSSEC validation. The one option that isolates a DNSSEC fault
+dnssec
Set the do bit — ask for signatures
+tcp
Force TCP. A timeout here while UDP works means TCP/53 is blocked
+ignore
Do not retry on truncation, so you can actually see the tc bit
+noedns
Send no OPT record. Success here where a normal query fails = EDNS-hostile middlebox
+bufsize=N
Set the advertised UDP buffer, to find the size at which truncation starts
+stats
Query time, which server answered, the transport, and MSG SIZE
-x <ip>
Reverse lookup — builds the in-addr.arpa name for you
-y alg:name:key
TSIG-sign the query, e.g. for an authorised zone transfer
+time=N +tries=N
Fail fast in scripts. Defaults are 5 seconds and 3 tries
-f <file>
Batch mode: one query per line. Best for many names at once
H2 · Reading the flags line
Flag
Meaning
qr
This message is a response
aa
Authoritative — from a server holding the zone, not from a cache
tc
Truncated — too big for UDP, retry over TCP
rd
Recursion desired — set by the asker
ra
Recursion available — set by the answerer. Absent on authoritative-only servers
ad
Authentic data — the resolver DNSSEC-validated this answer
cd
Checking disabled — you asked it not to validate
do
Lives in the OPT line, not the header. DNSSEC OK — "send me signatures"
🔑
An absent flag is as informative as a present one. No aa means you are reading a cache. No ra means the server does not recurse for you — normal for an authoritative server, a red flag if you thought you were querying a resolver.
I · Copy-paste blocks
I1 · Full domain audit
⚖️
Read-only, no dependencies beyond dig. Run against domains you own or are explicitly authorised to assess.
bash
#!/usr/bin/env bash# dns-audit.sh <domain> [wordlist]set -uo pipefailD="${1:?usage: dns-audit.sh <domain> [wordlist]}"; W="${2:-}"hr(){ printf '\n== %s ==\n' "$1"; }hr "ZONE RECORDS: $D"for t in SOA NS A AAAA MX TXT CAA DNSKEY; do o=$(dig +noall +answer "$D" "$t") [ -n "$o" ] && echo "$o" || printf '%-8s (none)\n' "$t"donehr "AUTHORITATIVE SERVERS"dig +short "$D" NS | sorthr "WILDCARD CHECK"st=$(dig "zz$RANDOM$RANDOM-probe.$D" +noall +comments | grep -o 'status: [A-Z]*' | head -1)if [ "$st" = "status: NXDOMAIN" ]; then echo "no wildcard -> existence checks are trustworthy"else echo "WILDCARD PRESENT ($st) -> every name resolves; enumeration unreliable"fihr "AXFR ATTEMPT (every nameserver)"for ns in $(dig +short "$D" NS); do if dig "@$ns" "$D" AXFR +time=3 +tries=1 2>/dev/null | grep -q "^$D"; then echo "OPEN AXFR on $ns <-- FINDING, report it" else echo "refused/timeout on $ns (expected)" fidonehr "DELEGATION HEALTH"for ns in $(dig +short "$D" NS); do printf ' %-30s ' "$ns" out=$(dig "@$ns" "$D" SOA +norecurse +time=3 +tries=1 2>/dev/null) s=$(echo "$out" | grep -o 'status: [A-Z]*' | head -1 | cut -d' ' -f2) if [ -z "$s" ]; then echo "NO RESPONSE <-- unreachable" elif echo "$out" | grep -q 'flags:.* aa'; then echo "$s aa=yes serial $(dig @$ns $D SOA +short +norecurse | awk '{print $3}')" else echo "$s aa=NO <-- LAME DELEGATION" fidone[ -z "$W" ] && { hr "DONE"; exit 0; }hr "NAME -> ADDRESS"while read -r w; do [ -z "$w" ] && continue ans=$(dig +noall +answer "$w.$D" A); [ -z "$ans" ] && continue ips=$(echo "$ans" | awk '$4=="A"{print $5}' | paste -sd, -) cn=$( echo "$ans" | awk '$4=="CNAME"{print $5}' | head -1) if [ -n "$cn" ]; then printf '%-34s %-8s CNAME %s -> %s\n' "$w.$D" "ALIAS" "$cn" "${ips:--}" else printf '%-34s %-8s %s\n' "$w.$D" "A" "$ips"; fidone < "$W"hr "REVERSE DNS OF DISCOVERED ADDRESSES"while read -r w; do [ -z "$w" ] && continue; dig +short "$w.$D" A; done < "$W" \ | grep -E '^[0-9]+\.' | sort -u \ | while read -r ip; do printf '%-16s %s\n' "$ip" "$(dig +short -x "$ip" | head -1)"; done
🚨
Re-verify every dictionary hit serially before the list leaves your hands. A high-concurrency sweep gets rate-limited by public resolvers, and throttled responses fool any script that tests "did dig print anything?" rather than testing status:. Keep concurrency around 8 with +tries=2 — a false positive is worse than a miss, because someone opens a firewall rule for a host that never existed.
I2 · Watch a change reach the world
bash
#!/usr/bin/env bash# watch-change.sh <name> <expected-value># Poll the big public resolvers until they all agree with you.N="${1:?usage: watch-change.sh <name> <expected-value>}"; WANT="${2:?}"while :; do printf '%s ' "$(date +%H:%M:%S)" ok=0 for r in 1.1.1.1 8.8.8.8 9.9.9.9; do got=$(dig @"$r" "$N" +short | head -1) if [ "$got" = "$WANT" ]; then printf '%-10s OK ' "$r"; ok=$((ok+1)) else printf '%-10s %-9s' "$r" "${got:-none}"; fi done echo [ "$ok" -eq 3 ] && { echo "all resolvers agree"; break; } sleep 10done
I3 · A monitoring check written correctly
bash
# Exits 0 only if status is NOERROR AND a real record came back.# Never test for empty output - that reports the same failure for a deleted# record, a broken resolver, a policy refusal and a network partition.check_dns() { local out out=$(dig "$1" A +noall +comments +answer 2>/dev/null) || return 1 echo "$out" | grep -q 'status: NOERROR' || return 1 echo "$out" | grep -qE 'IN[[:space:]]+(A|CNAME)'}check_dns api.example.com && echo OK || echo FAIL
I4 · Serial agreement — the alert most teams are missing
bash
# Alerts if the authoritative servers do not all report the same serial.# Catches missed serial bumps, failed transfers and expired zones in one check.serial_agreement() { local d="$1" n n=$(for ns in $(dig +short "$d" NS); do dig @"$ns" "$d" SOA +short +norecurse 2>/dev/null | awk '{print $3}' done | sort -u | wc -l) [ "$n" -eq 1 ]}serial_agreement example.com && echo "in sync" || echo "DIVERGENCE - check transfers"
I5 · DNSSEC expiry — the other missing alert
bash
# Warn N days before the signatures expire. RRSIGs expire on a DATE,# whether or not the zone changes - so this fires with no change to review.rrsig_days_left() { local d="$1" exp exp=$(dig "$d" SOA +dnssec +noall +answer | awk '/RRSIG/{print $9; exit}') [ -z "$exp" ] && { echo "unsigned"; return; } echo $(( ( $(date -u -d "${exp:0:8} ${exp:8:2}:${exp:10:2}:${exp:12:2}" +%s) \ - $(date -u +%s) ) / 86400 ))}d=$(rrsig_days_left example.org); echo "$d days of signature left"
🔑
I4 and I5 are the two alerts most teams do not have, and both catch failures that are otherwise completely silent: a stale secondary answering a share of live traffic with old data, and a signed zone that goes dark on a date nobody has in a calendar. Neither is in a default monitoring template.
💡
Where to read the reasoning. Every command here comes from the DNS track: reading responses and failure classes in Module 01 · records, zone files and domain auditing in 02 · delegation and caching in 03 · transport, EDNS and truncation in 04 · running BIND in 05 · the Linux resolver path in 06 · DNSSEC in 07 · cloud and Kubernetes in 08 · scale and security in 09.
If you keep only three things from this page: read status: first; no status: line at all means DNS told you nothing; and when dig and getent disagree, that is the diagnosis.
Spotted a mistake or want something added? Send me a note.