Module 05 — Running an Authoritative Server (BIND 9)
Updated 20 August 2026
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).
Part A · Standing up a nameserver
A1 · What named is, and installing it
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.
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.
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
named -V | head -2
which named rndc rndc-confgen tsig-keygen named-checkconf named-checkzone✅ Expected result — click to reveal
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-checkzoneSix 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
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.
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| Directive | Why it is there |
|---|---|
| listen-on port 5301 | A high port so the lab never collides with the system resolver on 53 |
| recursion no | The 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 primary | This server reads the zone from a file it owns. Formerly type master |
| dnssec-validation no | Validation is a resolver function. An authoritative-only server has nothing to validate |
🧪 Exercise A2.1 — Break the config three ways and read the errors
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
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 foundError 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
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.
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 thisA4 · Start it, and ask it a question
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.
# -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
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
;; ->>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.20Four 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
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.
rndc — remote 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.
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.
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
rndc -c /tmp/lab/rndc.conf status
rndc -c /tmp/lab/rndc.conf zonestatus lab.internal✅ Expected result — click to reveal
$ 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: nostatus 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
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.
| Command | What it does |
|---|---|
| rndc reload | Re-read named.conf and every zone file |
| rndc reload <zone> | Re-read one zone file. The everyday command |
| rndc reconfig | Re-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 flush | Empty the resolver cache. Meaningless on an authoritative-only server |
| rndc querylog on | Toggle query logging at runtime, with no restart |
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
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.
- 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
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.
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:
| Timer | What the secondary does with it |
|---|---|
| REFRESH | Ask the primary for its SOA this often, to compare serials |
| RETRY | If that check failed, try again after this long |
| EXPIRE | If it has been unreachable this long, stop answering for the zone entirely |
| MINIMUM | Not a secondary timer at all — the negative caching TTL, for resolvers |
C2 · The zone transfer
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.
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| Transfer | What it sends |
|---|---|
| AXFR | The whole zone. Starts and ends with the SOA. Always over TCP — a zone will not fit in a datagram |
| IXFR | Only 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
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
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 300The 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
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.
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
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
=== 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 2026081701There 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:
sed -i 's|2026081701 ; serial|2026081702 ; serial|' p/zone.db
rndc -c /tmp/lab/rndc.conf reload lab.internalThen 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
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.
That design detail matters: a forged NOTIFY cannot poison anything. The worst it can do is cause an extra SOA query.
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
};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
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.
tsig-keygen -a hmac-sha256 xfer-key > /tmp/lab/tsig.key
cat /tmp/lab/tsig.keykey "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:
// 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
# 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
$ 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 300The 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
Most people stop after reprinting. The walk round is the only step that tests the thing customers actually experience.
#!/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}')"
doneD2 · A secondary that will not update
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 order | What it rules out |
|---|---|
| Compare serials on every server | Same 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 log | Transfers attempted and failing, versus never attempted at all |
| dig @primary zone AXFR -y key from the secondary's host | allow-transfer or TSIG rejecting it. REFUSED means policy |
| Compare clocks on both hosts | TSIG 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 secondary | Forces a fetch ignoring the serial. If this fixes it, the problem was the serial |
D3 · Hardening checklist
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.
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"; };
};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:#fffFour ideas, and the rest of the module follows from them.
- recursion no is what makes a server authoritative. One binary, two jobs; running both publicly is the open-resolver finding.
- A secondary compares one number, not the data. Forget the serial and two healthy servers disagree forever, with no error anywhere.
- 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.
- Validate before loading, and verify the whole fleet after. named-checkconf -z before, serial comparison across every server after.
E2 · Production practice
| Habit | Why |
|---|---|
| recursion no; on every authoritative server | Otherwise you are an open resolver and an amplifier for attacks on other people |
| Run named-checkconf **-z**, never plain named-checkconf | Without -z the zone files are never opened; a config pointing at a broken zone passes cleanly |
| Bump the serial mechanically — script, hook, or generated zones | Human memory is not a control, and the failure is silent divergence rather than an error |
| After every change, compare serials across all authoritative servers | The only property users experience is fleet agreement. "The primary reloaded" is not that |
| rndc reload <zone>, never a restart | A restart drops in-flight queries and re-reads config you may have half-edited |
| Remember reconfig does not re-read edited zone files | It only picks up added or removed zones. People run it, see nothing change, and look elsewhere |
| allow-transfer keyed with TSIG, never an address list | Addresses are forgeable and cloud secondaries renumber. An open AXFR is your whole internal inventory |
| Keep NTP healthy on every nameserver | TSIG signatures expire in five minutes; clock skew breaks transfers with a valid key on both sides |
| Add out-of-band secondaries to also-notify | A 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 CI | Gives you review, history, mechanical serial bumps, and named-checkzone as a required check |
E3 · Capstone exercise
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:
- The primary is authoritative-only and provably refuses recursion.
- Zone transfers are denied by default and permitted to the secondary by key, not by address.
- The secondary loads the zone at startup and answers with aa set.
- Changes on the primary reach the secondary in seconds, not hours — say which mechanism you used and why the default was not enough.
- Demonstrate the serial bug: change a record so the two servers disagree, then show the single piece of evidence that identifies the cause.
- Fix it, and show a command that would have caught it automatically.
- Show what an unauthorised AXFR returns, and explain which RCODE it is and why.
- 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:
;; flags: qr aa rd;
;; WARNING: recursion requested but not availablera 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.
primary 192.0.2.99 primary serial 2026081701
secondary 192.0.2.20 secondary serial 2026081701Identical 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:
for s in 5401 5402; do dig @127.0.0.1 -p $s capstone.internal SOA +short | awk '{print $3}'; done | sort -u | wc -lAny 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:
sudo date -s "+10 minutes" # on one host onlybreaks 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:
- Proving requirement 1 from query output rather than the config file. "I wrote recursion no" is not evidence; the absent ra flag is
- Explaining why the NOTIFY default failed in requirement 4. The mechanism — notifies go to the NS set — is the answer, not the workaround
- 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
- Giving a check that runs unattended in requirement 6. "Remember to bump the serial" is not a control
- 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
| Link | Covers |
|---|---|
| BIND 9 Administrator Reference Manual | The whole product. Everything below is a chapter of it |
| Configurations and Zone Files · Name Server Operations | Parts A and B — named.conf, zone types, rndc, logging |
| Advanced Configurations · Security Configurations | Part C — transfers, NOTIFY, TSIG — and Part D's hardening |
| Configuration Reference | Every directive. Search it for a keyword; do not read it |
| Manual pages | named, rndc, rndc-confgen, tsig-keygen, named-checkconf, named-checkzone |
| BIND 9 Troubleshooting | ISC's own version of Part D |
| RFC 5936 — AXFR · RFC 1995 — IXFR | What a zone transfer is, and how a delta is computed |
| RFC 1996 — DNS NOTIFY | Four pages. Explains why NOTIFY is a hint and not a push |
| RFC 8945 — TSIG | Shared-key message authentication, including the time window that causes clock-skew failures |
| RFC 1912 §2.2 · RFC 1982 — Serial Number Arithmetic | Serial number conventions, and the modular arithmetic that makes wrap-around work |
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.
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:
- Top 25 DNS Interview Questions and Answers for 2026 — nitizsharma.com — zones and zone files, SOA and serial numbers, DNS forwarding, query logging
- Interview Questions & Answers for DNS — DevOpsSchool — "What is a zone? What types of zones are there?" verbatim
- Top 30 Most Common DNS Interview Questions — Verve AI — nameservers, zones and zone files; DNS security best practices
- 75+ Network Engineer Interview Questions for 2026 — Taggd
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.