⚡ Daily Ops Cheat Sheet — DNS & Linux Networking

Updated 20 August 2026

Daily Ops Cheat Sheet — DNS & Linux Networking

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.
  1. Read status: first. Not the answer, not the IP — the status line. Five outcomes, five different owners
  2. No status: line at all means DNS told you nothing. Nothing responded; you are debugging the network
  3. dig and your application do not take the same path. When they disagree, that is the diagnosis

A · Look something up

Official docs: dig · host · getent(1)DNS track Modules 01–02

A1 · What does this name resolve to?

bash
dig +short example.com                  # values only - for scripts, but see the WARNING
dig +noall +answer example.com          # values + TTL + type - the everyday form
dig example.com                         # everything, when you need the header

# a specific type
dig +noall +answer example.com MX
dig +noall +answer example.com TXT
dig +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.com
for 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 anything
dig "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 visible
dig "$N" +noall +comments +answer

# 3. other types - a name can exist as CNAME or TXT only
dig "$N" AAAA +short ; dig "$N" CNAME +short ; dig "$N" TXT +short

# 4. confirm against an AUTHORITATIVE server - no cache involved
dig @"$(dig +short example.com NS | head -1)" "$N" +noall +comments +answer
What you seeWhat it means
NOERRORANSWER: 1+Exists, with that record type
NOERRORANSWER: 0NODATA — name exists, no record of this type. Try another type
NXDOMAINNo such name at all
SERVFAIL / REFUSEDYou 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 target
dig +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 you
dig +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.

B · Who is responsible for this?

Official docs: dig@server, +norecurse · RFC 1912 §2.8DNS track Modules 02–03

B1 · Who operates the DNS, and who operates the mail?

bash
dig +short example.com NS       # nameserver hostnames name the DNS PROVIDER
dig +short example.com MX       # MX targets name the MAIL PLATFORM
If the NS records look likeThe provider is
ns-135.awsdns-16.comAWS Route 53
amber.ns.cloudflare.comCloudflare
ns1-01.azure-dns.comAzure DNS
ns-cloud-a1.googledomains.comGoogle 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.com
NS=$(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.com
for 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 bump
for 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.com
for 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"; fi
done
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 says
dig +short $D NS | sort

# what the PARENT says - +norecurse is ESSENTIAL
dig @"$(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.

C · Something is broken

Official docs: dig · getent(1) · resolvectl(1) · BIND 9 TroubleshootingDNS track Modules 01, 03, 06

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
bash
N=api.example.com

dig "$N" +noall +comments | grep -E 'status|flags'   # 1 and 2
dig "$N" +cd +noall +comments | grep status          # 3
getent hosts "$N"                                    # 4

C2 · dig works but the application does not

bash
N=api.example.com

echo "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.conf
echo "4. which resolver?"      ; resolvectl query "$N" 2>/dev/null || cat /etc/resolv.conf
PatternDiagnosis
1 and 2 agree, app still wrongNot 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 worksSearch-list expansion — the app queries a longer name. Confirm with dig +search
Right for you, wrong on their hostDifferent 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 hosts
grep -E '^(nameserver|search|options)' /etc/resolv.conf
grep '^hosts' /etc/nsswitch.conf

# the tiebreaker - configuration describes intent, the capture shows reality
sudo 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
ResultMeaning
+trace shows the wrong valueThe zone is wrong. Your change never landed
+trace right, your resolver wrongA cache, or a broken resolution path. Compare public resolvers
+trace works, normal query failsYour resolver is the problem — forward-only upstream down, firewall, poisoned entry

C5 · Decoding the failure

You seeMeansNext command
NXDOMAINNo such nameCheck spelling; check for a cached negative against an authoritative server
NOERRORANSWER: 0NODATA — wrong typeTry AAAA, CNAME, TXT
SERVFAILThe answering side faileddig @1.1.1.1 and @8.8.8.8. One fails = that resolver. All fail = the zone. Then +cd
REFUSEDPolicy, not a faultYou are asking the wrong server, or an ACL excludes you. Not an incident
connection timed outPacket silently droppedFirewall or routing. Check egress rules for UDP and TCP 53
connection refusedHost reached, port rejectedNothing listening. ss -ulnp | grep :53 on the server
is not a legal namedig rejected it locallyNo packet was sent. Fix the input

C6 · Big answers hang, small ones work

bash
S=1.1.1.1 ; N=org

dig @$S $N SOA +noall +comments | grep status              # baseline
dig @$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?
PatternDiagnosis
Small ok · tc set · +tcp times outTCP/53 is blocked. By far the most common. Breaks every DNSSEC-signed zone
Small query times out · +noedns worksEDNS-hostile server or middlebox dropping OPT records
All four fineTransport is healthy. Go back to C1

C7 · Is this SERVFAIL a DNSSEC problem?

bash
N=example.org ; R=1.1.1.1

dig @$R $N A +noall +comments | grep status       # SERVFAIL?
dig @$R $N A +cd +noall +comments | grep status   # NOERROR with +cd? -> DNSSEC
dig $N A +dnssec +noall +answer | awk '/RRSIG/{print "expires:",$9}'   # check this FIRST
dig $N DS +noall +answer ; dig $N DNSKEY +noall +answer | head -2
delv @$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 · Verify a change

Official docs: RFC 2308 — negative caching · RFC 8767 — serve-staleDNS track Module 03

D1 · Before you change anything

bash
D=example.com ; N=www.example.com

dig +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)"
done
dig "$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 all
dig +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

RuleWhy
Lower the TTL, then wait the OLD TTL, then changeCaches holding the old record never see the new low value until their copy expires. Same-day lowering helps nobody
Publish a DNSSEC DS after the signed zone is liveDS first means resolvers demand signatures your servers are not serving yet — SERVFAIL everywhere
Remove the DS before un-signingThe 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 cutoverServe-stale and application caches keep a tail of clients on the old address long past any TTL
Raise the TTL back up afterwardsA low TTL is a hard dependency on your nameservers being reachable right now

E · Inventory and audit a domain

Official docs: RFC 5936 — AXFR · RFC 9162 — Certificate Transparency · Route 53 APIDNS track Module 02 Part D
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 source
ZONE=$(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 diff
aws 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 meaningless
dig "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)"; fi
done

# 3. Certificate Transparency - finds one-off and historic names no wordlist would guess
curl -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 loop
dnsx -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.com
NAMES="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"
  done
done | sort -V

echo "--- 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

CheckWhy it matters
RFC 1918 addresses in a public zoneUseless externally, and leaks internal topology. A private record that escaped its horizon
No CAA recordEvery publicly trusted CA on earth may issue for you. One record fixes it, zero traffic impact
No AAAA anywhereIPv6-only clients — increasingly common on mobile — cannot reach you without translation
Missing PTR on SPF-listed sendersReverse-DNS deliverability penalty or outright rejection on outbound mail
p=reject with pct=25 in DMARCReads as protected, enforcing on a quarter of failing mail. A rollout nobody finished
More than 10 DNS lookups in SPFpermerror, and mail starts bouncing when a vendor expands their record
Open AXFR on any nameserverHands over every internal hostname to anyone who asks
All four NS records under one TLDA shared failure mode nobody chose

F · The layer around DNS

Official docs: ss(8) · ip(8) · tcpdump(1) · curl(1)

F1 · Is anything listening, and who?

bash
ss -tulnp                       # every listening TCP+UDP socket, with the process
ss -tulnp | grep ':53'          # is a DNS server actually running here?
ss -tnp state established       # current connections and their processes
ss -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 reachability
ping -c 3 93.184.216.34                   # by IP - removes DNS from the question

nc -zv example.com 443                    # is the TCP port open? fast and definitive
nc -zvu 8.8.8.8 53                        # UDP check

ip route get 93.184.216.34                # which interface and gateway will be used?
ip -brief addr                            # my addresses, one line per interface
ip -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 trace
mtr -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 is
curl -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 reached
curl -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 inventory
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName

# days until expiry, for monitoring
echo | 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 transports
sudo tcpdump -n -i any 'port 53' -c 20                   # stop after 20 packets
sudo tcpdump -n -i any 'host 1.1.1.1'                    # one peer
sudo 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 exists
systemctl is-active systemd-resolved

journalctl -u named -n 50 --no-pager              # last 50 lines
journalctl -u named -f                            # follow
journalctl -u systemd-resolved --since '10 min ago'

resolvectl statistics                              # cache hits/misses
resolvectl flush-caches                            # empty the local cache

H · The flags worth memorising

H1 · dig — the options you will actually use

OptionEffect
+shortValues only. Never in a check — five different failures look identical
+noall +answerThe everyday form: data with TTL and type, no noise
+noall +commentsJust the header — status: and flags:
@serverAsk a specific server. The feature that makes dig diagnostic rather than a lookup tool
+norecurseAnswer from cache only. Essential when comparing a parent's delegation with a child's NS set
+traceWalk from the root yourself. Bypasses every cache, including serve-stale
+searchApply the resolv.conf search list — reproduces what your application actually queries
+cdDisable DNSSEC validation. The one option that isolates a DNSSEC fault
+dnssecSet the do bit — ask for signatures
+tcpForce TCP. A timeout here while UDP works means TCP/53 is blocked
+ignoreDo not retry on truncation, so you can actually see the tc bit
+noednsSend no OPT record. Success here where a normal query fails = EDNS-hostile middlebox
+bufsize=NSet the advertised UDP buffer, to find the size at which truncation starts
+statsQuery time, which server answered, the transport, and MSG SIZE
-x <ip>Reverse lookup — builds the in-addr.arpa name for you
-y alg:name:keyTSIG-sign the query, e.g. for an authorised zone transfer
+time=N +tries=NFail 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

FlagMeaning
qrThis message is a response
aaAuthoritative — from a server holding the zone, not from a cache
tcTruncated — too big for UDP, retry over TCP
rdRecursion desired — set by the asker
raRecursion available — set by the answerer. Absent on authoritative-only servers
adAuthentic data — the resolver DNSSEC-validated this answer
cdChecking disabled — you asked it not to validate
doLives 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 pipefail
D="${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"
done

hr "AUTHORITATIVE SERVERS"
dig +short "$D" NS | sort

hr "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"
fi

hr "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)"
  fi
done

hr "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"
  fi
done

[ -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"; fi
done < "$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 10
done

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.