🔁 Zone transfer: AXFR / IXFR / SOA
Zone transfer is how a zone's data moves between servers. A primary owns the records; secondaries pull them. The SOA serial number is the heartbeat: when it bumps, secondaries notice and refresh. AXFR always uses TCP; ordinary queries are UDP-first and fall back to TCP on truncation. Syslog tells you something interesting here, and one stale field can keep your secondary running yesterday's data for days.
The cast of characters#
| Term | Meaning |
|---|---|
| Primary (master) | Authoritative server that holds the editable master copy |
| Secondary (slave) | Authoritative server that pulls the zone from the primary |
| NOTIFY | RFC 1996 push: primary nudges secondaries the instant a zone changes. See query-types for DNS opcodes. |
| SOA serial | A 32-bit integer; bumping it tells secondaries "there's new data" |
| AXFR | Full zone transfer: every record, every time. Used for first sync and resyncs. |
| IXFR | Incremental transfer: only records that changed since serial N. RFC 1995. |
| TSIG | Shared-key authentication for transfers: RFC 8945 (obsoletes RFC 2845 and RFC 4635). See also ntp (clock skew breaks TSIG). |
The dance:
primary secondary
------ ---------
record change committed
SOA serial bumps --NOTIFY--> "you should refresh"
|
+--SOA query--> primary
"serial = N+1"
received N before
so secondary requests:
--IXFR from serial=N--> primary
sends delta
applied, serial now N+1If IXFR can't satisfy the gap (e.g., secondary too far behind), the secondary falls back to AXFR.
The SOA serial: the heartbeat#
Every commit to an auth zone increments the SOA serial. Without that increment, secondaries don't know anything changed and won't pull.
Most modern authoritative servers handle the bump automatically: any record-mutation API or UI commit increments the SOA. You generally don't maintain a serial by hand.
To watch the serial, query a real zone's SOA. Here is isitdns.net as a live example:
dig @1.1.1.1 isitdns.net SOA +shortcoleman.ns.cloudflare.com. dns.cloudflare.com. 2409741473 10000 2400 604800 1800The third field (2409741473) is the serial. This is a snapshot; the value you see on your run will differ. After any record change the serial will be higher.
nslookup -type=SOA isitdns.net 1.1.1.1Server: 1.1.1.1
Address: 1.1.1.1#53
Non-authoritative answer:
isitdns.net
origin = coleman.ns.cloudflare.com
mail addr = dns.cloudflare.com
serial = 2409741473
refresh = 10000
retry = 2400
expire = 604800
minimum = 1800nslookup labels each SOA field, which is helpful for learning the structure. dig +multiline does the same with inline comments:
dig @1.1.1.1 isitdns.net SOA +multilineisitdns.net. 1800 IN SOA coleman.ns.cloudflare.com. dns.cloudflare.com. (
2409741473 ; serial
10000 ; refresh (2 hours 46 minutes 40 seconds)
2400 ; retry (40 minutes)
604800 ; expire (1 week)
1800 ; minimum (30 minutes)
)nslookup has no
+dnssecequivalent, so it cannot request the RRSIGs that accompany a signed SOA. Usedig +dnssecfor DNSSEC inspection.
The serial format that survives manual edits cleanly is YYYYMMDDNN, the syntax RFC 1912 §2.2 recommends. Internal schemes (epoch-style, counter-based) are also fine. RFC 1982 serial-number arithmetic governs comparison: "bigger = newer" with proper wrap-around math is the contract. See rfc-index for a pointer to RFC 1982. RFC 1982 comparison is defined only when the difference is less than 2^31; serials differing by exactly 2^31 have undefined ordering. Avoid serial values near wrap-around boundaries during manual edits.
dig for transfers: AXFR and IXFR#
dig @<primary> <zone> AXFRThe primary must allow the transfer from your IP (see "Allow / restrict transfers" below). dig's AXFR output is one record per line, ending with the SOA repeated. RFC 5936 §2.2 requires the transfer to conclude with the same SOA record it opened with; that repeated SOA is the end-of-zone marker.
dig @<primary> <zone> IXFR=<serial>Provide the serial your secondary currently holds. If the primary cannot satisfy the incremental gap, it responds with a full AXFR instead.
nslookup has no portable IXFR capability. For AXFR, some implementations accept -type=AXFR, but support and output format vary by platform. Use dig for reliable transfer testing.
nslookup can't show IXFR or inspect transfer wire details. Use dig.
What the DNS header flags look like#
Transfer queries are normal DNS messages but the header flags tell you what kind:
| Query type | opcode | qr | aa (on response) | Transport |
|---|---|---|---|---|
| SOA poll | QUERY (0) | 0/1 | 1 if server is authoritative (primary or secondary) | UDP or TCP |
| IXFR | QUERY (0) | 0/1 | 1 | UDP-first per RFC 1995 §2; falls back to TCP when the response overflows. In practice most secondaries just use TCP. |
| AXFR | QUERY (0) | 0/1 | 1 | TCP always: entire zone wouldn't fit in UDP |
| NOTIFY | NOTIFY (4) | 0/1 | n/a | UDP (TCP fallback) |
You can see the opcode in any dig response. A SOA query shows the same opcode: QUERY the transfer request uses:
dig @1.1.1.1 isitdns.net SOA;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51141
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1The ad flag appears because isitdns.net is DNSSEC-signed and 1.1.1.1 validates the answer; an unsigned zone shows only qr rd ra. An AXFR request produces the same opcode: QUERY header. The difference is the qtype in the question section and the multi-message TCP stream in the answer.
nslookup does not expose opcode or flag fields. Use dig for wire-level inspection.
For NOTIFY specifically you won't normally see it from dig (it's primary→secondary), but a capture will show opcode: 4.
What syslog shows#
Most authoritative DNS servers log transfer events in a recognizable shape. A typical line looks like:
client 198.51.100.42#12345 (example.com): transfer of 'example.com/IN': AXFR started (serial 2024071701)
client 198.51.100.42#12345 (example.com): transfer of 'example.com/IN': AXFR endedRecent BIND also appends transfer statistics (messages, records, bytes, seconds) to the ended line.
Key fields:
| Field | What it tells you |
|---|---|
client | Who asked for the transfer (IP + ephemeral port) |
transfer of '<zone>/IN' | Which zone and class |
AXFR / IXFR | Which type |
started / ended | Bookends for the conversation |
If the transfer was denied:
client 198.51.100.42#12345 (example.com): zone transfer 'example.com/IN' denieddenied is the line you grep for when "my secondary stopped syncing" lands in the queue. It tells you the ACL is the cause.
In dnstap the transfer request reaching an authoritative server appears as an AUTH_QUERY message with qtype set to AXFR or IXFR (message types defined in dnstap.proto); whether the multi-message response stream is also captured varies by server. See wireshark-dns-filters for capture-level inspection of DNS traffic.
Allow / restrict transfers#
By default a well-configured authoritative DNS server refuses outside zone transfers. To open one to a specific peer, you add the peer's IP, a TSIG key, or both to the zone's allow-transfer ACL (or whatever the equivalent is called in your DNS server). Commercial appliances expose this as an ACL field on the zone object; managed DNS providers handle secondaries through a dashboard pairing flow rather than open-Internet AXFR.
Use TSIG for anything crossing administrative boundaries. An IP address is trivially spoofable on a misconfigured network. TSIG clock skew breaks authentication; keep NTP synchronized on every server involved (see ntp).
Real-world scenarios#
| Scenario | Mechanism |
|---|---|
| Internal replication inside a cluster | Most vendors handle this internally; you don't expose AXFR |
dig AXFR test from a known host | Add the host's IP to the zone's allow-transfer |
| External secondary (a partner pulls your zone) | TSIG-keyed entry in allow-transfer, share the key out-of-band |
| Migrating a zone to a new provider | Temporarily allow their IP / TSIG, they AXFR, then cut NS |
SOA timers: the knobs that govern transfer behavior#
The SOA RDATA contains four timers (and a serial); they control transfer cadence and freshness:
| Field | What it does | Reasonable default |
|---|---|---|
refresh | How often a secondary checks the primary's SOA (when NOTIFY is not trusted) | 3600 (1h) |
retry | If a refresh failed, when to retry | 900 (15m) |
expire | If the primary stays unreachable this long, the secondary stops serving the zone | 604800 (1w) |
minimum (negative TTL) | How long resolvers may cache NXDOMAIN answers | 1800 (30m) |
The SOA record itself also has a TTL (set at the zone level, outside the RDATA), which governs how long other resolvers may cache the SOA record.
Pulling minimum down before a change avoids long NXDOMAIN-caching pain when you add a record that previously didn't exist. See soa for a full walkthrough of each timer and the SOA record structure.
Gotchas#
- AXFR is TCP-only. A firewall that blocks TCP/53 (only allows UDP/53) breaks transfers silently. See anatomy-of-a-query for the TCP vs UDP decision logic.
- NOTIFY without authentication is spoofable. Well-configured secondaries accept NOTIFY only from their configured masters; check
also-notify/masterson each side. - TSIG clock skew. TSIG signs requests with a timestamp tolerance ("fudge") of 300 seconds recommended by RFC 8945 §10. NTP drift beyond that breaks transfers. See ntp.
- IXFR fallback to AXFR is silent in most server logs unless you turn up verbosity. If transfers feel slow, check whether they're falling back.
- A stub zone is not a secondary. Stubs copy NS + SOA (plus glue) only; they do not AXFR/IXFR the full zone. See stub.
- Never trust the
allow-transferdefault. BIND 9.18 and earlier default to allowing transfers to any host; BIND 9.20 changed the default to deny (9.20.0 release notes). NSD and Knot deny without an explicit ACL; PowerDNS allows localhost only. Set the ACL explicitly on every zone: an open AXFR hands your entire zone to anyone who asks. - Dynamic update (RFC 2136) is different. DDNS updates the zone in-place and bumps SOA, but is unrelated to AXFR/IXFR, those are server-to-server. See ddns.