Module 05 — Running an Authoritative Server (BIND 9)

Updated 20 August 2026

Module 05 · Running an Authoritative Server (BIND 9)

Four modules of reading other people's DNS. Now you run your own: a primary and a secondary on one laptop, a TSIG-authenticated zone transfer between them, and the serial-number bug from Module 03 committed deliberately so you recognise it when it costs somebody a weekend.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01–04. You need zone files and named-checkzone (02), NS/SOA and delegation (02, 03), the aa flag and REFUSED (01), and why AXFR needs TCP (04).

Everything in this module runs on one machine. Two named processes on 127.0.0.1, on ports 5301 and 5302, in /tmp/lab. Nothing touches the system resolver, nothing needs port 53, nothing needs a second host, and nothing survives a reboot — so you can break it freely.

Part A · Standing up a nameserver

A1 · What named is, and installing it

The analogy. Think of one shop unit that can be run as either a bakery or a wholesaler.

Same building, same fittings. What you sell is a decision you make on day one, and it changes who is allowed to walk in.

named is that unit. Run as an authoritative server it answers only about its own goods. Run as a resolver it fetches anything for anyone — and doing both on a public street is how you end up working for people you never agreed to serve.

named — pronounced name-dee, the name daemon — is BIND's server. It is the reference implementation of DNS, and the same binary can act as an authoritative server, a recursive resolver, or both at once.

One binary, two jobs, and keeping them apart is the first design decision you make. Module 01 B1 introduced authoritative servers and recursive resolvers as different roles; named can do either. Running both on one public address is the open resolver finding from Module 01 — you become a free amplifier for attacks on other people.

So the very first line of any authoritative configuration is recursion no;. Not as a hardening step you get to later — as the definition of what this server is.

bash
sudo apt install -y bind9 bind9-utils bind9-dnsutils    # Debian / Ubuntu
sudo dnf install -y bind bind-utils                     # RHEL / Rocky / Fedora

named -V | head -2
🧪 Exercise A1.1 — Confirm the toolchain, not just the daemon
bash
named -V | head -2
which named rndc rndc-confgen tsig-keygen named-checkconf named-checkzone
Expected result — click to reveal
plain text
BIND 9.18.39-0ubuntu0.24.04.5-Ubuntu (Extended Support Version) <id:>
running on Linux x86_64 6.18.5 #1 SMP PREEMPT_DYNAMIC

/usr/sbin/named
/usr/sbin/rndc
/usr/sbin/rndc-confgen
/usr/sbin/tsig-keygen
/usr/bin/named-checkconf
/usr/bin/named-checkzone

Six binaries, and you will use every one of them in this module. Installing BIND gives you a toolchain, not just a daemon — and named-checkconf, named-checkzone and tsig-keygen are useful even on machines that never run a nameserver.

Note "Extended Support Version". BIND ships in two tracks: stable ESV releases with long support, and feature releases that move faster. On anything you have to operate, run the ESV — DNS is infrastructure, and infrastructure wants boring.

If named is missing but dig works, you installed only bind9-dnsutils back in Module 01. The client tools and the server are separate packages, which catches people out.


A2 · The smallest working named.conf

The analogy. Think of the notice you put in the shop window on opening day.

Who you serve, what hours, which counter is which — and, most importantly, the line that says "we do not take orders for other shops' goods".

That one line is recursion no;, and leaving it off is how a small shop ends up with a queue of strangers using it as a free delivery service.

named.conf has two kinds of statement that matter here: a global options block, and one zone block per zone served.

bash
mkdir -p /tmp/lab/p /tmp/lab/s

cat > /tmp/lab/p/named.conf <<'EOF'
options {
    directory   "/tmp/lab/p";
    pid-file    "/tmp/lab/p/named.pid";
    listen-on port 5301 { 127.0.0.1; };
    listen-on-v6 { none; };
    recursion no;                    // this is an AUTHORITATIVE server
    allow-transfer { none; };        // deny by default, permit per zone
    dnssec-validation no;            // nothing to validate: we do not recurse
};

zone "lab.internal" {
    type primary;
    file "/tmp/lab/p/zone.db";
};
EOF
DirectiveWhy it is there
listen-on port 5301A high port so the lab never collides with the system resolver on 53
recursion noThe line that defines this as an authoritative server. Omit it and you have built an open resolver
allow-transfer { none; }Deny zone transfers globally, then permit them per zone. Default-deny, always
type primaryThis server reads the zone from a file it owns. Formerly type master
dnssec-validation noValidation is a resolver function. An authoritative-only server has nothing to validate
primary/secondary replaced master/slave in BIND 9.16. Both still work, and you will meet master constantly in older configurations, tutorials and Stack Overflow answers. Write the new names; read both.
🧪 Exercise A2.1 — Break the config three ways and read the errors
bash
cd /tmp/lab

# 1. a missing semicolon - the classic
sed 's|listen-on port 5301 { 127.0.0.1; };|listen-on port 5301 { 127.0.0.1; }|' \
    p/named.conf > bad1.conf ; named-checkconf bad1.conf

# 2. a typo in a keyword
sed 's|type primary;|type primaryy;|' p/named.conf > bad2.conf ; named-checkconf bad2.conf

# 3. a zone file that does not exist - note the -z flag
sed 's|file "/tmp/lab/p/zone.db";|file "/tmp/lab/p/missing.db";|' \
    p/named.conf > bad3.conf ; named-checkconf -z bad3.conf
Expected result — click to reveal
plain text
bad1.conf:5: missing ';' before 'listen-on-v6'

bad2.conf:15: 'primaryy' unexpected

zone lab.internal/IN: loading from master file /tmp/lab/p/missing.db failed: file not found
zone lab.internal/IN: not loaded due to errors.
_default/lab.internal/IN: file not found

Error 1 points at line 5 and names the line after the mistake. That is normal for a recursive-descent parser: it reads on until something does not fit. When named-checkconf blames a line, look at the line above it too — this one habit saves a surprising amount of time.

Error 2 is precise because primaryy is not a keyword at all.

Error 3 is the important one, and it needed -z. Plain named-checkconf only parses the configuration; it does not open the zone files. A config pointing at a nonexistent, unreadable or invalid zone file passes cleanly.

So the rule is: always named-checkconf -z. It loads every zone as named would. Without it you get a green light from the syntax checker and a server that starts up refusing to answer for the very zone it exists to serve — which is Module 03's lame delegation, self-inflicted.


A3 · The zone file, validated before the server ever sees it

The analogy. Think of proofreading the menu before you print five hundred copies.

Once it is printed and on the tables, every mistake is served to customers all evening. Checking costs a minute; reprinting costs the night.

This is the zone file format from Module 02 C1, now for a zone you will actually serve.

bash
cat > /tmp/lab/p/zone.db <<'EOF'
$TTL 300
$ORIGIN lab.internal.
@   IN SOA ns1.lab.internal. hostmaster.lab.internal. (
        2026081701 ; serial
        7200       ; refresh
        900        ; retry
        1209600    ; expire
        300 )      ; minimum - negative TTL
@   IN NS  ns1.lab.internal.
@   IN NS  ns2.lab.internal.
ns1 IN A   127.0.0.1
ns2 IN A   127.0.0.1
@   IN A   192.0.2.20
www IN CNAME @
api IN A   192.0.2.30
api IN A   192.0.2.31
EOF

named-checkzone lab.internal /tmp/lab/p/zone.db
named-checkzone -D lab.internal /tmp/lab/p/zone.db     # and READ this
Two records in that file exist only because the zone has to describe itself. The SOA names the primary and carries the timers that the secondary in Part C will obey. The two NS records are the zone's own statement of who serves it — and Module 03 C1 established that a resolver reaches you via the parent's delegation, not these. They still must be correct and must match, because a secondary refreshes from them and because delegation checkers compare the two.

A4 · Start it, and ask it a question

The analogy. Think of unlocking the door and serving the very first customer yourself.

Not because you enjoy it — because that is how you find out the till works, the prices scan, and the receipt prints. You are checking the machinery, not making a sale.

bash
# -c <file> : use this config.  Run it detached so it survives your shell.
setsid named -c /tmp/lab/p/named.conf > /tmp/lab/p.log 2>&1 < /dev/null
sleep 2
pgrep -a named
🧪 Exercise A4.1 — Query your own nameserver, and check the flags
bash
dig @127.0.0.1 -p 5301 lab.internal SOA +noall +comments +answer
dig @127.0.0.1 -p 5301 api.lab.internal A +noall +answer
dig @127.0.0.1 -p 5301 www.lab.internal A +noall +answer
Expected result — click to reveal
plain text
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50756
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: 397d8dbd9fb4f662010000006a8674409ff7f1ea4829c8d0 (good)
;; ANSWER SECTION:
lab.internal.	300	IN	SOA	ns1.lab.internal. hostmaster.lab.internal. 2026081701 7200 900 1209600 300

api.lab.internal.	300	IN	A	192.0.2.30
api.lab.internal.	300	IN	A	192.0.2.31

www.lab.internal.	300	IN	CNAME	lab.internal.
lab.internal.		300	IN	A	192.0.2.20

Four things in that header, and you have met every one of them before.

aa is set. Your server holds the zone, so it marks the answer authoritative — the flag you have been using since Module 01 to tell ground truth from cache.

ra is absent, and dig warns you about it. ;; WARNING: recursion requested but not available is recursion no; working exactly as intended. This is a healthy authoritative server, not a broken one — that warning line is what a correctly configured nameserver looks like.

COOKIE: ... (good) is a DNS cookie, an EDNS option from Module 04 C2. BIND 9.18 does it automatically: a shared token that makes off-path spoofing considerably harder, with no configuration from you.

And the TTLs are the full 300, not counted down, because nothing here is a cache. Full TTL plus aa — the signature of an authoritative answer, exactly as promised in Module 01 C5.

api returned both addresses as one RRset with a shared TTL, and www returned the CNAME plus the resolved target — Module 02 A1 and B2, now coming out of a server you configured.

🎯 Interview questions — Standing up a server

Q. What is the difference between named-checkconf and named-checkzone?

named-checkconf parses named.conf — syntax and structure. named-checkzone parses a zone file and reports errors and warnings in the records.

The detail that matters far more than the definition: plain named-checkconf does not open the zone files. A configuration pointing at a missing, unreadable or broken zone file passes cleanly, and then named starts and refuses to answer for that zone.

So the command to actually run is named-checkconf -z, which loads every zone as the server would. That single flag is the difference between a pre-flight check and a false sense of security — and I would put it in CI rather than trusting anyone to remember it.

Q. What is the first line of any authoritative server configuration?

recursion no;

One binary does both jobs, and an authoritative server that also recurses for the world is an open resolver — it lets strangers consume your capacity and, far worse, makes you a reflector and amplifier for attacks on other people, since a small forged query produces a large response to a victim.

What I would add to show it is a design principle rather than a checklist item: the two roles have opposite requirements. An authoritative server needs to be reachable by everyone and to answer only about its own zones. A resolver needs to answer anything but only for a known client set. Trying to satisfy both on one address is why the failure keeps recurring, which is why the right answer is separate servers or at minimum separate addresses.


Part B · Controlling a running server

B1 · rndc and the control channel

The analogy. Think of a two-way radio to the shop floor.

You can change the price list, ask what is in stock, or close a counter — all without shutting the shop and reopening it.

And it is locked to one channel with a code, because a radio anyone could shout orders down would be worse than no radio at all.

rndcremote name daemon control — talks to a running named over an authenticated TCP channel. It is how you reload zones, inspect state and flush caches without restarting anything.

The control channel is authenticated with a shared HMAC key, and that is not optional. An unauthenticated control channel would let anyone who can reach the port reload zones, dump the cache or stop the server. named will not enable one without a key.

This is the same primitive as TSIG in Part C — a symmetric key, an HMAC over the message. Learn it once here and zone-transfer authentication is the same idea applied to a different message.

bash
tsig-keygen -a hmac-sha256 rndc-key > /tmp/lab/rndc.key

cat /tmp/lab/rndc.key >> /tmp/lab/p/named.conf
cat >> /tmp/lab/p/named.conf <<'EOF'
controls {
    inet 127.0.0.1 port 5391 allow { 127.0.0.1; } keys { "rndc-key"; };
};
EOF

{ cat /tmp/lab/rndc.key
  echo 'options { default-key "rndc-key"; default-server 127.0.0.1; default-port 5391; };'
} > /tmp/lab/rndc.conf

named-checkconf -z /tmp/lab/p/named.conf
pkill named ; sleep 1
setsid named -c /tmp/lab/p/named.conf > /tmp/lab/p.log 2>&1 < /dev/null
🧪 Exercise B1.1 — Inspect a running server without touching it
bash
rndc -c /tmp/lab/rndc.conf status
rndc -c /tmp/lab/rndc.conf zonestatus lab.internal
Expected result — click to reveal
plain text
$ rndc -c /tmp/lab/rndc.conf status
version: BIND 9.18.39-0ubuntu0.24.04.5-Ubuntu (Extended Support Version) <id:>
running on localhost: Linux x86_64 6.18.5 #1 SMP PREEMPT_DYNAMIC
boot time: Thu, 20 Aug 2026 03:29:12 GMT
last configured: Thu, 20 Aug 2026 03:29:13 GMT
configuration file: /tmp/lab/p/named.conf
CPUs found: 2
worker threads: 2
UDP listeners per interface: 2
number of zones: 2 (0 automatic)
debug level: 0
xfers running: 0
xfers deferred: 0
soa queries in progress: 0
query logging is OFF

$ rndc -c /tmp/lab/rndc.conf zonestatus lab.internal
name: lab.internal
type: primary
files: /tmp/lab/p/zone.db
serial: 2026081702
nodes: 5
last loaded: Thu, 20 Aug 2026 03:29:33 GMT
secure: no
dynamic: no

status gives you four things worth knowing in an incident:

  • last configured — if it is older than your last change, your change never loaded. This one line settles the most common "but I edited the file" argument
  • xfers running / xfers deferred — deferred transfers piling up means secondaries are struggling and you are about to have out-of-sync servers
  • number of zones — 2 here: your zone plus the built-in bind chaos zone
  • query logging is OFF — the switch you will want in B3

zonestatus is the per-zone version and serial is the field to read. It is the number a secondary compares against, and Part C is about what happens when it does not change.

secure: no means the zone is not DNSSEC-signed. Module 07 turns that to yes.


B2 · reload, reconfig, and knowing which you need

The analogy. Think of restocking a shelf versus redrawing the floor plan.

Redrawing the plan tells staff where the aisles are. It does not put anything on the shelves.

That is reconfig versus reload, and it is why people edit a zone file, run the wrong one, see nothing change, and go looking for the problem somewhere else entirely.

CommandWhat it does
rndc reloadRe-read named.conf and every zone file
rndc reload <zone>Re-read one zone file. The everyday command
rndc reconfigRe-read named.conf and load new or removed zones only — does not re-read unchanged zone files
rndc retransfer <zone>On a secondary: fetch the zone again now, ignoring the serial
rndc notify <zone>On a primary: re-send NOTIFY to the secondaries
rndc flushEmpty the resolver cache. Meaningless on an authoritative-only server
rndc querylog onToggle query logging at runtime, with no restart
Never restart named to pick up a zone change. A restart drops in-flight queries, discards the resolver cache if there is one, and re-reads everything including configuration you may have half-edited. rndc reload <zone> is atomic, instant and scoped to the one thing you changed.

The distinction people get wrong: reconfig will not pick up an edited zone file. It reads named.conf to find zones that were added or removed. Edit zone.db, run reconfig, and nothing happens — you conclude the file is fine and go looking somewhere else. reload is what re-reads zone data.


B3 · Reading the log

The analogy. Think of the shop's day book.

One line in it tells you which version of the price list is actually on the shelf right now — which is a different question from which version you last printed.

If the version in the book is not the one you just wrote, you have found your problem without touching anything else.

named's log is unusually informative, and three categories carry almost everything you need: zone loading, transfers, and NOTIFY.

Learn these four lines and you can diagnose most zone problems by reading rather than guessing:
  • zone lab.internal/IN: loaded serial 2026081701 — the zone loaded, and the serial it loaded with
  • zone lab.internal/IN: Transfer started. — a secondary has begun fetching
  • transfer of 'lab.internal/IN' from 127.0.0.1#5301: Transfer status: success — it worked
  • zone lab.internal/IN: sending notifies (serial N) — a primary telling its secondaries there is news

The one that matters most is the first. If the serial in the log is not the serial you just wrote, your edit did not take effect — and you have saved yourself an hour of debugging DNS when the problem was a file that never loaded.


Part C · Primary and secondary

C1 · What a secondary is, and what it is not

The analogy. Think of a second till that is open right now — not a spare tyre in the boot.

Customers are being sent to both tills all day long. Nobody prefers the first one.

So if the second till has last month's prices, that is not a dormant risk waiting for an emergency. Customers are being charged the wrong amount today, and only about half of them.

A secondary holds a complete copy of the zone, obtained from the primary by a zone transfer, and answers authoritatively from it. To a resolver, primary and secondary are indistinguishable — both set aa, both are listed in the delegation.

A secondary is not a cache and not a load balancer, and confusing it with either causes real mistakes.

It is not a cache: it holds the whole zone, not the records somebody happened to ask for, and it is authoritative rather than counting a TTL down.

It is not a failover: resolvers spread queries across all the nameservers in the delegation, all the time. There is no primary-preferred behaviour. A secondary carrying stale data is not a spare wheel sitting in the boot — it is answering a quarter of your live traffic with the wrong answer.

And this is why the serial-number bug in C3 is so damaging. It does not take a server down. It makes one of your live, in-rotation servers quietly disagree with the others.

The four SOA timers from Module 02 B4 are the secondary's instructions:

TimerWhat the secondary does with it
REFRESHAsk the primary for its SOA this often, to compare serials
RETRYIf that check failed, try again after this long
EXPIREIf it has been unreachable this long, stop answering for the zone entirely
MINIMUMNot a secondary timer at all — the negative caching TTL, for resolvers

C2 · The zone transfer

The analogy. Think of updating the other till's price list.

You can photocopy the whole book — simple, always correct, and wasteful if only one price changed. Or you can send just the changed pages — far cheaper, but only possible if someone kept track of what changed.

Those are AXFR and IXFR, and the reason IXFR sometimes silently falls back to copying the whole book is that nobody kept the list of changes.

bash
cat > /tmp/lab/s/named.conf <<'EOF'
options {
    directory   "/tmp/lab/s";
    pid-file    "/tmp/lab/s/named.pid";
    listen-on port 5302 { 127.0.0.1; };
    listen-on-v6 { none; };
    recursion no;
    dnssec-validation no;
};

zone "lab.internal" {
    type secondary;
    file "/tmp/lab/s/zone.db";          // where the fetched copy is written
    primaries { 127.0.0.1 port 5301; };
};
EOF

setsid named -c /tmp/lab/s/named.conf > /tmp/lab/s.log 2>&1 < /dev/null
TransferWhat it sends
AXFRThe whole zone. Starts and ends with the SOA. Always over TCP — a zone will not fit in a datagram
IXFROnly the differences since the secondary's serial. Falls back to AXFR if the primary cannot compute a delta
🧪 Exercise C2.1 — Watch the first transfer, then read it off the wire
bash
grep -Ei 'transfer|zone lab' /tmp/lab/s.log | tail -6
dig @127.0.0.1 -p 5302 lab.internal SOA +noall +comments +answer
Expected result — click to reveal
plain text
zone lab.internal/IN: Transfer started.
transfer of 'lab.internal/IN' from 127.0.0.1#5301: connected using 127.0.0.1#5301
zone lab.internal/IN: transferred serial 2026081701
transfer of 'lab.internal/IN' from 127.0.0.1#5301: Transfer status: success
transfer of 'lab.internal/IN' from 127.0.0.1#5301: Transfer completed: 1 messages, 10 records, 332 bytes, 0.001 secs
zone lab.internal/IN: sending notifies (serial 2026081701)

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32294
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; ANSWER SECTION:
lab.internal.	300	IN	SOA	ns1.lab.internal. hostmaster.lab.internal. 2026081701 7200 900 1209600 300

The secondary now answers with aa set. It never read your zone file; it fetched the zone over the network and is serving it as authoritatively as the primary.

Three details in the log worth naming. 10 records — the whole zone, not a delta, because the secondary had nothing. 1 messages — the entire transfer fitted in one TCP message. And the secondary immediately sends notifies of its own, because it in turn could have secondaries beneath it.

And this is Module 04 B1 made concrete: 332 bytes in one TCP stream. A real zone with thousands of records is hundreds of kilobytes. There has never been a version of DNS where that could travel over UDP — which is why "TCP for zone transfers" is the one TCP answer everybody remembers.


C3 · The serial number, and the bug that comes from forgetting it

The analogy. Think of the version number printed on the front of the price list.

Staff at the other tills do not read the prices to decide whether to re-copy. They read the version number. Same number means "nothing to do" — so they do nothing, correctly.

Change the prices and forget to change the number, and half your tills will keep charging last month's prices forever. Nothing errors. Nobody is at fault. And it will never reproduce when you go and check one till.

A secondary asks the primary for its SOA and compares serials. It transfers only if the primary's serial is higher than its own. It does not compare the data. It does not look at file timestamps. It compares one number.

Which produces the most famous operational bug in DNS: edit the zone, forget the serial, and the primary serves new data while every secondary keeps serving the old.

Nothing errors. Nothing alerts. Both servers are healthy, both are authoritative, both are in the delegation, and they disagree. Users get one answer or the other depending on which nameserver their resolver happened to pick — so roughly half your traffic is correct and half is not, at random, and it never reproduces when you test it.

This is the intermittent, resolver-dependent failure from Module 03 C1. Commit it now, on purpose.

🧪 Exercise C3.1 — Change a record, forget the serial, and watch the two servers disagree
bash
cd /tmp/lab

echo "=== baseline: do they agree? ==="
printf 'primary   %s\nsecondary %s\n' \
  "$(dig @127.0.0.1 -p 5301 lab.internal A +short)" \
  "$(dig @127.0.0.1 -p 5302 lab.internal A +short)"

echo "=== edit the record, do NOT touch the serial, reload ==="
sed -i 's|^@   IN A   192.0.2.20|@   IN A   192.0.2.99|' p/zone.db
rndc -c /tmp/lab/rndc.conf reload lab.internal
sleep 3

printf 'primary   %s\nsecondary %s\n' \
  "$(dig @127.0.0.1 -p 5301 lab.internal A +short)" \
  "$(dig @127.0.0.1 -p 5302 lab.internal A +short)"
printf 'primary serial   %s\nsecondary serial %s\n' \
  "$(dig @127.0.0.1 -p 5301 lab.internal SOA +short | awk '{print $3}')" \
  "$(dig @127.0.0.1 -p 5302 lab.internal SOA +short | awk '{print $3}')"
Expected result — click to reveal
plain text
=== baseline: do they agree? ===
primary   192.0.2.20
secondary 192.0.2.20

=== edit the record, do NOT touch the serial, reload ===
zone reload queued

primary   192.0.2.99   <- new value
secondary 192.0.2.20   <- STILL THE OLD VALUE

primary serial   2026081701
secondary serial 2026081701

There it is. Two authoritative servers for one zone, giving two different answers, and both reporting the same serial.

Read the last two lines carefully, because they are the whole diagnosis. The serials are identical — so from the secondary's point of view there is nothing to do. It checked, saw no change, and correctly did nothing. The secondary is not broken. It is behaving exactly as specified.

Note also that rndc reload reported success and the primary genuinely did reload. Every component reported success. The only evidence anything is wrong is that the two servers disagree — and nothing in the system is looking for that.

This is why "same serial, different data" is the signature to memorise. If two authoritative servers report the same serial but return different records, somebody edited a zone file without bumping the serial. It is not a network problem, not a cache, not a delegation issue.

Now imagine this at 500 hosts. Four nameservers in the delegation, one edited without a serial bump. Twenty-five per cent of cold lookups get the old address. The application team sees a 25% error rate with no pattern, the network team sees healthy DNS servers, and nobody looks at serials because everything is "up".

The fix, and the discipline that prevents it:

bash
sed -i 's|2026081701 ; serial|2026081702 ; serial|' p/zone.db
rndc -c /tmp/lab/rndc.conf reload lab.internal

Then both servers return 192.0.2.99. Never edit a zone file by hand without a mechanical step that bumps the serial — a Makefile, a pre-commit hook, or generation from a template. Human memory is not a suitable mechanism, which is what every DNS-in-Git workflow exists to prove.

</callout>


C4 · NOTIFY — not waiting for REFRESH

The analogy. Think of tapping the other till on the shoulder instead of waiting for them to check the noticeboard at four o'clock.

And note what the tap actually says: not "here are the new prices", just "go and look". The other till still walks over and checks the version number itself.

That is why a fake tap on the shoulder is harmless — the worst it can do is make someone walk over for nothing.

Left alone, a secondary only checks for changes every REFRESH — 7200 seconds in our zone, two hours. That was acceptable in 1987 and is not now.

NOTIFY inverts it: when a primary loads a new serial, it tells the secondaries immediately. The secondary then does what it always does — asks for the SOA, compares serials, transfers if higher. NOTIFY is a hint to check now, not a push of data. It carries no records and is not trusted; the secondary still verifies for itself.

That design detail matters: a forged NOTIFY cannot poison anything. The worst it can do is cause an extra SOA query.

plain text
zone "lab.internal" {
    type primary;
    file "/tmp/lab/p/zone.db";
    allow-transfer { key xfer-key; };
    also-notify { 127.0.0.1 port 5302; };   // our secondary is on a non-standard port
    notify explicit;                        // notify ONLY the also-notify list
};
By default named notifies every server in the zone's NS records — which for our lab means ns1/ns2, both 127.0.0.1 on port 53, where nothing is listening. also-notify names extra targets explicitly, and notify explicit restricts notification to that list only.

The real-world version of this: secondaries that are not listed in the zone's NS records receive no NOTIFY at all unless you add them to also-notify. Hidden primaries and out-of-band secondaries are exactly that case, and forgetting it means those servers only update on REFRESH — hours late, silently.


C5 · TSIG — authenticating the transfer

The analogy. Think of handing over the price list on a password rather than on a face.

Recognising faces sounds fine until staff move desks, agencies send someone new, or somebody simply looks the part. A shared password does not care where the person is standing.

The one catch worth remembering: the password here has a timestamp on it, so if the two clocks drift apart the password stops working even though nobody changed it.

Restricting transfers by IP address is weak: addresses are forgeable, and cloud secondaries move. TSIG authenticates the message instead, with a shared HMAC key — the same primitive as the rndc channel in B1.

bash
tsig-keygen -a hmac-sha256 xfer-key > /tmp/lab/tsig.key
cat /tmp/lab/tsig.key
plain text
key "xfer-key" {
	algorithm hmac-sha256;
	secret "Ob4v/djH49l0Ad9Vmd8QakFxrbmBDthwupKM9QmCtBE=";
};

The key block goes in both configurations. The primary permits transfers to holders of the key; the secondary is told to use it when talking to that server:

plain text
// primary
zone "lab.internal" { ... allow-transfer { key xfer-key; }; };

// secondary
server 127.0.0.1 { keys { xfer-key; }; };
🧪 Exercise C5.1 — Get refused, then succeed with the key
bash
# 1. no key
dig @127.0.0.1 -p 5301 lab.internal AXFR +noall +comments

# 2. with the key  (paste YOUR secret from tsig.key)
dig @127.0.0.1 -p 5301 lab.internal AXFR \
    -y "hmac-sha256:xfer-key:Ob4v/djH49l0Ad9Vmd8QakFxrbmBDthwupKM9QmCtBE=" \
    +noall +answer
Expected result — click to reveal
plain text
$ dig @127.0.0.1 -p 5301 lab.internal AXFR +noall +comments
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 39795
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

$ dig @127.0.0.1 -p 5301 lab.internal AXFR -y "hmac-sha256:xfer-key:..." +noall +answer
lab.internal.		300	IN	SOA	ns1.lab.internal. hostmaster.lab.internal. 2026081702 7200 900 1209600 300
lab.internal.		300	IN	NS	ns1.lab.internal.
lab.internal.		300	IN	NS	ns2.lab.internal.
lab.internal.		300	IN	A	192.0.2.99
api.lab.internal.	300	IN	A	192.0.2.30
api.lab.internal.	300	IN	A	192.0.2.31
ns1.lab.internal.	300	IN	A	127.0.0.1
ns2.lab.internal.	300	IN	A	127.0.0.1
www.lab.internal.	300	IN	CNAME	lab.internal.
lab.internal.		300	IN	SOA	ns1.lab.internal. hostmaster.lab.internal. 2026081702 7200 900 1209600 300

The first is status: REFUSED — the exact code you got from a.iana-servers.net in Module 02 D3.1. Now you have seen it from the other side: it is allow-transfer doing its job. REFUSED means policy, and here you are the policy.

The second is the entire zone, and note what it starts and ends with: the SOA, twice. That is not a duplicate — it is how AXFR delimits itself. The trailing SOA is the end-of-transfer marker.

This is the open-AXFR finding from Module 02 D3, seen from the server side. A zone with allow-transfer { any; } hands that entire list to anyone who asks: every internal hostname, every staging system, every management interface. The correct setting is a key, not an address list — addresses are forgeable and cloud secondaries change theirs.

One practical warning about TSIG: it is time-sensitive. The HMAC covers a timestamp, and BIND rejects signatures outside a small window — by default five minutes. Clock skew between primary and secondary breaks zone transfers with a TSIG error and nothing else obviously wrong. If transfers fail with a clean key on both sides, check NTP before you check anything else.

And obviously: that secret is a credential. It authenticates a full zone dump. Never commit it to Git in the clear, and rotate it when people leave.

🎯 Interview questions — Primary, secondary and transfers

Q. What is the difference between AXFR and IXFR?

AXFR transfers the whole zone; IXFR transfers only the changes since the secondary's current serial. Both run over TCP, since neither fits in a datagram, and IXFR falls back to AXFR if the primary cannot compute a delta — commonly after a restart or a hand-edited zone file.

Why it matters at scale rather than in a lab: on a zone with hundreds of thousands of records, AXFR on every small change is expensive in bandwidth and in load on the primary, and it lengthens the window in which secondaries disagree. IXFR turns a two-record change into a two-record transfer.

The practical caveat worth adding: IXFR requires the primary to keep a journal of changes, so hand-editing the zone file and reloading typically forces a full AXFR anyway — which is one of the arguments for dynamic updates over file editing on large zones.

Q. You changed a record on the primary and the secondaries did not pick it up. Why?

Almost always: the serial was not incremented. A secondary compares serials, not data — so if the number did not change, it correctly concludes there is nothing to do. The signature is two authoritative servers reporting the same serial while returning different records.

Other causes: NOTIFY not reaching the secondary because it is not in the zone's NS records and not in also-notify; allow-transfer or TSIG rejecting the transfer; or clock skew breaking the TSIG signature.

The detail that separates a strong answer: pointing out that this is not an outage. Both servers are healthy and in rotation, so a fraction of live traffic gets the wrong answer at random and it never reproduces on demand. And the real fix is not "remember to bump the serial" — it is to make the bump mechanical: generate the zone, or use a pre-commit hook. Human memory is not a control.

Q. How do you secure zone transfers?

allow-transfer restricted to a TSIG key rather than an address list, with a global allow-transfer { none; } default and per-zone exceptions. TSIG signs each message with a shared HMAC key, so it survives NAT and changing secondary addresses in a way that IP-based ACLs do not.

Two things worth adding. First, the reason it matters: an open AXFR returns your entire zone to anyone who asks — every internal hostname, staging system and management interface — which is a decades-old audit finding that still appears, almost always on a forgotten secondary rather than the primary. Second, TSIG is time-sensitive: the signature covers a timestamp with a five-minute default window, so clock skew silently breaks zone transfers and NTP is the first thing to check when a correct key stops working.


Part D · Field recipes

D1 · The safe zone-change procedure

The analogy. Think of a price change done properly: change it, reprint it, then walk round and check every till shows the new price.

Most people stop after reprinting. The walk round is the only step that tests the thing customers actually experience.

Official docs: named-checkzone · rndc · RFC 1912
bash
#!/usr/bin/env bash
# zone-apply.sh <zone> <zonefile> — edit, validate, bump, reload, verify
set -euo pipefail
ZONE="$1"; FILE="$2"; RNDC="rndc -c /tmp/lab/rndc.conf"

# 1. bump the serial MECHANICALLY - never by hand
OLD=$(awk '/serial/{print $1}' "$FILE" | head -1)
NEW=$(( OLD + 1 ))
sed -i "s/${OLD} *; serial/${NEW} ; serial/" "$FILE"
echo "serial $OLD -> $NEW"

# 2. validate BEFORE the server sees it
named-checkzone "$ZONE" "$FILE" || { echo "ABORT: zone invalid"; exit 1; }

# 3. reload just this zone
$RNDC reload "$ZONE"
sleep 2

# 4. verify the PRIMARY loaded the new serial
P=$($RNDC zonestatus "$ZONE" | awk '/^serial:/{print $2}')
[ "$P" = "$NEW" ] || { echo "ABORT: primary still on $P"; exit 1; }

# 5. verify EVERY authoritative server agrees - this is the step people skip
for s in "127.0.0.1 -p 5301" "127.0.0.1 -p 5302"; do
  printf '  %-24s serial %s\n' "$s" \
    "$(dig @$s "$ZONE" SOA +short +norecurse | awk '{print $3}')"
done
Step 5 is the one that matters and the one that is always missing. Steps 1–4 confirm your server did what you asked. Step 5 confirms the fleet agrees — which is the only property users actually experience. A change that loaded on the primary and not the secondaries is not a completed change; it is C3's bug with extra confidence attached.

D2 · A secondary that will not update

The analogy. Think of the one till that never got the new prices.

Check the version number on its price list before you blame the photocopier. Nine times out of ten the number never changed, which means the till behaved perfectly and the mistake was upstream.

Check, in this orderWhat it rules out
Compare serials on every serverSame serial + different data = the serial was not bumped. Stop here; nothing else is wrong
rndc zonestatus on the primary — is serial what you wrote?The file never loaded. Check for a named-checkzone failure in the log
grep -i transfer the secondary's logTransfers attempted and failing, versus never attempted at all
dig @primary zone AXFR -y key from the secondary's hostallow-transfer or TSIG rejecting it. REFUSED means policy
Compare clocks on both hostsTSIG signatures expire in five minutes. Skew breaks a perfectly good key
Is the secondary in the zone's NS set or in also-notify?If neither, it gets no NOTIFY and only updates on REFRESH — hours late
rndc retransfer <zone> on the secondaryForces a fetch ignoring the serial. If this fixes it, the problem was the serial

D3 · Hardening checklist

The analogy. Think of locking the stockroom, and not taping your full supplier list to the shop window.

Neither costs you a single sale. Both remove a great deal of what a stranger can learn or misuse — which is exactly the character of every line in the table below.

plain text
options {
    recursion no;                       // authoritative only. Non-negotiable
    allow-transfer  { none; };          // default deny, permit per zone with a key
    allow-query     { any; };           // authoritative data is public by design
    allow-update    { none; };          // no dynamic updates unless you mean it
    version         none;               // do not advertise your BIND version
    minimal-responses yes;              // smaller answers, less amplification
    rate-limit { responses-per-second 20; window 5; };   // RRL - Module 09
};
controls {
    inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { "rndc-key"; };
};
Two of those lines are the ones auditors actually check, and both are Module 01 and 02 findings seen from the server side. recursion no closes the open-resolver amplification hole. allow-transfer { none; } plus a per-zone TSIG key closes the open-AXFR hole that hands your entire internal hostname list to anyone who asks.

rate-limit is Response Rate Limiting, which caps how many identical responses you will send to the same source — the standard defence against being used as a reflector. Module 09 covers what it does and does not stop.

version none is minor but free. Advertising BIND 9.11.4 in a version.bind query tells an attacker exactly which CVEs to try.


Part E · Putting it together

E1 · How this all fits — the complete picture

Diagram source
flowchart TD
    ED["you edit zone.db<br>and BUMP THE SERIAL"] --> CK["named-checkzone<br>validate BEFORE loading"]
    CK -->|"fails"| STOP["nothing loads<br>old data still served"]
    CK -->|"OK"| RL["rndc reload zone"]
    RL --> P["PRIMARY<br>type primary<br>reads the file<br>new serial live"]
    P -->|"NOTIFY<br>RFC 1996"| S["SECONDARY<br>type secondary"]
    S --> CMP{"primary serial<br>HIGHER than mine?"}
    CMP -->|"no"| NOOP["do nothing<br>SILENT DIVERGENCE<br>if you forgot the bump"]
    CMP -->|"yes"| XFR["AXFR or IXFR<br>over TCP<br>TSIG-authenticated"]
    XFR --> SERVE["secondary serves<br>the new data, aa set"]
    P --> SERVE2["primary serves<br>the new data, aa set"]
    SERVE --> USERS["resolvers pick ANY<br>of the delegated servers"]
    SERVE2 --> USERS
    NOOP --> USERS
    style NOOP fill:#ef4444,color:#fff
    style STOP fill:#f59e0b,color:#fff
    style USERS fill:#8b5cf6,color:#fff

Four ideas, and the rest of the module follows from them.

  1. recursion no is what makes a server authoritative. One binary, two jobs; running both publicly is the open-resolver finding.
  2. A secondary compares one number, not the data. Forget the serial and two healthy servers disagree forever, with no error anywhere.
  3. A secondary is in live rotation, not a spare. Resolvers use all delegated servers, so stale secondary data is wrong answers now, not a dormant risk.
  4. Validate before loading, and verify the whole fleet after. named-checkconf -z before, serial comparison across every server after.

E2 · Production practice

HabitWhy
recursion no; on every authoritative serverOtherwise you are an open resolver and an amplifier for attacks on other people
Run named-checkconf **-z**, never plain named-checkconfWithout -z the zone files are never opened; a config pointing at a broken zone passes cleanly
Bump the serial mechanically — script, hook, or generated zonesHuman memory is not a control, and the failure is silent divergence rather than an error
After every change, compare serials across all authoritative serversThe only property users experience is fleet agreement. "The primary reloaded" is not that
rndc reload <zone>, never a restartA restart drops in-flight queries and re-reads config you may have half-edited
Remember reconfig does not re-read edited zone filesIt only picks up added or removed zones. People run it, see nothing change, and look elsewhere
allow-transfer keyed with TSIG, never an address listAddresses are forgeable and cloud secondaries renumber. An open AXFR is your whole internal inventory
Keep NTP healthy on every nameserverTSIG signatures expire in five minutes; clock skew breaks transfers with a valid key on both sides
Add out-of-band secondaries to also-notifyA secondary not in the zone's NS set receives no NOTIFY and updates only on REFRESH — hours late
Keep zone files in Git, apply through CIGives you review, history, mechanical serial bumps, and named-checkzone as a required check

E3 · Capstone exercise

Build the whole thing from an empty directory, then break it in the two ways that matter. No scrolling back.

Brief. From nothing, stand up a primary and a secondary for capstone.internal on 127.0.0.1 ports 5401 and 5402, satisfying all eight:

  1. The primary is authoritative-only and provably refuses recursion.
  2. Zone transfers are denied by default and permitted to the secondary by key, not by address.
  3. The secondary loads the zone at startup and answers with aa set.
  4. Changes on the primary reach the secondary in seconds, not hours — say which mechanism you used and why the default was not enough.
  5. Demonstrate the serial bug: change a record so the two servers disagree, then show the single piece of evidence that identifies the cause.
  6. Fix it, and show a command that would have caught it automatically.
  7. Show what an unauthorised AXFR returns, and explain which RCODE it is and why.
  8. Break TSIG deliberately in a way that is not a wrong key, and say how you would diagnose it in production.
Model answer — attempt it first, then click

1–3. The build. Primary named.conf needs recursion no; and allow-transfer { none; }; in options, with allow-transfer { key xfer-key; }; on the zone. Proof of requirement 1 is in the query output, not the config:

plain text
;; flags: qr aa rd;
;; WARNING: recursion requested but not available

ra absent and that warning line is the proof. Requirement 3's proof is aa present in the secondary's answers plus a Transfer status: success line in its log.

4. NOTIFY, via also-notify { 127.0.0.1 port 5402; }; notify explicit;. The default was not enough because named notifies the servers in the zone's NS records — and those point at port 53, where nothing in this lab is listening. Without it the secondary would wait out REFRESH, two hours.

5. The evidence is the serial comparison, not the record.

plain text
primary   192.0.2.99      primary serial   2026081701
secondary 192.0.2.20      secondary serial 2026081701

Identical serials with different data. That one line pair identifies the cause completely: the secondary compared serials, saw no change, and correctly did nothing. Anyone who reports "the secondary is broken" has not read it — the secondary is the only component behaving exactly as specified.

6. The fix is sed the serial and rndc reload. The command that would have caught it:

bash
for s in 5401 5402; do dig @127.0.0.1 -p $s capstone.internal SOA +short | awk '{print $3}'; done | sort -u | wc -l

Any answer other than 1 is a divergence. One line, cheap enough to run on every change and in monitoring — and it catches serial mistakes, failed transfers and expired zones with the same test.

7. status: REFUSED. It is RCODE 5 — policy, not failure. The server understood the request perfectly and declined it, exactly as Module 01 D3 defined and exactly what a.iana-servers.net returned in Module 02. Nothing is broken; you are simply not on the list.

8. Skew the clock, not the key. TSIG signs a timestamp and BIND rejects signatures outside a five-minute window, so:

bash
sudo date -s "+10 minutes"     # on one host only

breaks every transfer while both configurations remain perfectly correct. In production you diagnose it by the log message — a TSIG error or clocks are unsynchronized rather than BADKEY — and by comparing date on both hosts. The instinct to check NTP before re-checking a key that has not changed is what distinguishes someone who has met this before.

The five things most people miss:

  1. Proving requirement 1 from query output rather than the config file. "I wrote recursion no" is not evidence; the absent ra flag is
  2. Explaining why the NOTIFY default failed in requirement 4. The mechanism — notifies go to the NS set — is the answer, not the workaround
  3. Naming the serial comparison as the evidence in requirement 5, rather than the differing A record. The A record is the symptom; the matching serials are the diagnosis
  4. Giving a check that runs unattended in requirement 6. "Remember to bump the serial" is not a control
  5. Requirement 8 at all. Almost everyone breaks the key. Clock skew is the failure that actually happens in production, because keys are configured once and clocks drift daily

E4 · Official documentation

LinkCovers
BIND 9 Administrator Reference ManualThe whole product. Everything below is a chapter of it
Configurations and Zone Files · Name Server OperationsParts A and B — named.conf, zone types, rndc, logging
Advanced Configurations · Security ConfigurationsPart C — transfers, NOTIFY, TSIG — and Part D's hardening
Configuration ReferenceEvery directive. Search it for a keyword; do not read it
Manual pagesnamed, rndc, rndc-confgen, tsig-keygen, named-checkconf, named-checkzone
BIND 9 TroubleshootingISC's own version of Part D
RFC 5936 — AXFR · RFC 1995 — IXFRWhat a zone transfer is, and how a delta is computed
RFC 1996 — DNS NOTIFYFour pages. Explains why NOTIFY is a hint and not a push
RFC 8945 — TSIGShared-key message authentication, including the time window that causes clock-skew failures
RFC 1912 §2.2 · RFC 1982 — Serial Number ArithmeticSerial number conventions, and the modular arithmetic that makes wrap-around work
How to read these efficiently.

Read RFC 1996 end to end — it is four pages and it is the clearest explanation of why NOTIFY carries no data and cannot be used to poison anything.

Use the BIND Configuration Reference as a lookup only. Search the directive name. Reading it front to back is a way to lose an afternoon.

The Troubleshooting chapter is worth one pass before you need it, so you recognise the failure names when you meet them at 3am.

The offline route, and it is genuinely good on BIND. man named.conf is the full configuration reference on the machine. rndc -h lists every control command. named-checkconf -z and named-checkzone -D need no documentation at all — they tell you what the server believes, which beats any specification when you are debugging one specific zone.


E5 · Self-assessment

1. What single directive makes a BIND server authoritative rather than a resolver, and what happens without it?

recursion no;

Without it, the same binary will happily resolve arbitrary names for anyone who asks — an open resolver. That lets strangers consume your capacity and, far worse, makes you a reflector and amplifier: a small forged query produces a large response aimed at a victim.

It is the first line of an authoritative configuration, not a hardening step to get to later.

2. Why is named-checkconf alone not enough?

It parses named.conf only — it never opens the zone files. A configuration pointing at a missing, unreadable or invalid zone file passes cleanly, and then named starts and refuses to answer for that zone.

named-checkconf -z loads every zone as the server would. That flag is the difference between a pre-flight check and false confidence, and it belongs in CI rather than in someone's memory.

3. A secondary decides whether to transfer based on what?

One number: the primary's SOA serial, compared with its own. It transfers only if the primary's is higher. It does not compare records, file timestamps or checksums.

Which is exactly why forgetting to bump the serial produces silent divergence rather than an error — the secondary is doing precisely what it was told.

4. Two authoritative servers return different A records but the same serial. Diagnosis?

Someone edited the zone file on the primary and did not increment the serial. The primary reloaded and serves new data; the secondary compared serials, saw no change, and correctly did nothing.

Same serial + different data is the signature. It is not caching, not delegation, not a network problem — and the secondary is not broken.

5. Why is a stale secondary worse than a dead one?

Because resolvers spread queries across all the nameservers in the delegation, all the time. There is no primary-preferred behaviour and a secondary is not a spare.

A dead server produces retries and a small latency penalty. A stale one answers confidently with the wrong data for its share of live traffic — so a quarter of users get the old address at random, and it never reproduces on demand.

6. AXFR versus IXFR, and when does IXFR fall back?

AXFR sends the whole zone; IXFR sends only the changes since the secondary's serial. Both use TCP.

IXFR falls back to AXFR whenever the primary cannot compute a delta — commonly after a restart, or after the zone file was hand-edited rather than changed through a journal-aware mechanism like dynamic update.

At scale IXFR matters: it turns a two-record change on a large zone into a two-record transfer instead of hundreds of kilobytes.

7. What does NOTIFY actually send, and why is that design safe?

A hint that the zone changed — no records, no data. The secondary responds by doing what it always does: query the primary for its SOA, compare serials, and transfer only if higher.

That is what makes it safe: a forged NOTIFY cannot inject anything. The worst outcome is an extra SOA query, because the secondary verifies everything for itself.

Default targets are the servers in the zone's NS records; anything else needs also-notify.

8. Why authenticate zone transfers with TSIG rather than an IP allow-list?

Source addresses are forgeable, and cloud secondaries change addresses. TSIG authenticates the message with a shared HMAC key, so it survives NAT, renumbering and address spoofing.

It also fails safely: a wrong or missing key gets REFUSED rather than a partial transfer.

The operational catch is the time window — signatures cover a timestamp and expire in about five minutes, so clock skew breaks transfers with a perfectly correct key, and NTP is the first thing to check.

9. rndc reload versus rndc reconfig — when does the difference bite?

reload re-reads named.conf and the zone files. reconfig re-reads named.conf and picks up added or removed zones only — it does not re-read an unchanged zone statement's file.

It bites when you edit a zone file, run reconfig because it sounds safer, see no change, and go looking for the problem somewhere else. reload <zone> is the everyday command.

10. What would you put in monitoring for a zone you own?

A cross-server serial comparison: query every authoritative server for the zone's SOA and alert if the serials are not all equal. It catches missed serial bumps, failed transfers, and expired zones with one cheap check.

Alongside it: that every delegated server answers with aa set — the lame-delegation test from Module 03 — and that the parent's delegation matches the child's NS set.

What I would not alert on is any single server being briefly behind. Transfers take a moment; the alert should fire on sustained divergence, not on the propagation window.


Next — Module 06 · The Resolver Side of a Linux Host.

You can now read DNS and serve it. Module 06 is about the layer that has silently shaped every result in this track: what your application does, which is not what dig does. nsswitch.conf, /etc/hosts, the search list and ndots, systemd-resolved and that 127.0.0.1 address from Module 01 B2 — and why dig can return a perfect answer while curl connects somewhere else entirely.

📚 Sources for the interview questions

Every command, configuration file, log line and error message in this module was run for real on 20 August 2026: two named 9.18.39 processes on 127.0.0.1 ports 5301 and 5302, with a TSIG-authenticated transfer between them. The named-checkconf errors, the REFUSED on the unauthenticated AXFR, the transfer log, and the serial-divergence in C3.1 are all captured output, not reconstructions.

Specifications verified directly: RFC 1035, RFC 1912, RFC 1982, RFC 1995, RFC 1996, RFC 5936, RFC 8945, plus the BIND 9 ARM.

Question selection cross-referenced against publicly published 2026 DNS and networking interview question sets:

Answers were rewritten and deepened rather than reproduced. Published sets define primary and secondary and stop; they rarely mention that a secondary is in live rotation rather than standby, that the serial is the only thing compared, or that TSIG clock skew is the failure mode you will actually meet — and those are the parts that come up once an interviewer has run a nameserver themselves.

Spotted a mistake or want something added? Send me a note.