control statements (and their behaviour):\n\n- `$TTL TIME` — the default TTL for any record below this line that\n doesn't specify its own. Required at the top of the file in\n modern zone files.\n- `$ORIGIN NAME` — set the origin used when expanding relative\n names below this line. Use it when a single zone file contains\n records from multiple subdomains, although doing this is\n unusual.\n- `$INCLUDE FILE [ORIGIN]` — splice another file in at this point.\n Useful for breaking a huge zone into manageable chunks.\n- `$GENERATE RANGE LHS [TYPE] RHS` — BIND extension that produces\n a series of records. Often used to generate large reverse-zone\n PTR ranges:\n\n ```\n $GENERATE 1-254 $ PTR host-$.example.com.\n ```\n\n produces 254 PTR records for `1.example.com.` through\n `254.example.com.` (in the appropriate `in-addr.arpa.` zone).\n\n### `@` and the \"ditto\" rule\n\n`@` stands for the current `$ORIGIN`. Used most often on the SOA\nand apex records:\n\n```\n@ IN SOA ns1.example.com. hostmaster.example.com. ( … )\n@ IN NS ns1.example.com.\n@ IN NS ns2.example.com.\n@ IN A 192.0.2.10\n```\n\nA line that starts with whitespace inherits the name from the\nprevious record:\n\n```\nwww IN A 192.0.2.10\n IN A 192.0.2.11\n IN AAAA 2001:db8::10\n```\n\nAll three records belong to `www.example.com.`. This is the\nclassic compact form for multi-record names; whether to use it or\nto repeat the name is a style choice. Repeating the name is more\nverbose but more grep-friendly.\n\n### Time-value syntax\n\nTime fields (TTL, SOA refresh/retry/expire/min) accept either a\nplain number of seconds or a number with a unit:\n\n- `s` — seconds\n- `m` — minutes\n- `h` — hours\n- `d` — days\n- `w` — weeks\n\nSo `1h`, `60m`, and `3600` are equivalent. You can combine in\nsome implementations (`1h30m` = 5400 seconds).\n\n### Trailing dots — once more, with feeling\n\nThe trailing-dot rule is so common as a source of bugs that it's\nworth writing again:\n\n- `mail.example.com.` (with the trailing dot) is *absolute*.\n- `mail.example.com` (no trailing dot) is *relative to the current\n `$ORIGIN`*. If `$ORIGIN` is `example.com.`, this becomes\n `mail.example.com.example.com.`.\n- A single label like `www` (with no dots at all) is also relative,\n but typically what you want — it becomes `www.example.com.`\n under the same origin.\n- The MNAME and RNAME of an SOA record, and the targets of NS,\n MX, CNAME, PTR, and SRV records, all need to be either\n absolute (with the trailing dot) or short labels you intend to\n qualify.\n\nIf you ever see a record like `MX 10 mail.example.com` and the MX\ntarget resolves to a name like `mail.example.com.example.com.`,\nthis is the bug you're looking at. `named-checkzone` will catch\nthe obvious cases (the MX target won't resolve to an A record).\n\n### Reverse zone files\n\nThe forward and reverse zones are separate files with their own\nSOAs. A reverse zone file might look like:\n\n```\n$TTL 1h\n$ORIGIN 0.2.0.192.in-addr.arpa.\n\n@ IN SOA ns1.example.com. hostmaster.example.com. (\n 2025051601 1h 15m 2w 1h\n )\n@ IN NS ns1.example.com.\n@ IN NS ns2.example.com.\n\n10 IN PTR www.example.com.\n11 IN PTR web2.example.com.\n20 IN PTR mail.example.com.\n21 IN PTR mail2.example.com.\n```\n\nWithout `$ORIGIN`, you'd write the names out fully:\n\n```\n10.0.2.0.192.in-addr.arpa. IN PTR www.example.com.\n```\n\nMost teams set `$ORIGIN` to the reverse zone and use the short\nform. The PTR target (the right-hand side) is always a full,\nabsolute name with the trailing dot.\n\n### Pre-flight: linting before pushing\n\n```\nnamed-checkconf /etc/named.conf\nnamed-checkzone example.com /var/named/example.com.zone\n```\n\nThese two commands catch the overwhelming majority of zone-file\nbugs:\n\n- Syntax errors (typos, missing fields).\n- Unresolvable MX or NS targets within the zone.\n- Records that fall outside the zone.\n- Missing SOA or NS at the apex.\n- Forgotten trailing dots that produce nonsense names.\n\nRun them before every reload, ideally as a pre-commit hook on the\nzone-files repository.\n\n---\n\n## BIND configuration\n\nBIND is the reference open-source name server. The `named` daemon\nreads `named.conf` at startup, loads its zones, and starts answering\nqueries. Most of what you'll do as an operator either edits a zone\nfile or edits `named.conf`. The configuration syntax is its own thing\n— similar to C in superficial appearance, but with no relation to any\nDNS standard.\n\n### File layout\n\nCommon defaults vary by distribution but the shape is:\n\n- `/etc/named.conf` (RHEL, CentOS, Fedora) or `/etc/bind/named.conf`\n (Debian, Ubuntu) — the main configuration.\n- `/var/named/` (RHEL family) or `/var/cache/bind/` (Debian) — the\n working directory; zone files often live here, as do the\n serialized journal files (`*.jnl`) and statistics dumps.\n- `/var/log/named/` — log files, if you've configured the logging\n channel to write to disk rather than syslog.\n- `/etc/rndc.key` — TSIG key for `rndc` to talk to the server.\n\n### Comments\n\nThree syntaxes work, all equivalent:\n\n```\n/* C-style block comment */\n// C++-style line comment\n# shell-style line comment\n```\n\n### The basic statements\n\nA `named.conf` is a sequence of *statements*. The ones you'll use\nmost:\n\n- `options { ... }` — global options. Exactly one.\n- `zone \"NAME\" IN { ... }` — declare a zone you serve. One per zone.\n- `acl \"NAME\" { ... }` — define a named address match list.\n- `key \"NAME\" { ... }` — declare a TSIG key.\n- `controls { ... }` — configure the `rndc` control channel.\n- `logging { ... }` — configure logging channels and categories.\n- `view \"NAME\" { ... }` — split-horizon DNS (separate views of the\n same name to different clients).\n- `server IP { ... }` — per-remote-server overrides.\n- `include \"FILE\";` — splice another configuration file in.\n\n### A minimal authoritative-only `named.conf`\n\n```\noptions {\n directory \"/var/named\";\n listen-on { 127.0.0.1; 192.0.2.10; };\n listen-on-v6 { ::1; 2001:db8::10; };\n allow-query { any; };\n recursion no;\n version \"no\";\n minimal-responses yes;\n};\n\nzone \"example.com\" IN {\n type primary; # was \"master\" pre-BIND 9.16\n file \"primary/example.com.zone\";\n allow-transfer { 198.51.100.20; }; // ns2's IP\n also-notify { 198.51.100.20; };\n notify yes;\n};\n\nzone \"0.2.0.192.in-addr.arpa\" IN {\n type primary;\n file \"primary/192.0.2.rev\";\n};\n\nlogging {\n channel main {\n file \"/var/log/named/named.log\" versions 3 size 10m;\n severity info;\n print-time yes;\n print-category yes;\n print-severity yes;\n };\n category default { main; };\n};\n```\n\nA few things to note about this skeleton:\n\n- `recursion no` makes this server *authoritative-only* — it won't\n walk the tree on behalf of arbitrary clients. The modern best\n practice for any internet-facing authoritative server.\n- `version \"no\"` doesn't actually set the version; it tells the\n server to refuse to disclose its version. Cosmetic but reduces\n fingerprinting.\n- `minimal-responses yes` strips the `AUTHORITY` and `ADDITIONAL`\n sections from responses where they're not required, reducing\n payload size and amplification potential.\n- `listen-on`/`listen-on-v6` restrict which interfaces the server\n binds to. Without these, BIND listens on all interfaces.\n- The `notify` and `also-notify` statements inside the zone block\n are how the primary tells secondaries to refresh after a change.\n\n### A minimal recursive resolver `named.conf`\n\n```\noptions {\n directory \"/var/named\";\n listen-on { 127.0.0.1; 192.0.2.5; };\n listen-on-v6 { ::1; };\n allow-query { localnets; };\n allow-recursion { localnets; };\n recursion yes;\n forwarders { 1.1.1.1; 8.8.8.8; };\n forward first;\n dnssec-validation auto;\n minimal-responses yes;\n};\n\nzone \".\" IN {\n type hint;\n file \"named.ca\"; # the root hints file\n};\n\nzone \"0.0.127.in-addr.arpa\" IN {\n type primary;\n file \"primary/127.0.0.zone\";\n};\n```\n\nNotes:\n\n- `localnets` is a built-in ACL that matches any address on a\n network the server itself has an interface in. It's a cheap way\n to limit recursion to \"the network this server is on\".\n- `forwarders` plus `forward first` makes this resolver use the\n upstream resolvers when possible but fall back to walking the\n tree itself if the forwarders fail. `forward only` is the\n alternative if you never want it to walk the tree.\n- `dnssec-validation auto` turns on DNSSEC validation using the\n built-in trust anchors. With this on, the resolver will return\n `SERVFAIL` for DNSSEC-broken zones — which is correct behaviour\n but occasionally surprising when an external zone has botched\n its DNSSEC.\n\n### Address match lists and ACLs\n\nMany statements take an *address match list*. A match list is one\nor more of:\n\n- An IP address: `192.0.2.10`\n- A network in CIDR: `192.0.2.0/24`\n- The negation of any of the above: `!192.0.2.66`\n- Another ACL by name: `localnets`, `localhost`, `any`, `none`,\n or any user-defined name\n- A `key` reference: `key \"tsig-name\"` (so transfers/updates can be\n authorized by signature rather than IP)\n\nOrder matters; the first match wins.\n\n```\nacl \"trusted\" {\n 127.0.0.1;\n ::1;\n 192.0.2.0/24;\n !192.0.2.66; // explicitly excluded inside the trusted block\n};\n\noptions {\n allow-query { trusted; };\n allow-recursion { trusted; };\n};\n```\n\n### Zone types\n\n- `primary` (or `master`) — this server holds the editable copy.\n- `secondary` (or `slave`) — this server fetches a read-only copy\n from a primary via zone transfer.\n- `hint` — used only for the root zone; the file lists the root\n servers' addresses, used to bootstrap the recursor's\n understanding of where to start a tree walk.\n- `forward` — declares a zone whose queries should be forwarded to\n a specific set of servers, regardless of the global forwarders.\n- `stub` — a partial copy of another zone's NS records, used when\n you want to be a slave for the *delegation* but not the data.\n Rare in modern setups.\n- `static-stub`, `redirect`, `mirror` — newer specialised types,\n used in particular operational scenarios.\n\nA secondary zone block looks like:\n\n```\nzone \"example.com\" IN {\n type secondary;\n masters { 192.0.2.10; };\n file \"secondary/example.com.zone\";\n allow-transfer { none; }; // unless you have tertiaries\n};\n```\n\nSecondaries should `allow-transfer { none; }` unless they're\nthemselves serving as a master for further servers down the chain.\n\n### NOTIFY and incremental zone transfers\n\nWhen you change a zone on the primary and bump the SOA serial, the\nprimary sends a NOTIFY message to each name server listed in the\nzone's NS records (and to anything in `also-notify`). On NOTIFY,\nthe secondary immediately polls the primary, compares serials, and\nissues an *AXFR* (full zone transfer) or *IXFR* (incremental zone\ntransfer, RFC 1995) to fetch the new data.\n\nIXFR exchanges only the records that have changed since the\nsecondary's last serial, which is dramatically more efficient on\nlarge zones. Modern BIND uses IXFR by default if it can; if a\nsecondary's stored serial is too old or the journal has been\ntruncated, it falls back to AXFR.\n\n`rndc reload` and `rndc reload ZONE` ask the server to re-read its\nconfiguration / zone data; with NOTIFY plus IXFR, propagation to\nsecondaries is typically a few seconds.\n\n### Operational tools\n\n```\nnamed-checkconf [-z] [/etc/named.conf]\n # validate the config; -z also tries to load every zone\n\nnamed-checkzone example.com /var/named/example.com.zone\n # validate one zone file\n\nrndc reload\nrndc reload example.com\nrndc reconfig\nrndc retransfer example.com\nrndc flush # clear the recursive cache\nrndc flushname NAME # clear one name from the recursive cache\nrndc dumpdb -all # dump the cache and zone data to a file\nrndc dumpdb -cache # cache only\nrndc dumpdb -zones # zones only\nrndc stats # write statistics to a file\nrndc status # quick health summary\nrndc trace [LEVEL] # set debug level\nrndc notrace # turn off debug output\nrndc querylog on # log every query (expensive)\nrndc querylog off\nrndc stop # graceful shutdown\nrndc halt # immediate shutdown without sync\n\nsystemctl status named\nsystemctl reload named\nsystemctl restart named\njournalctl -u named -f\n```\n\n`rndc` talks to `named` over a control channel, authenticated by\na TSIG key in `/etc/rndc.key`. The control statement in the\nconfig:\n\n```\ncontrols {\n inet 127.0.0.1 port 953\n allow { 127.0.0.1; }\n keys { \"rndc-key\"; };\n};\n\nkey \"rndc-key\" {\n algorithm hmac-sha256;\n secret \"abcd1234...==\";\n};\n```\n\n`rndc-confgen` generates the key file. Distribution packages\ntypically set this up out of the box.\n\n### Logging\n\nThe `logging` statement defines *channels* (where messages go) and\n*categories* (which kinds of messages). Useful categories:\n\n- `default` — anything not matched by another category.\n- `general` — generic messages.\n- `queries` — every query (loud; only enable when debugging).\n- `query-errors` — only failed queries (useful in production).\n- `client` — client connections.\n- `network` — network-level events.\n- `resolver` — recursive resolver activity.\n- `xfer-in`, `xfer-out` — zone-transfer events.\n- `notify` — NOTIFY messages.\n- `update`, `update-security` — dynamic update events.\n- `dnssec` — DNSSEC validation, signing, and key events.\n- `lame-servers` — broken delegations the resolver has found.\n- `security` — denied queries / transfers / updates.\n\nChannels are file or syslog destinations with severity filters:\n\n```\nlogging {\n channel security_log {\n file \"/var/log/named/security.log\" versions 5 size 5m;\n severity info;\n print-time yes;\n print-severity yes;\n print-category yes;\n };\n channel xfer_log {\n file \"/var/log/named/xfer.log\" versions 5 size 5m;\n severity info;\n print-time yes;\n };\n\n category security { security_log; };\n category xfer-in { xfer_log; };\n category xfer-out { xfer_log; };\n category notify { xfer_log; };\n};\n```\n\n### Views (split-horizon DNS)\n\nSometimes you want internal clients to see different DNS data than\nexternal clients. BIND views let one server present different zone\ncontents based on who's asking.\n\n```\nview \"internal\" {\n match-clients { 192.0.2.0/24; };\n recursion yes;\n zone \"example.com\" IN {\n type primary;\n file \"primary/example.com.internal\";\n };\n};\n\nview \"external\" {\n match-clients { any; };\n recursion no;\n zone \"example.com\" IN {\n type primary;\n file \"primary/example.com.external\";\n };\n};\n```\n\n`match-clients` is evaluated in declaration order; the first\nmatching view wins. Once views are in use, *every* zone must be\ninside a view.\n\nViews are powerful but operationally costly — you have two zone\nfiles to keep consistent, two sets of records to update on changes,\ntwo transfer relationships to manage. Modern alternatives include\nhosting internal records in a separate, dedicated internal zone\n(e.g. `example.internal.`) or using a DNS-aware split tunnel on the\nclient side.\n\n### Forwarders revisited\n\nPer-zone forwarding is configured with a `forward` zone:\n\n```\nzone \"internal.corp.\" IN {\n type forward;\n forwarders { 10.0.0.5; 10.0.0.6; };\n forward only;\n};\n```\n\nThis is the common pattern when one zone lives on a different DNS\ninfrastructure (e.g. a Windows AD domain) but you want one resolver\nendpoint for everything.\n\n---\n\n## Query tools: dig, host, nslookup, delv\n\nYou will spend more time staring at `dig` output than at any other\nDNS tool. It's the operational gold standard: explicit, verbose, no\nhidden behaviour, no platform variations worth worrying about. The\nothers are useful in narrower situations.\n\n### dig\n\n```\ndig [@server] [domain] [type] [class] [+flags] [-options]\n```\n\nDefaults: query the resolver in `/etc/resolv.conf` for the `A`\nrecord of the given name in the `IN` class.\n\n#### Common forms\n\n```\ndig www.example.com # default A query, default resolver\ndig www.example.com AAAA # specifically IPv6 address\ndig example.com MX\ndig example.com NS\ndig example.com SOA\ndig example.com TXT\ndig example.com ANY # all types (commonly filtered)\ndig @8.8.8.8 example.com # query a specific resolver\ndig @ns1.example.com example.com SOA # ask the authoritative server directly\ndig +short example.com # just the answers, no headers\ndig -x 192.0.2.10 # reverse: ask for the PTR\ndig +trace example.com # walk from the root yourself\ndig +tcp example.com # force TCP\ndig +tries=1 +timeout=2 … # don't retry, fail fast\ndig -p 5353 @ns1.local example.com # non-default port\ndig -y key-name:secret example.com # TSIG-signed query\n```\n\n#### Reading dig output\n\nA typical `dig` output has four sections you actually care about,\nplus some commentary at the top and bottom.\n\n```\n; \u003c\u003c>> DiG 9.18.x \u003c\u003c>> www.example.com\n;; global options: +cmd\n;; Got answer:\n;; ->>HEADER\u003c\u003c- opcode: QUERY, status: NOERROR, id: 12345\n;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1232\n;; QUESTION SECTION:\n;www.example.com. IN A\n\n;; ANSWER SECTION:\nwww.example.com. 300 IN A 192.0.2.10\nwww.example.com. 300 IN A 192.0.2.11\n\n;; Query time: 18 msec\n;; SERVER: 1.1.1.1#53(1.1.1.1)\n;; WHEN: Sat May 17 09:22:13 PDT 2025\n;; MSG SIZE rcvd: 73\n```\n\nThe pieces:\n\n- **HEADER** line — opcode (`QUERY`, occasionally `UPDATE`),\n *status* (`NOERROR`, `NXDOMAIN`, `SERVFAIL`, `REFUSED`,\n `FORMERR`, `NOTIMP`, …), and a transaction ID.\n- **flags** — `qr` (this is a response), `aa` (authoritative\n answer), `tc` (truncated), `rd` (recursion desired by client),\n `ra` (recursion available from server), `ad` (DNSSEC\n authenticated), `cd` (checking disabled — DNSSEC validation\n bypassed by client request).\n- **counts** — how many records in each section.\n- **OPT PSEUDOSECTION** — EDNS0 metadata.\n- **QUESTION SECTION** — what was asked. Always one record.\n- **ANSWER SECTION** — the real records, when there are any.\n- **AUTHORITY SECTION** — the NS records for the zone the answer\n came from, or for the zone where the answer wasn't found.\n- **ADDITIONAL SECTION** — extra records the server thought you\n might want (often glue, OPT pseudosection details, EDNS\n metadata).\n- **Query time / SERVER / WHEN / MSG SIZE** — diagnostics.\n\n#### Status codes you'll see\n\n- **NOERROR** — the query succeeded. If the answer section is\n empty, that's \"name exists, but has no records of the requested\n type\" (NODATA).\n- **NXDOMAIN** — the name doesn't exist.\n- **SERVFAIL** — the server failed to produce an answer. Common\n causes: DNSSEC validation failure, broken authoritative server,\n recursion failure.\n- **REFUSED** — the server is unwilling to answer (often\n recursion turned off for non-trusted clients, or a query for a\n zone the server isn't authoritative for and isn't recursing on\n behalf of you).\n- **FORMERR** — the query was malformed. Almost always a tooling\n bug rather than something the user wrote.\n- **NOTIMP** — the server doesn't implement the requested\n operation.\n\n#### Useful +options\n\n- `+short` — strip everything except the answer values.\n- `+noall +answer` — same idea, slightly more readable for\n multi-record answers.\n- `+nocmd` — suppress the leading \"\u003c\u003c>> DiG …\" version line.\n- `+stats` — print the trailing stats block.\n- `+nostats` — suppress it.\n- `+trace` — perform an iterative resolution from the root,\n showing each delegation. Equivalent to walking the tree by hand.\n- `+norecurse` — don't ask the server to recurse; useful when\n pinging an authoritative server with `@server` and you want to\n prove it answers authoritatively.\n- `+dnssec` (or `+do`) — set the DO bit; ask for DNSSEC RRs.\n- `+nodnssec` — clear it.\n- `+cdflag` — set the CD (checking-disabled) bit; ask the resolver\n not to validate DNSSEC.\n- `+adflag` — display the AD flag handling.\n- `+nsid` — request the server's NSID (a server-identifier EDNS\n option, useful when the same anycast IP is many servers).\n- `+subnet=CIDR` — send EDNS Client Subnet (RFC 7871). Useful when\n testing geo-aware DNS.\n- `+bufsize=N` — UDP payload size to advertise via EDNS0.\n- `+tcp` — force TCP.\n- `+notcp` — disable fallback to TCP on truncation.\n- `+retry=N`, `+tries=N`, `+timeout=S` — retry/timeout tuning.\n- `+keepopen` — reuse one TCP connection across multiple queries on\n the same `dig` command line.\n\nYou can pass several names on one command line and `dig` will\nquery each in turn:\n\n```\ndig +noall +answer example.com mail.example.com www.example.com\n```\n\n#### Zone transfers with dig\n\n```\ndig @ns1.example.com example.com AXFR # request a full zone transfer\ndig @ns1.example.com example.com IXFR=2025051601 # incremental from this serial\n```\n\nThis is a quick smoke-test for whether a server allows transfers\nfrom your IP. Production transfers should always be authenticated\nwith TSIG.\n\n#### Bulk lookups\n\n```\ndig -f names.txt +noall +answer\n```\n\nreads names from a file, one per line, and runs each query.\nCombined with `xargs` and `awk`, useful for auditing thousands of\nrecords.\n\n### host\n\n`host` is `dig` for the impatient — terse, sensible defaults,\nnothing to read.\n\n```\nhost example.com # A and AAAA\nhost -t MX example.com\nhost -t NS example.com\nhost -t TXT example.com\nhost -a example.com # all (verbose) — shows every type the server returns\nhost -v example.com # verbose; closer to dig output\nhost 192.0.2.10 # reverse lookup\nhost example.com 1.1.1.1 # use a specific resolver\n```\n\n`host` is fine for one-line probes. For anything operational, use\n`dig`.\n\n### nslookup\n\n`nslookup` is the historical Unix DNS tool, present on every\nplatform including Windows. It has two modes: command-line and\ninteractive.\n\n```\nnslookup example.com\nnslookup -type=MX example.com\nnslookup -type=NS example.com\nnslookup example.com 8.8.8.8 # specific resolver\n\nnslookup\n> set type=mx\n> example.com\n> set type=ns\n> example.com\n> exit\n```\n\nPractical notes:\n\n- `nslookup`'s output is not stable across implementations (BSD\n vs GNU vs Windows). Don't parse it in scripts.\n- The `-` form on Windows / older Unix sometimes triggers an\n interactive mode if you forget the trailing query string.\n- For modern operational work, prefer `dig` everywhere except\n Windows servers where you don't have BIND tools installed.\n Windows ships `Resolve-DnsName` in PowerShell, which is\n generally a better choice than `nslookup`.\n\n### delv\n\n`delv` (\"Domain Entity Lookup & Validation\") is a DNSSEC-aware\nclient. It does its own validation against the configured trust\nanchors and tells you whether the answer is `fully validated`,\n`partially validated`, or unvalidated. Use it to test whether a\nzone's DNSSEC chain is intact.\n\n```\ndelv www.example.com\ndelv +rtrace www.example.com # show validation steps\ndelv +cd www.example.com # disable validation\ndelv -a /etc/bind/bind.keys example.com\n```\n\n### kdig and drill\n\n`kdig` ships with the Knot DNS toolkit; `drill` ships with\nNSD/Unbound. Both are functionally similar to `dig` with slightly\ndifferent option flags. Worth knowing they exist; `dig` is fine\nunless you have a specific reason to use one of these.\n\n### Resolve-DnsName (PowerShell)\n\nModern Windows operators use `Resolve-DnsName` instead of\n`nslookup`:\n\n```powershell\nResolve-DnsName example.com\nResolve-DnsName example.com -Type MX\nResolve-DnsName example.com -Server 8.8.8.8\nResolve-DnsName example.com -DnssecOk\nResolve-DnsName -Name example.com -Type ANY -Server ns1.example.com\n```\n\nReturns objects, not text — pipeable to `Where-Object`,\n`Select-Object`, etc.\n\n### Network-level tooling for DNS troubleshooting\n\nDNS lives on TCP and UDP port 53. Sometimes the answer to \"DNS\nisn't working\" is at the network layer:\n\n```\nnc -uvz host 53 # UDP port-knock\nnc -vz host 53 # TCP port-knock\ntcpdump -ni any port 53 # see DNS packets fly by\ntcpdump -ni any port 53 -w dns.pcap # capture for later analysis\ntshark -ni any port 53 # decoded DNS traffic\nss -tnlp | grep :53 # what's listening locally?\nss -unlp | grep :53 # UDP listeners\n```\n\nCombine these with `dig +trace` and authoritative-server probes\n(`dig @ns1 …`) to localise where the failure is happening.\n\n---\n\n## Operational tasks\n\nThese are the recipes you reach for repeatedly. Most are\n\"recipe-shaped\" — same five steps every time, with the values\nfilled in.\n\n### Add a record\n\n1. Edit the zone file.\n2. Bump the SOA serial. Always.\n3. `named-checkzone ZONE FILE` to validate.\n4. `rndc reload ZONE` (or `rndc reload` for everything).\n5. `dig @LOCAL_NS NEW_NAME TYPE` to confirm the record is served.\n6. After `notify` propagates, query each authoritative server\n listed in NS records: `dig @nsX NEW_NAME TYPE +short`.\n7. Once all secondaries agree, you're done.\n\n### Remove a record\n\nSame process as add. The thing to watch is *whatever depends on\nthe record* — if any cache (yours, your customers', a CDN) still\nholds a copy, the record will continue to resolve until the TTL\nexpires. To remove a record promptly, lower the TTL well in\nadvance:\n\n1. Lower the TTL to (e.g.) 300 seconds. Bump SOA. Reload.\n2. Wait for *more than* the previous TTL — every cache in the\n world that still has the old record now has the\n short-TTL version.\n3. Remove the record. Bump SOA. Reload.\n4. Within the new TTL, every cache will discard it.\n\n### Move a service to a new IP\n\nThe \"TTL game\". Plan it in advance:\n\n1. T-2 days: Lower the record's TTL to 300 (or whatever short\n value you can stand). Bump SOA. Reload. Verify the lowered\n TTL is what resolvers worldwide are now caching by querying\n external resolvers and watching the TTL count down to 300.\n2. T-0: Change the record to the new IP. Bump SOA. Reload.\n3. Watch the new IP receive traffic; watch the old IP empty out\n over the next 5 minutes.\n4. Once traffic at the old IP is at zero, decommission it.\n5. T+ a week: Raise the TTL back to its original value once\n you're confident no further changes are coming.\n\n### Decommission a host\n\nRemove every record that points to it (forward `A`/`AAAA`,\nreverse `PTR`, any `MX` listing it, any `SRV` listing it) before\nyou turn the host off. Drop the TTLs first (TTL game above) so\ncaches expire promptly. Don't pull the host until DNS has caught\nup; otherwise you'll have a window where queries succeed and\nconnections fail.\n\n### Stand up a new secondary\n\n1. On the new secondary, configure a `secondary` zone block\n pointing at the primary's IP, with the matching TSIG key if\n you're using one (you should be — see TSIG below).\n2. On the primary, add the new secondary's IP to `allow-transfer`\n and `also-notify`.\n3. Add the new secondary's hostname to the zone's NS records and\n bump the SOA serial.\n4. `rndc reconfig` on both ends.\n5. Watch the secondary log: it should issue an AXFR, then start\n responding.\n6. Test with `dig @new-secondary ZONE SOA +norecurse` — you should\n see the same SOA serial as the primary.\n7. Lastly, ask the parent zone's administrator to update the\n delegation NS records to include the new secondary. Until the\n delegation is updated, public traffic still won't reach the new\n secondary; but cache and existing zone-transfer subscribers will\n notice it.\n\n### Retire a secondary\n\nReverse the above process. Remove the NS record, wait for\ndelegation TTL, remove from `allow-transfer` / `also-notify`,\nshut down the secondary.\n\n### Rotate a TSIG key\n\n1. Generate a new key alongside the old.\n2. Distribute the new key to both ends.\n3. Configure both primary and secondary to accept *either* key\n (BIND supports multiple keys per server statement).\n4. `rndc reconfig` both ends.\n5. Verify zone transfers work using the new key (force one with\n `rndc retransfer`).\n6. Remove the old key from both configurations.\n7. `rndc reconfig` again.\n\n### Force a zone transfer\n\n```\nrndc retransfer ZONE\n```\n\non the secondary. The secondary contacts the primary and refetches\nthe zone. Useful when you've changed something the secondary\nhasn't picked up; usually a sign that NOTIFY isn't reaching it\n(firewall, IP mismatch in `also-notify`, etc.).\n\n### Flush the cache (recursor)\n\n```\nrndc flush # everything\nrndc flushname EXAMPLE.com.\n```\n\nAfter fixing data on the authoritative side, this lets the\nrecursor re-fetch instead of waiting for TTL expiration.\n\n### Inspect the cache\n\n```\nrndc dumpdb -cache /var/named/cache_dump.txt\nless /var/named/cache_dump.txt\n```\n\nUseful for \"is the bad answer in the cache, or is the upstream\ngiving me bad answers?\".\n\n### Check that all authoritatives agree\n\n```\nfor ns in $(dig +short example.com NS); do\n echo \"=== $ns ===\"\n dig @\"$ns\" example.com SOA +short\n dig @\"$ns\" www.example.com +short\ndone\n```\n\nIf the SOA serials disagree, you have a propagation problem — one\nsecondary is stale. Investigate NOTIFY and allow-transfer.\n\n### Rolling out a new authoritative server while keeping uptime\n\n1. Set up the new server as a secondary, fetching from your\n current primary.\n2. Wait until it has the zone fully and is responding correctly.\n3. Add it to the zone's NS records (so the parent zone learns\n about it).\n4. Wait out the parent's delegation TTL.\n5. Remove the old server from NS records, wait again, then turn\n it off.\n\nThis avoids any window where a queried name server is offline.\n\n### The dangerous changes\n\nA handful of operational changes have a long blast radius if you\nget them wrong. Treat them with extra care:\n\n- **Delegation changes.** Updating which name servers are\n authoritative for your zone with the parent. Wrong NS data\n here can take your domain offline for hours or days.\n- **DNSSEC key rolls.** A botched roll can `SERVFAIL` your zone\n worldwide. Dry-run with a test zone first; let `dnssec-policy`\n manage the timing for you in modern BIND.\n- **Apex record changes.** The apex is what most external\n consumers actually use; change windows here should respect TTLs\n on the order of 24h, not 1h.\n- **Cutting a zone.** Splitting a zone into a parent + delegated\n child is more invasive than it looks; the new child needs its\n own NS records, the parent's records inside that subdomain need\n to move to the child, and the cut moment needs careful\n coordination.\n\n---\n\n## Security: TSIG, DNSSEC, and the threat model\n\nDNS has two distinct security stories, addressing two different\nthreats.\n\n**TSIG** authenticates *transactions between cooperating servers*.\nIt guarantees that a zone transfer or dynamic update came from the\nexpected source and wasn't tampered with in flight. The shared\nsecret means TSIG works only between servers that have already\nexchanged keys out of band; it doesn't protect against an attacker\non the path between a client and a recursor, and it doesn't help an\narbitrary resolver verify that the answer it got from an\nauthoritative server is genuine.\n\n**DNSSEC** authenticates *the data itself*, end to end, so that any\nresolver — anywhere on the Internet — can prove cryptographically\nthat an answer it received originated from the zone owner and\nwasn't modified in transit. DNSSEC defends against cache poisoning,\non-path tampering, and any other attempt to substitute false\nanswers. It does not protect query confidentiality (use DoT or DoH\nfor that) and it does not stop denial-of-service.\n\nProduction zones in 2025 typically use both: TSIG for\nadmin-to-admin and primary-to-secondary, DNSSEC for the public-\nfacing answers.\n\n### TSIG — Transaction SIGnatures\n\nTSIG (RFC 8945, originally 2845) attaches an HMAC to a DNS message,\nkeyed on a shared secret known to both ends.\n\n#### Generating a key\n\n```\ntsig-keygen -a hmac-sha256 example-tsig > tsig.key\ncat tsig.key\n```\n\nOutput looks like:\n\n```\nkey \"example-tsig\" {\n algorithm hmac-sha256;\n secret \"abcd1234abcd1234abcd1234abcd1234abcd1234abcd==\";\n};\n```\n\nKeep this file readable only to root and the named user.\n\n#### Using the key on the primary\n\n```\ninclude \"/etc/bind/tsig.key\";\n\nzone \"example.com\" IN {\n type primary;\n file \"primary/example.com.zone\";\n allow-transfer { key \"example-tsig\"; };\n also-notify { 198.51.100.20 key \"example-tsig\"; };\n notify yes;\n};\n```\n\nNow the primary requires that any AXFR/IXFR request be\nTSIG-signed with the named key. The IP-based `allow-transfer`\ncheck is replaced (or augmented) by a cryptographic one — a\nmuch stronger guarantee.\n\n#### Using the key on the secondary\n\n```\ninclude \"/etc/bind/tsig.key\";\n\nserver 192.0.2.10 {\n keys { \"example-tsig\"; };\n};\n\nzone \"example.com\" IN {\n type secondary;\n masters { 192.0.2.10 key \"example-tsig\"; };\n file \"secondary/example.com.zone\";\n allow-transfer { none; };\n};\n```\n\nThe `server` clause says \"when talking to this IP, sign messages\nwith this key\", and the `masters` clause restates it for clarity.\n\n#### TSIG-signed dynamic updates\n\nA separate use of TSIG: signing `nsupdate` messages so the\nauthoritative server will accept dynamic updates from authorized\nclients only.\n\n```\nzone \"example.com\" IN {\n type primary;\n file \"primary/example.com.zone\";\n update-policy {\n grant updater-key zonesub *.example.com. A AAAA TXT;\n };\n};\n```\n\nA client signs its update with the matching `updater-key`. See\n\"Dynamic updates\" below.\n\n#### TSIG errors\n\nThe `BADTIME` and `BADKEY` errors are common when a primary and\nsecondary's clocks are skewed too far apart (TSIG signatures have\na built-in time window) or when the keys disagree:\n\n- **BADTIME** — clock skew exceeds the fudge factor (default 300s\n on each side). Run NTP/chrony.\n- **BADKEY** — the signing key isn't recognised by the receiver.\n- **BADSIG** — the signature didn't verify (data tampered, or\n wrong key on one side).\n\n`tsig verify failure` in named's logs is one of these.\n\n### DNSSEC — DNS Security Extensions\n\nDNSSEC adds cryptographic signatures to every record set in a\nzone, plus the metadata to walk a chain of trust from the root to\nyour zone. The pieces:\n\n- **DNSKEY** — the public keys for the zone. There are two roles:\n the *Key Signing Key* (KSK), which signs DNSKEY RRsets, and the\n *Zone Signing Key* (ZSK), which signs everything else. The\n separation lets you roll ZSKs frequently and KSKs rarely.\n- **RRSIG** — a signature record over each RRset.\n- **NSEC** / **NSEC3** — proof of *non-existence*. When a name\n doesn't exist, the authoritative server returns an NSEC record\n saying \"the names that do exist are X and Y, and the queried\n name falls between them, so it provably doesn't exist\". NSEC3\n hashes the names to make zone enumeration harder.\n- **DS** (Delegation Signer) — a hash of the child's KSK,\n published *in the parent zone*. This is the link in the chain\n of trust between parent and child.\n\nA client validating a name follows the chain: root → TLD → your\nzone, verifying signatures at each step. The root's keys are\nconfigured as the *trust anchor* in resolvers like BIND\n(`dnssec-validation auto`).\n\n#### Signing a zone\n\nModern BIND uses `dnssec-policy` to manage the entire signing\nlifecycle automatically. A minimal policy:\n\n```\ndnssec-policy \"default\" {\n keys {\n ksk lifetime unlimited algorithm 13;\n zsk lifetime P30D algorithm 13;\n };\n};\n\nzone \"example.com\" IN {\n type primary;\n file \"primary/example.com.zone\";\n dnssec-policy \"default\";\n inline-signing yes;\n};\n```\n\n`inline-signing yes` keeps an unsigned working copy of the zone\nand lets BIND maintain the signed copy automatically. With\n`dnssec-policy`, BIND generates keys, rolls them according to the\npolicy's lifetimes, and re-signs records before signatures\nexpire. You don't have to think about the details if you trust\nthe defaults.\n\nThe older, manual workflow (`dnssec-keygen`, `dnssec-signzone`)\nstill works but is worth replacing with `dnssec-policy` if you\ncan.\n\n#### Bootstrapping the chain to the parent\n\nAfter your zone is signed, the parent (your TLD or your\ndelegation provider) needs your DS record to complete the chain\nof trust. Steps:\n\n1. After signing, retrieve your DS record from your zone:\n\n ```\n dig @localhost example.com DS\n # or\n dnssec-dsfromkey -2 KSK_KEY_FILE\n ```\n\n2. Submit that DS record to your registrar/parent. Most modern\n registrar control panels have a DNSSEC tab where you paste in\n the DS values.\n\n3. Wait for the parent to publish the DS in their zone (delays\n vary; some are minutes, some are hours).\n\n4. Validate end-to-end with a DNSSEC-aware resolver:\n\n ```\n delv example.com\n ```\n\n You should see `; fully validated` in the output.\n\n#### Key rolls\n\nA *KSK roll* changes the parent's DS record, and is therefore\nmore involved than a *ZSK roll* which changes only signatures\ninside your zone. With `dnssec-policy`, BIND coordinates the\nroll: it publishes the new key, waits the configured time for\nold caches to expire, swaps the key in service, then retires the\nold key. For KSK rolls specifically, an automated mechanism\ncalled CDS/CDNSKEY (RFC 7344, RFC 8078) lets BIND publish the\nnew DS in your zone and have the parent automatically pick it\nup. Most modern registrars support this; check yours.\n\n#### When DNSSEC goes wrong\n\nA misconfigured or expired DNSSEC zone returns SERVFAIL to\nvalidating resolvers. Symptoms:\n\n- Some users (those whose recursors validate) can't resolve your\n domain at all; others (whose recursors don't validate, or have\n CD set) work fine.\n- `dig +dnssec` shows RRSIG records but the SERVFAIL still\n comes back when validation runs.\n- `delv` shows the validation step that failed.\n\nCommon causes:\n\n- The DS record at the parent doesn't match your KSK (you rolled\n but didn't update the parent).\n- A signature expired (the `dnssec-policy` machinery wasn't\n running, or the zone was forgotten). RRSIGs have an expiry\n built in; if the signing daemon stops, your zone breaks\n silently after the longest signature lifetime.\n- The chain of trust from the root has a break above you (rare,\n but newsworthy when it happens).\n\nThe emergency fix when your own DNSSEC has failed: turn off\nDNSSEC on the zone (or, if you're a recursor and a critical\nexternal zone is broken, turn on `dnssec-must-be-secure no` for\nthat zone, or use a \"negative trust anchor\" — `rndc managed-keys`\nwith appropriate options).\n\n#### DANE and TLSA\n\nA DNSSEC-secured benefit: you can publish a hash of your TLS\ncertificate in DNS via a TLSA record, and a DANE-aware client\nwill verify the certificate against that hash *before* trusting\nthe CA chain. This protects against rogue CAs. Not yet broadly\ndeployed, but the standard is mature.\n\n```\n_443._tcp.www.example.com. IN TLSA 3 1 1\n \u003cSHA256 hash of cert>\n```\n\n### Other security hardening\n\n- **Hide your version**: `version \"no\";` in `options`.\n- **Refuse recursion** on authoritative servers: `recursion no;`.\n- **Limit recursion** on resolvers: `allow-recursion { trusted; };`.\n- **Rate limiting**: `rate-limit { responses-per-second 10; };` —\n blunt-instrument protection against DDoS amplification.\n- **Run as a dedicated user**, not root.\n- **Consider chroot**: older BIND packaging shipped a `bind-chroot`\n variant; modern systems with systemd sandboxing achieve similar\n isolation through unit-file directives.\n- **Monitor**: SOA serial mismatches between primary and\n secondaries, SERVFAIL spikes, NXDOMAIN spikes (often\n reconnaissance), DNSSEC validation failures.\n- **Patch promptly** when CVEs come out. BIND has a regular\n cadence of security advisories.\n\n### Modern privacy: DoT, DoH, query minimisation\n\nDNS queries are sent in cleartext on UDP/53 by default. Anyone on\nthe path can see what names you're resolving. Two transports\nencrypt this:\n\n- **DoT** (DNS-over-TLS, RFC 7858) — uses TCP/853.\n- **DoH** (DNS-over-HTTPS, RFC 8484) — uses TCP/443.\n\nBoth protect the *stub-to-resolver* leg. The\n*resolver-to-authoritative* leg is still cleartext in most\ndeployments, though there are RFCs (DoT to authoritative,\nRFC 9103) addressing this. *Query name minimisation* (RFC 9156,\nimplemented in modern recursors) sends only the relevant label of\nthe query name to each authoritative server during a tree walk,\nlimiting how much information any single authoritative server\nsees.\n\nFor an end-user resolver (laptop, phone), pointing at a DoH or\nDoT-capable resolver like `1.1.1.1` is a no-cost privacy\nimprovement. For an enterprise resolver, DoT inside the\norganisation plus regular DNS to authoritatives is a common\ncompromise.\n\n---\n\n## Troubleshooting recipes\n\nThe vast majority of DNS issues fall into a handful of patterns.\nKnowing the patterns means recognising them in seconds rather than\ndebugging from first principles.\n\n### \"The site is down\"\n\nFirst, isolate whether DNS is the problem at all. Three quick\nqueries from the affected machine, in order:\n\n```\ndig +short example.com # answer from default resolver\ndig +short example.com @8.8.8.8 # answer from an external resolver\ndig +short example.com @ns1.example.com # answer straight from the authoritative\n```\n\nOutcomes:\n\n- **All three agree, and the IP is reachable** → DNS is fine; the\n problem is at the application layer, the network, or the load\n balancer. Move on to `curl -vk https://...`, `nc`, `ping`.\n- **All three agree, and the IP is not reachable** → DNS is fine;\n the problem is the host or its network.\n- **Authoritative agrees with itself but external resolvers\n disagree** → propagation problem. The cache somewhere has stale\n data; wait out the TTL or, if it's your own resolver, flush.\n- **Authoritative disagrees with itself across NS records** → one\n of your secondaries didn't pull the latest zone. Check the SOA\n serial on each NS:\n\n ```\n for ns in $(dig +short example.com NS); do\n s=$(dig +short @\"$ns\" example.com SOA | awk '{print $3}')\n echo \"$ns -> serial $s\"\n done\n ```\n\n- **External resolver returns SERVFAIL** but the authoritative\n works → DNSSEC validation failure on the external resolver's\n side, OR an authoritative server is unreachable from that\n resolver's network.\n- **Default resolver returns nothing** but `@ns1` works → your\n resolver is down, or a forwarder it depends on is down, or\n there's a path problem from your machine to the resolver.\n\n### \"The new record isn't visible\"\n\n```\ndig +short newhost.example.com @ns1.example.com # is it actually in the zone?\n```\n\nIf yes:\n\n- Check that all secondaries have the latest SOA serial:\n\n ```\n for ns in $(dig +short example.com NS); do\n dig @\"$ns\" example.com SOA +short\n done\n ```\n\n- Wait out the TTL of the NXDOMAIN that was previously cached, or\n flush the local resolver:\n\n ```\n sudo systemd-resolve --flush-caches # systemd-resolved\n sudo killall -HUP mDNSResponder # macOS\n ipconfig /flushdns # Windows\n sudo rndc flush # local BIND recursor\n ```\n\n Browsers cache DNS independently of the OS — restart the\n browser, or test with `curl` to bypass the browser cache.\n\nIf the record isn't in the zone on the authoritative server, you\nforgot to bump the SOA, didn't reload, or edited the wrong file.\n\n### \"DNSSEC says SERVFAIL\"\n\n```\ndig +dnssec +cd example.com # bypass validation\ndelv example.com # validating client\n```\n\nIf `+cd` works and `delv` shows a chain failure, the DNSSEC chain\nis broken. Common causes:\n\n- The parent zone's DS record doesn't match your KSK (registrar\n not updated after a roll).\n- Your zone's signatures expired (signing daemon stopped, or\n policy hasn't run).\n- An RRset has been added that wasn't signed.\n\nLook at the `delv` output for the specific failure. The fastest\nfix is usually rolling the zone's keys forward (re-signing) and\nre-publishing the DS to the parent.\n\n### \"Wrong server is answering\"\n\nThe stub resolver is sending queries somewhere different from\nwhere you think. Check:\n\n```\ncat /etc/resolv.conf\nresolvectl status # systemd-resolved\nnetworksetup -getdnsservers Wi-Fi # macOS\n```\n\nCommon causes:\n\n- VPN client added a name server when connecting and didn't\n remove it cleanly.\n- DHCP is overriding `/etc/resolv.conf`.\n- macOS per-domain `/etc/resolver/` files are routing some names\n to a corporate resolver.\n- The browser is using DoH directly to a public resolver,\n bypassing the OS.\n\n### \"The secondary won't pull the zone\"\n\n```\ndig @primary-ip example.com AXFR # try a transfer manually\n```\n\n`Transfer failed.` → check `allow-transfer` ACL on the primary,\nTSIG keys on both ends, time skew if TSIG is in use, firewall\nbetween primary and secondary on TCP/53.\n\n```\njournalctl -u named -f # both ends\ntail -F /var/log/named/named.log\n```\n\nLook for `xfer-in:`, `xfer-out:`, `transfer of`, `notify`,\n`refused`, and `bad signature` messages.\n\n### \"Why is this query slow?\"\n\n```\ndig +stats example.com # see Query time\ndig +trace example.com # see each step\n```\n\nA slow trace usually points at a slow upstream (root → TLD →\nzone) or at a misconfigured authoritative server that's\ntiming-out on UDP and forcing TCP fallback. Use `dig +tcp` to\ntest that path independently.\n\n### \"Weird intermittent failures\"\n\nSeveral common patterns:\n\n- **DNSSEC at the recursor is dropping responses too large for\n UDP and the path filters TCP/53.** Test with `dig +tcp`.\n- **Anycast routing change**: queries that used to go to instance\n A now go to instance B, which doesn't have the same data\n (because zone replication isn't perfect). Use `+nsid` to\n identify which instance is answering.\n- **One secondary out of N is stale**: queries that hit it return\n old data, queries that hit the others return new data.\n- **EDNS firewall mishandling**: a firewall doesn't pass DNS\n packets larger than 512 bytes. Test with `dig +bufsize=512`\n vs `+bufsize=4096`.\n\n### \"Cache poisoning concern\"\n\nIf you're worried someone is injecting forged answers into your\nrecursor:\n\n- Run a DNSSEC-validating recursor (`dnssec-validation auto` or\n similar in your platform).\n- Make sure your recursor uses random source ports for outbound\n queries (BIND has done this since 9.5; you almost certainly\n don't need to configure it).\n- Restrict inbound queries to known clients\n (`allow-recursion`), or your recursor becomes the cache for\n the world *and* an open amplifier for DDoS.\n\n### Useful one-liners for diagnosis\n\n```\n# Compare what every authoritative server returns for a name\nfor ns in $(dig +short example.com NS); do\n echo \"== $ns ==\"\n dig @\"$ns\" www.example.com +short\ndone\n\n# Walk the tree by hand\ndig +trace +nodnssec example.com\n\n# What's my recursor and what TTL is it caching?\ndig www.example.com | head -20\n\n# Strip everything except the answer\ndig +short www.example.com\n\n# Get the SOA serial of every NS for a zone\nfor ns in $(dig +short example.com NS); do\n printf '%-35s %s\\n' \"$ns\" \"$(dig @\"$ns\" example.com SOA +short)\"\ndone\n\n# Detect a misconfigured PTR (forward and reverse don't agree)\nip=$(dig +short www.example.com)\nptr=$(dig +short -x \"$ip\")\nfwd=$(dig +short \"$ptr\")\necho \"$ip -> $ptr -> $fwd\"\n# Should: $ip == $fwd. If not, you have a forward-confirmed-reverse-DNS bug.\n```\n\n---\n\n## Dynamic updates\n\nDNS records can be added, modified, or removed at runtime via\n*dynamic update* (RFC 2136). The protocol uses an UPDATE opcode (as\nopposed to QUERY) and is most commonly used for:\n\n- DHCP servers updating PTR records as leases come and go.\n- Active Directory clients registering themselves with the AD-DNS.\n- ACME/DNS-01 certificate validation, where Certbot or similar\n briefly creates a `_acme-challenge` TXT record to prove control\n of the domain.\n- Cloud automation that maintains records for ephemeral compute.\n\n### Configuring a zone for updates\n\n```\nzone \"example.com\" IN {\n type primary;\n file \"primary/example.com.zone\";\n update-policy {\n grant updater-key zonesub * A AAAA TXT;\n };\n notify yes;\n};\n\nkey \"updater-key\" {\n algorithm hmac-sha256;\n secret \"abcd...==\";\n};\n```\n\n`update-policy` is more granular than the older `allow-update`\n(which is purely IP-based). The `grant` syntax allows specifying\nexactly what records the holder of a given key may change. The\n`zonesub` qualifier limits updates to subdomains of the zone;\nother qualifiers (`name`, `subdomain`, `wildcard`, `self`,\n`selfsub`, `external`) constrain in different ways.\n\n### Using `nsupdate` from the command line\n\n```\nnsupdate -k /etc/bind/updater.key\n> server ns1.example.com\n> zone example.com\n> update add foo.example.com 300 A 192.0.2.99\n> update add foo.example.com 300 AAAA 2001:db8::99\n> send\n> quit\n```\n\nTo remove a record:\n\n```\nnsupdate -k /etc/bind/updater.key \u003c\u003c EOF\nserver ns1.example.com\nzone example.com\nupdate delete foo.example.com A\nsend\nEOF\n```\n\nTo add and remove atomically (in one update message):\n\n```\nnsupdate -k /etc/bind/updater.key \u003c\u003c EOF\nserver ns1.example.com\nzone example.com\nupdate delete foo.example.com A\nupdate add foo.example.com 300 A 192.0.2.100\nsend\nEOF\n```\n\n### When to use dynamic update\n\n- **Yes** for DHCP-driven DNS, ACME challenges, ephemeral compute\n registration.\n- **Maybe** for application-driven DNS (a deployment tool adding\n records as part of a release). Worth keeping the human-edited\n zones separate from the dynamically-updated ones — sub-zone\n delegation is common (\"`dyn.example.com.` is for automation,\n everything else is hand-edited\").\n- **No** for one-off changes you'll forget about. Edit the zone\n file, bump the serial, reload — the audit trail is your VCS.\n\n### `journal` files\n\nWhen dynamic updates are in use, BIND maintains a `*.jnl` file\nalongside the zone file. The journal records updates so they\nsurvive a restart. Don't delete `.jnl` files casually — if they\nget out of sync with the zone file, you may lose updates or\nfail to start.\n\nTo safely \"freeze\" a zone for hand-editing:\n\n```\nrndc freeze ZONE # stop accepting updates, flush journal to file\n# edit the zone file\nrndc thaw ZONE # resume updates\n```\n\nThis is the right way to make a one-off manual edit on a\ndynamically-updated zone.\n\n---\n\n## Networking utilities you'll reach for alongside DNS\n\nSometimes the answer to \"DNS isn't working\" is \"DNS is fine, the\nnetwork isn't\". A small toolbox of networking utilities lives next\nto `dig` in your operator brain.\n\n### Reachability\n\n```\nping -c 4 host # ICMP echo\nping6 host # IPv6 (or `ping -6`)\nping -c 4 1.1.1.1 # IP literal: bypass DNS entirely\nfping -c 4 host1 host2 host3 # parallel\nmtr host # interactive ping+traceroute (install separately)\nmtr --report -c 50 host # batch mode, useful in scripts\n```\n\n`mtr` is invaluable when you suspect a network path issue. It\nshows per-hop loss and latency, and you can read the pattern in\nthe table: a single hop with high loss followed by clean hops is\nthat hop's problem; rising loss from a hop onward is everything\nbeyond that hop being affected.\n\n### Path tracing\n\n```\ntraceroute host # UDP-based by default on Linux\ntraceroute -T host # TCP traceroute (firewalls often pass TCP/80)\ntraceroute -I host # ICMP traceroute\ntraceroute -p 443 -T host # specific TCP port\ntcptraceroute host 443 # purpose-built TCP traceroute\n```\n\nDifferent probe types reach different paths because routers and\nfirewalls treat them differently. If UDP traceroute hits a black\nhole at hop 4, try TCP — it might get further.\n\n### Port reachability\n\n```\nnc -zv host 443 # TCP port-knock\nnc -uvz host 53 # UDP port-knock (less reliable)\nnc -lvp 9000 # listen on TCP/9000 (test harness)\nnmap -p 53,80,443 host # multi-port scan\nnmap -sU -p 53,123 host # UDP scan\n```\n\n`nc -zv host 443` is the fastest way to answer \"can I reach\nthis port from here?\".\n\n### Listening sockets\n\n```\nss -tnlp # listening TCP sockets, with PIDs\nss -tnp # established TCP, with PIDs\nss -unlp # listening UDP, with PIDs\nss -unp # active UDP, with PIDs\nss -s # summary: counts of sockets\nss -tnp '( dport = :443 )' # filter by port\nnetstat -tnlp # older alternative; ss is preferred\nlsof -iTCP -sTCP:LISTEN # everything listening on TCP\nlsof -i :53 # whatever is on port 53\n```\n\n`ss -tnlp | grep :53` answers \"is `named` listening?\".\n\n### Local interfaces and routing\n\n```\nip a # all interfaces and addresses\nip -br a # one-line-per-interface summary\nip r # routing table\nip route get 1.1.1.1 # which interface and gateway would be used\nip neigh # ARP / IPv6 neighbour cache\nip -s link # per-interface stats\nifconfig # legacy (Linux); still everywhere on macOS/BSD\narp -an # legacy (Linux); ip neigh on modern systems\n```\n\n`ip route get $IP` is the right tool to answer \"if I send a\npacket to this address, which interface does it leave on?\".\n\n### TLS and HTTPS\n\n```\ncurl -vk https://host/path\ncurl --resolve host:443:1.2.3.4 -v https://host/path\n # use this IP, but pretend the SNI is host\nopenssl s_client -connect host:443 -servername host \u003c/dev/null 2>&1 | head\n # the full TLS handshake\nopenssl s_client -connect host:443 -showcerts \u003c/dev/null 2>&1\n # all the certs in the chain\nnmap --script ssl-cert -p 443 host\n```\n\n`--resolve` is invaluable for testing a new endpoint *before*\nyou've cut DNS over to it: it lets `curl` connect to the new IP\nwhile sending the right SNI/Host header, so the load balancer\nroutes correctly.\n\n### Packet capture\n\n```\ntcpdump -ni any port 53 # DNS, all interfaces\ntcpdump -ni eth0 host 1.1.1.1 # to/from a specific peer\ntcpdump -ni any 'port 53 or port 853' # DNS + DoT\ntcpdump -ni any -w dns.pcap port 53 # save for later\ntshark -ni any port 53 # decoded DNS\ntshark -nr dns.pcap -Y 'dns.flags.rcode != 0' # filter packets in a saved capture\nwireshark dns.pcap # GUI, the same data\n```\n\nA capture of one minute of port-53 traffic answers most \"why is\nDNS slow / failing\" questions definitively.\n\n### Connectivity tests for specific protocols\n\n```\n# SMTP banner\nnc -v mail.example.com 25\nopenssl s_client -starttls smtp -connect mail.example.com:25\n\n# IMAP\nopenssl s_client -connect imap.example.com:993\n\n# SSH host key fingerprint\nssh-keyscan host\n```\n\n---\n\n## Gotchas\n\nA collection of foot-guns that catch experienced operators.\n\n### Trailing dots\n\nAlready covered, but worth reiterating: a missing trailing dot in a\nzone file appends `$ORIGIN` to your name, silently. Pre-flight every\nzone change with `named-checkzone`. The places this bites hardest:\n\n- The MNAME and RNAME of the SOA record.\n- The target of NS, MX, CNAME, PTR, and SRV records.\n- Anything you're copying from external documentation that uses\n the bare-name shortcut.\n\n### Forgotten serial bumps\n\nAfter `vim example.com.zone`, `rndc reload` will silently succeed\nwithout re-publishing the change to secondaries if you didn't bump\nthe SOA serial. Symptom: primary serves new data, secondaries\nserve old data. Fix: bump the serial to a higher value than the\nlast one served and reload again.\n\nSince the serial is a 32-bit unsigned integer, any value works;\ncommon conventions are `YYYYMMDDnn` (date plus two-digit revision)\nor Unix epoch. Both make it obvious which serial is newest.\n\n### Apex CNAMEs\n\nZone apex (the bare zone name) cannot be a CNAME. Period. If you\nneed an apex pointing at, say, a CDN's hostname, you have three\noptions:\n\n- Use an `A`/`AAAA` record at the apex pointing at the CDN's IPs\n directly. Risky if the CDN's IPs change.\n- Use a hosted DNS provider that offers an \"ALIAS\"/\"ANAME\"/\n \"flattened CNAME\" record. Their server resolves the target\n internally and serves an `A`/`AAAA` answer.\n- Move the public-facing name to a non-apex name (e.g.\n `www.example.com.` instead of `example.com.`) and 301-redirect\n from the apex. This is the cleanest from a DNS-purist\n perspective.\n\n### Glue records out of sync with reality\n\nIf the parent zone has glue (`A`/`AAAA` records for your name\nservers) and you change the IPs, you must update both the glue\n*and* the records inside your zone. The parent's glue is what\npublic resolvers actually use to find you; the records inside your\nzone are what your zone says.\n\n### \"Lame\" delegations\n\nWhen the parent's NS records list a name server that doesn't know\nabout the zone (the secondary was retired without updating\ndelegation, or the new server hasn't been configured yet), that's\na *lame delegation*. Public resolvers eventually try every name\nserver in the delegation; some get unhelpful answers from the lame\nones. The symptom is occasional slow resolution of your domain.\nFix: keep delegation in sync with reality.\n\n### Round-robin isn't load balancing\n\nMultiple `A` records for one name are returned in (typically\nrotated) order, but the client decides which to use. Most stub\nresolvers use the first one, fall back to subsequent ones only on\nconnection failure. There's no health checking. For real load\nbalancing or HA, use a load balancer or a CDN.\n\n### TTL planning takes time\n\nYou can't drop TTL from `86400` to `60` and expect the world to\nhave the new TTL within an hour. Resolvers that already have your\nrecord cached will keep it for the *old* TTL. So if you're going\nto make a change that depends on a short TTL, drop the TTL at\nleast one full TTL period in advance.\n\n### Negative cache TTL bites you\n\nIf a name doesn't exist, the resolver caches the `NXDOMAIN` for\nthe SOA's negative TTL (the last field of the SOA). If you create\na new name and it doesn't immediately resolve everywhere, that's\nbecause some resolvers are still in the negative-cache window.\nWait it out, or flush, or pre-emptively keep the negative TTL low\non zones where you'll be adding new names frequently.\n\n### DNS over HTTPS bypasses your resolver\n\nA laptop browsing the web on your network may bypass your\ncorporate resolver entirely, sending queries via DoH directly to\n`dns.google` or `cloudflare-dns.com`. Effects: corporate\nDNS-based filtering doesn't apply, split-horizon for internal\nnames doesn't work in the browser, and you can't see what names\nare being queried. Mitigations: corporate-managed browsers can\ndisable DoH, or you can serve a \"Canary domain\" (RFC 8484) to\nsignal that DoH should be off, or you can implement DoH at your\nown resolver and require its use.\n\n### `127.0.0.1` and `::1` aren't symmetric\n\nIf your zone defines `localhost.example.com.` to point at\n`127.0.0.1`, define an AAAA pointing at `::1` too. Otherwise\nIPv6-preferring clients may try the v6 path, get NXDOMAIN, and\nbehave oddly.\n\n### `/etc/hosts` short-circuits DNS\n\nDon't forget that `/etc/hosts` (or\n`C:\\Windows\\System32\\drivers\\etc\\hosts`) can override DNS for\nspecific names. This is sometimes the cause of \"weird local DNS\nopinions\". Always check it when \"this one machine sees a different\nanswer than the others\".\n\n### IDN and Punycode\n\nInternationalised domain names (`αβγ.example`) are encoded for DNS\nas Punycode (`xn--mxac0bs.example`). Browsers do this automatically;\nyour zone file should contain the Punycode form. If you publish a\nliteral Unicode label, things will break.\n\n### Browsers cache DNS independently\n\nRestarting the browser, or testing with `curl`, is often necessary\nto bypass the browser's DNS cache after fixing a record.\n\n### CNAMEs in places they shouldn't be\n\n- A CNAME on the same name as another record → illegal, will\n cause inconsistent answers.\n- A CNAME at the zone apex → illegal.\n- A CNAME as the target of an MX, NS, or PTR record → legal in\n the protocol but problematic in practice. Mail systems\n particularly may reject mail to MX targets that are CNAMEs.\n\n### Expired DNSSEC signatures\n\nRRSIG records have an expiry. If your signing daemon crashes and\nnobody notices, your signatures will expire and your zone will\nSERVFAIL for validating resolvers. Monitor signature expiry as a\nproactive metric, not just as a reactive outage signal.\n\n### Time skew breaks TSIG\n\nTSIG signatures include a timestamp. If primary and secondary\nclocks are off by more than the fudge factor (default 300s), zone\ntransfers fail with BADTIME. Run NTP on both ends.\n\n### Zone transfer failures often look like configuration drift\n\nIf your secondary \"isn't getting updates\", check in this order:\nNOTIFY (is the primary actually telling the secondary?),\n`allow-transfer` ACL (does the primary let the secondary fetch?),\nTSIG (do the keys match?), firewall (is TCP/53 open between\nthem?), reachability (`dig @primary . SOA`).\n\n### Old `nslookup` lies\n\nThe output format of `nslookup` differs across implementations.\nThe fields that look like authoritative answers may actually be\ncached answers. The fields labelled \"Server:\" tell you which\nrecursor was queried, not where the data came from. Use `dig` for\nanything you'll need to interpret accurately.\n\n---\n\n## Version notes and ecosystem\n\n### BIND versions in the wild\n\n- **BIND 9.18 LTS** — current long-term support series. Stable,\n fully featured, what most modern Linux distributions ship.\n- **BIND 9.16** — the previous LTS. End-of-life as of mid-2023;\n stragglers exist in long-running enterprise deployments.\n- **BIND 9.11 and earlier** — explicitly out of support. If you're\n on these, the upgrade path is the most important task on your\n backlog. Older versions have unpatched CVEs.\n\nThe configuration syntax has been stable across recent versions;\nthe most visible changes:\n\n- `master`/`slave` zone types have been deprecated in favour of\n `primary`/`secondary` (both still work).\n- Manual DNSSEC management is being phased out in favour of\n `dnssec-policy`. Modern configurations should use the latter.\n- `dnssec-must-be-secure` and a few other DNSSEC operator knobs\n have been refined.\n- `geoip` ACLs use the MaxMind DB format (the older\n `geoip-directory` style was retired in 9.16).\n- `serve-stale` (RFC 8767) is on by default in many configurations\n — return cached answers past TTL when authoritatives are\n unreachable, to keep the network usable during outages.\n\n### Alternatives to BIND\n\n- **Knot DNS** (CZ.NIC) — fast authoritative server, used heavily\n in TLDs. Comes with `kdig`.\n- **NSD** (NLnet Labs) — minimalist authoritative server, used\n notably for the root zone's distributions.\n- **Unbound** (NLnet Labs) — recursive-only resolver, lightweight\n and security-focused. Many modern setups pair Unbound (recursor)\n with NSD (authoritative).\n- **PowerDNS** — separate authoritative and recursor packages,\n with database backends (so zone data lives in MySQL/Postgres\n rather than text files). Popular for hosted DNS.\n- **CoreDNS** — DNS server written in Go; common in Kubernetes\n ecosystems as the in-cluster DNS.\n- **systemd-resolved** — the default on most modern Linux\n desktops, a small recursive resolver glued into the systemd\n init system.\n- **dnsmasq** — small server doing DHCP + DNS, common on home\n routers and embedded devices. Easy configuration, limited\n features.\n- **Microsoft DNS Server** — bundled with Windows Server,\n AD-integrated. The default on most Windows-shop networks. Uses\n AD-replicated storage rather than zone files; can serve plain\n zones too.\n\nThe protocol is the same across all of them. Configuration\nsyntax differs, monitoring and operational tools differ, but the\nzone data and the wire-protocol behaviour are interoperable.\n\n### Hosted DNS providers\n\n- **Route 53** (AWS) — feature-rich, integrated with AWS, supports\n alias records pointing at AWS resources.\n- **Cloud DNS** (GCP) — similar story for Google Cloud.\n- **Azure DNS** — for Azure.\n- **Cloudflare** — popular, with extensive features for caching,\n DDoS protection, and integration with their other services.\n ANAME-style apex flattening included.\n- **Constellix, NS1, DNSimple, Hurricane Electric, etc.** — smaller\n providers, each with their own feature mix.\n\nThe benefit is operational: you don't run name servers, you don't\nworry about availability, you get a control panel and an API. The\ncost is the lock-in (your DNS data lives in their proprietary\nformat, even if they accept zone-file imports/exports), and the\ncost of egress queries (some charge per query, some flat).\n\n### Modern best practices\n\n- **Authoritative-only servers**, no recursion. Recursion belongs\n in dedicated recursive resolvers, not on your authoritative\n servers.\n- **Multiple authoritative servers** in different networks (and\n ideally different providers). DNS-only outages take whole\n applications down.\n- **DNSSEC** for any zone that holds public data. The chain of\n trust eliminates a wide class of cache-poisoning attacks.\n- **Query-name minimisation** at recursors. Reduces information\n leakage to authoritative servers.\n- **Monitoring**: SOA serial agreement across NS, response-time\n graphs from external probes, signature expiry alerts, NXDOMAIN\n spikes, SERVFAIL spikes, transfer-success metrics.\n- **Version control your zone files**. Zone files live in git the\n same way infrastructure-as-code does. Pre-commit hooks run\n `named-checkzone`. CI pushes the validated files to the primary\n via SSH or your provider's API.\n- **Separate zones for stable infrastructure and dynamic data**.\n ACME challenges, DHCP-driven PTRs, ephemeral compute should\n live in their own subzones (`dyn.example.com.`,\n `_acme-challenge.example.com.`) so the dynamically-updated\n data doesn't interfere with hand-edited records.\n- **Secure transfers and updates with TSIG**, not IP-based ACLs\n alone.\n- **Plan for IPv6**. AAAA records, IPv6-reachable name servers,\n reverse zones in `ip6.arpa.`, dual-stack everything.\n\n### Useful RFCs\n\nWhen you need the formal answer, the relevant RFCs are typically:\n\n- **RFC 1034, 1035** — DNS concepts and protocol. The 1987\n originals; still authoritative.\n- **RFC 2181** — clarifications on the DNS spec.\n- **RFC 2308** — negative caching, repurposing of SOA minimum.\n- **RFC 2845, 8945** — TSIG.\n- **RFC 4033, 4034, 4035** — DNSSEC core.\n- **RFC 5155** — NSEC3.\n- **RFC 6891** — EDNS0.\n- **RFC 7344, 8078** — automated DS publication for child zones.\n- **RFC 7858** — DNS over TLS.\n- **RFC 8484** — DNS over HTTPS.\n- **RFC 8767** — serve-stale.\n- **RFC 9156** — query name minimisation.\n- **RFC 9460** — SVCB and HTTPS records.\n\nFor day-to-day work, you almost never need to read an RFC. For\ndebugging weird interop issues with another implementation,\nsometimes nothing else will do.\n\n\n---\n\n## Recipe collection: programming against the resolver\n\nMost ops work touches DNS through `dig`, `host`, or whatever the OS\ngives you. When you need DNS *inside* an application — config\ndiscovery, SRV-driven service location, custom health checks — you\nreach for a resolver library. This section is a tour of the major\noptions and the gotchas that come up most.\n\n### C — the system resolver\n\nThe classic C path is the libc resolver. The interesting calls live in\n`\u003cresolv.h>`:\n\n- `res_init()` — initialise the resolver state from `/etc/resolv.conf`.\n- `res_query()` / `res_search()` — issue a query, return the raw DNS\n response packet for you to parse.\n- `getaddrinfo()` — the modern, protocol-agnostic name → socket\n lookup. Almost always what you want for \"give me an address to\n connect to.\"\n- `getnameinfo()` — the reverse: socket → name.\n\n```c\n#include \u003cnetdb.h>\n#include \u003cstdio.h>\n#include \u003cstring.h>\n\nint main(int argc, char **argv) {\n struct addrinfo hints = {0}, *res, *p;\n hints.ai_family = AF_UNSPEC; // v4 or v6\n hints.ai_socktype = SOCK_STREAM; // TCP\n\n int rc = getaddrinfo(argv[1], \"https\", &hints, &res);\n if (rc) { fprintf(stderr, \"%s\\n\", gai_strerror(rc)); return 1; }\n\n for (p = res; p; p = p->ai_next) {\n char host[NI_MAXHOST];\n getnameinfo(p->ai_addr, p->ai_addrlen, host, sizeof host,\n NULL, 0, NI_NUMERICHOST);\n printf(\"%s\\n\", host);\n }\n freeaddrinfo(res);\n return 0;\n}\n```\n\nGotchas worth memorising:\n\n- `getaddrinfo` is the right call for almost all application code; the\n older `gethostbyname` / `gethostbyaddr` are not thread-safe and only\n speak IPv4.\n- Always free the result with `freeaddrinfo`.\n- Iteration order of the returned list is policy-driven (RFC 6724);\n don't assume it.\n- `res_query` returns a raw DNS packet; if you want anything other\n than A/AAAA you are doing protocol-level work.\n\n### C — `libldns` and `libunbound`\n\nWhen you actually need to parse arbitrary record types, validate\nDNSSEC, or do recursion yourself, the system resolver is too thin.\nTwo well-maintained C libraries:\n\n- **`libldns`** (NLnet Labs) — high-level DNS protocol toolkit. Good\n at building, parsing, and signing zone data. The `drill` tool\n ships with it and is a useful debugging companion to `dig`.\n- **`libunbound`** — embeddable validating recursive resolver. If you\n want to *do* DNSSEC validation inside your application without\n trusting the upstream resolver, this is the standard answer.\n\nBoth ship on most distros (`libldns-dev` / `libunbound-dev`).\n\n### Perl, Python, Go, Rust — the modern picks\n\n- **Perl**: `Net::DNS` — the long-lived workhorse. Builds and parses\n any record type, talks UDP/TCP/TLS, supports TSIG, DNSSEC.\n- **Python**: `dnspython` (`pip install dnspython`). The de facto\n library; covers query, zone parsing, dynamic update, TSIG, DNSSEC.\n- **Go**: `github.com/miekg/dns`. A near-complete DNS protocol\n implementation; used by CoreDNS internally.\n- **Rust**: `hickory-dns` (formerly `trust-dns`). Async client +\n resolver + server crates.\n\nThe Python equivalent of the C example above:\n\n```python\nimport dns.resolver\n\nanswers = dns.resolver.resolve(\"example.com\", \"A\")\nfor r in answers:\n print(r.address)\n\n# Specific record types\nfor r in dns.resolver.resolve(\"example.com\", \"MX\"):\n print(r.preference, r.exchange)\n\n# Use a specific server\nr = dns.resolver.Resolver(configure=False)\nr.nameservers = [\"1.1.1.1\", \"9.9.9.9\"]\nr.timeout = 2\nr.lifetime = 5\nanswers = r.resolve(\"example.com\", \"AAAA\")\n```\n\n### Building queries by hand (when libraries get in the way)\n\nWhen you need to test resolver behaviour or replay a pcap, you build\nthe wire format directly. Concise idioms:\n\n```bash\n# dig has a +qr mode that prints the wire format\ndig +qr example.com any | grep -E '^;.*HEADER|;.*flags:'\n\n# Construct an arbitrary query with kdig (knot-dnsutils)\nkdig +retry=0 +tries=1 +timeout=2 @1.1.1.1 example.com TYPE65\n\n# Send a question over a non-standard port\ndig @127.0.0.1 -p 5353 example.com\n\n# Force TCP\ndig +tcp example.com\n\n# Force EDNS off (test legacy paths)\ndig +noedns example.com\n\n# DNSSEC: ask for the records and the chain\ndig +dnssec +cd example.com # +cd disables validation client-side\ndelv example.com # libldns's validator-aware lookup\n```\n\n---\n\n## Recipe collection: less-common BIND configuration directives\n\nA short reference for directives that show up rarely enough to forget\ntheir exact spelling but are critical when you need them.\n\n### Address-match-list and ACL composition\n\n```\nacl \"internal\" {\n 127.0.0.1/32;\n 10.0.0.0/8;\n 192.168.0.0/16;\n fd00::/8;\n};\n\nacl \"transfer-peers\" { key \"tsig-secondary\"; };\n\noptions {\n allow-query { internal; localhost; };\n allow-recursion { internal; };\n allow-transfer { transfer-peers; };\n allow-update { none; }; # stop dynamic update by default\n};\n```\n\nACLs are evaluated top-to-bottom and the first match wins. `!` negates\nan entry: `allow-query { !blocked; any; };` permits everyone except\nthe `blocked` ACL.\n\n### Sortlist and address sorting\n\n`sortlist` reorders the answer-section addresses on a per-client basis\nto prefer \"near\" addresses. Largely superseded by client-side\n`getaddrinfo` policy, but still useful in older environments:\n\n```\noptions {\n sortlist {\n { 192.168.1.0/24; { 192.168.1.0/24; 10.0.0.0/8; }; };\n };\n};\n```\n\n### Periodic intervals — when nameservers do work\n\n```\noptions {\n cleaning-interval 60; # cache cleaning, in minutes\n interface-interval 60; # rescan interfaces\n statistics-interval 60; # dump stats to log\n heartbeat-interval 60; # dial-on-demand heartbeat (rare today)\n max-cache-ttl 86400;\n max-ncache-ttl 3600;\n};\n```\n\nIn modern environments leave the defaults alone unless you have a\nspecific reason. The values above are illustrative, not recommended.\n\n### `trusted-keys` / `managed-keys` (DNSSEC)\n\nThe chain of DNSSEC trust starts with a key configured locally:\n\n```\ntrusted-keys {\n \".\" 257 3 8 \"AwEAA…\"; # static trust anchor\n};\n\n# OR (recommended) — automatically tracked via RFC 5011\nmanaged-keys {\n \".\" initial-key 257 3 8 \"AwEAA…\";\n};\n```\n\n`managed-keys` is preferred because it follows root-zone key rollovers\nwithout manual intervention.\n\n### `check-names` — hostname validation\n\n```\noptions {\n check-names master fail; # zone-load: refuse bad names\n check-names slave warn;\n check-names response ignore; # don't second-guess what we resolve\n};\n```\n\nThe defaults are usually fine; touch this only when an upstream is\nserving names that your `master` strictness rejects.\n\n### View clauses for split-horizon\n\n```\nview \"internal\" {\n match-clients { internal; };\n recursion yes;\n zone \"corp.example.com\" { type master; file \"internal/corp.zone\"; };\n};\n\nview \"external\" {\n match-clients { any; };\n recursion no;\n zone \"corp.example.com\" { type master; file \"external/corp.zone\"; };\n};\n```\n\nOrder matters: the first `view` whose `match-clients` matches the\nquery source wins. Put narrow ACLs first, `match-clients { any; };`\nlast.\n\n### Logging channels\n\n```\nlogging {\n channel \"default_syslog\" {\n syslog daemon;\n severity info;\n };\n channel \"queries\" {\n file \"/var/log/named/queries.log\" versions 5 size 100m;\n severity info;\n print-time yes;\n };\n category default { default_syslog; };\n category queries { queries; };\n};\n```\n\nThe `versions N size M` form does built-in log rotation. For\nhigh-traffic resolvers, log rotation that BIND owns is generally\nfaster and more reliable than handing rotation to `logrotate`.\n\n### `check_soa` and other operational scripts\n\nSeveral SOA-comparison helpers ship with BIND in `contrib/`:\n\n- `check_soa \u003czone>` — query each authoritative server for the zone\n and report SOA serial / mismatches.\n- `nsdiff` (third-party) — diff two zones in dynamic-update form,\n ready to feed to `nsupdate`.\n- `named-checkconf -z` — load-test every zone the config references.\n- `named-checkzone \u003czone> \u003cfile>` — validate a single zone file.\n\nCron-style health check:\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\nzone=corp.example.com\nexpected_serial=$(dig +short SOA @master.ns \"$zone\" | awk '{print $3}')\nfor ns in slave1.ns slave2.ns slave3.ns; do\n actual=$(dig +short SOA @\"$ns\" \"$zone\" | awk '{print $3}')\n [[ \"$actual\" == \"$expected_serial\" ]] || \\\n echo \"DRIFT: $ns serial=$actual master=$expected_serial\"\ndone\n```\n\n### Miscellaneous record types worth knowing exist\n\n| Type | Use |\n| --- | --- |\n| `AFSDB` | AFS database server location (legacy AFS / DCE) |\n| `RP` | Responsible person (mostly cosmetic) |\n| `LOC` | Geographic location (latitude/longitude) |\n| `NAPTR` | Used by ENUM and SIP; pattern-substitution rules |\n| `CERT` | Holds a certificate inline in DNS |\n| `IPSECKEY` | Public key material for IPsec opportunistic encryption |\n| `SSHFP` | SSH host-key fingerprint for verification at first connect |\n| `TLSA` | DANE — bind a TLS cert to a DNS name |\n| `SVCB` / `HTTPS` | Modern service binding; supersedes some SRV uses |\n| `URI` | RFC 7553 URI record |\n\nYou will go your whole career without provisioning some of these. But\nrecognising them in `dig` output is its own skill.\n\n### Avoiding common BIND pitfalls\n\n- **Forgetting to bump the SOA serial after editing a zone.** Slaves\n won't pull the new data. `named-checkzone` will not catch this.\n Use `serial-update-method unixtime` or a wrapper script that\n bumps automatically.\n- **Trailing-dot mistakes in zone files.** `host.example.com` with no\n trailing dot is parsed as `host.example.com.example.com.` —\n silently. Always use either fully-qualified-with-trailing-dot or\n bare-and-relative; never the in-between.\n- **`$ORIGIN` reset by `$INCLUDE`**. Set `$ORIGIN` *after* an\n `$INCLUDE` if the included file changed it.\n- **Slave fails after master IP change.** If `also-notify` and the\n slave's `masters {}` block both reference an old address, the\n zone goes stale; the symptom is a serial drift you can see with\n the `check_soa` recipe above.\n- **DNSSEC chain breaks at parent.** If you re-sign with a new KSK\n but forget to push the DS record to the parent, every validating\n resolver returns SERVFAIL until the parent catches up.\n","licenseDocuments":[]}}
The interface is localized. Skill descriptions and documents are machine-translated by AI into your language; code, commands, paths and technical terms stay in the original, and the author's document governs.
Explore skills
Productivity · SKILL.md

dns-networking

dns-networking

Operator's manual for DNS covering zone files, resource records, query resolution lifecycle, operational commands, TSIG/DNSSEC security, and production troubleshooting recipes.

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill documentation

GitHub

name: dns-networking description: Operator's manual for DNS covering zone files, resource records, query resolution lifecycle, operational commands, TSIG/DNSSEC security, and production troubleshooting recipes. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: networking triggers: dns, bind, zone file, named.conf, resource records, dig nslookup, how do i manage dns, dnssec tsig role: reference scope: infrastructure output-format: manifests related-skills: linux-cli-reference archetypes: educational anti_triggers: implement from scratch, architect a system, build infrastructure, design patterns response_profile: verbosity: high directive_strength: low abstraction_level: operational


DNS and Networking — Quick Reference

Operator's manual for DNS covering the mental model needed to read named.conf, every resource record you'll meet in practice, the lifecycle of a DNS query, operational commands, security (TSIG and DNSSEC), and troubleshooting recipes for production.

When to Use

  • Staring at a zone file and needing to remember trailing dot conventions, SOA record fields, and delegation behavior
  • Being handed an existing BIND deployment and needing to be productive quickly
  • Adding a record, retiring a host, moving a service, or delegating a subdomain without breaking dependencies
  • Determining within two minutes whether "the site is down" is DNS, networking, application, or something else
  • Modernizing: turning on DNSSEC, switching to TSIG-secured zone transfers, replacing nslookup with dig, moving from BIND to hosted

When NOT to Use

  • For a deep dive on the protocol's bytes-on-the-wire — read the RFCs (1034, 1035, 6604, 6891, 8482, 9156)
  • For non-BIND server documentation (Knot DNS, NSD, Unbound, PowerDNS, Microsoft DNS Server) — most concepts transfer but config syntax differs
  • For DHCP integration, IPAM tooling, or DNS-as-a-Service specific tooling (Route 53, Cloud DNS, Azure DNS)

Mental model

A few sentences you can hold in your head and lean on for everything else:

  1. DNS is a tree. The root is the dot at the end of every fully qualified name. Top-level domains (com., org., net., io., uk., …) are children of the root. Below the TLDs are the "second level" domains people register, and below those are the leaves (host names, service names, alias names). A domain is a subtree; a zone is the part of a domain that one server is authoritative for; the difference between domain and zone is delegation (the parent has handed off responsibility for a subtree to another server, so the parent's zone stops at the delegation point).
  2. A name is read leaf-to-root. www.example.com. is the host www inside the zone example.com., which is below the TLD com., which is below the root .. The trailing dot is the root. In zone files, an unqualified name (no trailing dot) is treated as relative to the current $ORIGIN; an absolute name has the trailing dot.
  3. Resolution walks the tree top-down with caching at every step. Your machine asks a recursive resolver (configured in /etc/resolv.conf, systemd-resolved, or the platform equivalent). The resolver, on a cache miss, asks a root server, then a TLD server, then the zone's authoritative server, following referrals down. Each step's answer goes into the resolver's cache for the record's TTL.
  4. The data is resource records attached to names. A name like mail.example.com. can have multiple records of different types attached: an A record for the IPv4 address, an AAAA for IPv6, maybe an MX saying that this host is a mail exchanger for some other domain, maybe a TXT for SPF or DKIM, maybe a CNAME pointing somewhere else (but only if no other records exist on that name).
  5. Authoritative vs. recursive. An authoritative server holds real data for some zones and answers questions only about those zones. A recursive resolver holds no data of its own; it walks the tree and caches what it learns. Most servers can do both, but the modern best practice is to keep them separate — authoritative servers should not do recursion, recursive resolvers should not serve zones.
  6. TTL is a contract. When an authoritative server hands you a record, the TTL on it tells every cache in the world how long they may keep that answer before re-asking. A short TTL means changes propagate fast at the cost of more queries to the authoritative servers; a long TTL means the opposite. Plan TTL drops before any change you want to take effect quickly.
  7. Almost every DNS bug is one of three things: a stale cache somewhere; a serial number that wasn't bumped, so secondaries never pulled the new zone; or a missing trailing dot in a zone file, which silently turned mail.example.com. into mail.example.com.example.com..

The rest of this card is the long form of those seven sentences.


The namespace, names, and zones

How the tree is structured

The DNS namespace is a tree, similar in shape to a UNIX filesystem turned upside down. The root sits at the top, written as a single dot .. Its children are the top-level domains (TLDs): generic ones like com., net., org., info., io., dev., and the country codes like uk., de., jp., nz., plus a long tail of newer TLDs. Below each TLD live the second-level domains people register — example.com., wikipedia.org., nytimes.com.. Below those, the holder of the second-level domain is free to put whatever structure makes sense — www, mail, api, sub-environments (prod., staging.), per-team or per-service subdomains, and so on.

Each node in the tree has a label — the part between dots — that can be up to 63 characters long. The root's label is the empty string. The full domain name of any node is the sequence of labels from that node up to the root, written left-to-right with dots as separators. Names are always read from leaf to root.

There's a hard limit of 255 octets on the wire-format length of a domain name, including length bytes between labels and the zero-byte root label. In practice, that means around 253 printable ASCII characters across all the labels.

Absolute names, relative names, and the trailing dot

A domain name written with a trailing dot is absolute (also called fully qualified). The trailing dot is the root label. The name www.example.com. is unambiguous and complete. Without the trailing dot, the name is relative, and what it resolves to depends on context:

  • In a zone file, a relative name has the current $ORIGIN appended to it. If the current $ORIGIN is example.com. and you write www, that means www.example.com.. If you write www.example.com without the trailing dot, the origin is still appended and you've just defined a record for the name www.example.com.example.com. — almost always not what you wanted.
  • On the command line and in resolver libraries, a relative name is passed through the search list (configured in resolv.conf or the platform's equivalent), which appends each search-list entry in turn until something resolves.
  • In application configuration, behaviour varies. Most modern software treats names as absolute even without a trailing dot; some legacy software does search-list expansion. When in doubt, spell out the absolute name.

The trailing-dot rule in zone files is the single most common source of mistakes when editing DNS data by hand. Pre-flight every change with named-checkzone (or your provider's equivalent linter) before pushing it.

Domains versus zones

A domain is a subtree of the namespace. The example.com. domain includes example.com. itself, every direct child like www.example.com. and mail.example.com., and every grandchild like db.prod.example.com. and so on, all the way down.

A zone is the unit of administrative control — the chunk of a domain that a particular set of authoritative servers serves directly. Zones are bounded by delegation. If example.com. delegates prod.example.com. to a different set of servers, then example.com. and prod.example.com. are two zones, even though they're nested in the same domain.

The same name can be a node in a domain and the apex of a zone. The apex of a zone is the topmost node — for the example.com. zone, the apex is example.com. itself. The apex must hold an SOA record and at least one NS record. Apex CNAMEs are illegal.

Reading domain names

A few worked examples to build intuition:

  • mail.support.example.co.uk. — mail is a host or service name inside the support.example.co.uk. zone, which lives under example.co.uk., which lives under co.uk. (the conventional British-commercial second level), which lives under the country TLD uk..
  • _dmarc.example.com. — the underscore at the start is a convention for DNS-based service location names. This one holds the DMARC policy TXT record for example.com..
  • 15.16.192.in-addr.arpa. — a name in the reverse-mapping namespace for the IPv4 address 192.16.15.x. The octets are reversed because IPv4 addresses get more specific from left to right while DNS names get less specific from left to right.

The reverse-mapping namespace

Forward mapping (name → address) is the obvious case. Reverse mapping (address → name) uses a special branch of the namespace called in-addr.arpa. for IPv4 and ip6.arpa. for IPv6.

For IPv4, the reverse name is built by reversing the dotted-quad of the IP address and appending in-addr.arpa.. So the reverse name for 192.16.15.10 is 10.15.16.192.in-addr.arpa.. The records attached to that reverse name are PTR records that name the host.

The octets are reversed because of how delegation works. The owner of network 192.16.0.0/16 is responsible for everything beneath 16.192.in-addr.arpa., and they may delegate 15.16.192.in-addr.arpa. (the 192.16.15.0/24 subnet) further. By putting the most general octet (192.) closest to the root, you can delegate by network boundary the same way you delegate by name boundary in the forward namespace.

For IPv6 the convention is similar but uses one nibble per label, reversed, under ip6.arpa.. The address 2001:db8::1 becomes 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.. In practice, you'll generate these mechanically with your tooling, not by hand.

Delegation

Delegation is how authority for a subtree gets handed off. The parent zone keeps NS records pointing at the authoritative servers for the child zone. Optionally, if the child's name servers are themselves inside the child zone, the parent also includes glue records — A and AAAA records giving the addresses of the child's name servers, breaking what would otherwise be a chicken- and-egg lookup.

For example, if example.com. delegates prod.example.com. to servers ns1.prod.example.com. and ns2.prod.example.com., the example.com. zone needs:

prod.example.com.    NS    ns1.prod.example.com.
prod.example.com.    NS    ns2.prod.example.com.
ns1.prod.example.com. A    192.0.2.10     ; glue
ns2.prod.example.com. A    192.0.2.11     ; glue

Without the glue records, a resolver trying to find a name in prod.example.com. would receive the NS records, then try to look up ns1.prod.example.com. and find that the only place that name is defined is inside the very zone it's trying to reach. Glue breaks the loop.

If the child's name servers live in a different zone (e.g. you use a hosted provider, so your NS records point at the provider's name servers), no glue is needed because resolvers can resolve those servers' names through the regular namespace.

Subdomains versus delegated zones

A subdomain doesn't have to be a separate zone. If you want prod.example.com. to be its own administrative scope with its own authoritative servers, you delegate. If you just want some records under that name (web.prod.example.com., db.prod.example.com.) served by the same servers as the parent, you don't have to delegate — you just put those records in the example.com. zone file directly, with their full names. Most organisations use a mix: delegation when teams are independent, no delegation when everything is run by one team.


Resolution: how a query travels from a client to an answer

Every DNS query you ever issue follows roughly the same path. Knowing the path is the difference between guessing at why something is broken and pointing at the exact step that's failing.

The actors

  • Stub resolver — the small piece of code in your operating system that takes a getaddrinfo() call and turns it into a DNS query. The stub knows how to send a query, wait for an answer, and hand the result back. It does not walk the namespace; it asks one question and expects the recursive resolver to do the work.
  • Recursive resolver (also called a "recursive nameserver" or just "recursor") — the server that walks the tree on the stub's behalf, follows referrals, caches what it learns, and returns a final answer or an error. Your ISP runs one. Your laptop, on most modern OSes, runs a tiny one of its own (systemd-resolved on Linux, mDNSResponder on macOS). Your office network usually has one or more (often a Windows DC, or a dedicated Pi-hole / AdGuard Home / Unbound box). You can also use a public resolver like 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), or 9.9.9.9 (Quad9).
  • Authoritative server — the server that holds the master copy (or a slave/secondary copy) of a zone's data and answers authoritative questions about that zone. Different zones have different authoritative servers. The root zone has the 13 named root servers (a.root-servers.net. through m.root-servers.net.), each of which is in fact many physically distributed instances behind anycast.

Recursive vs. iterative queries

A recursive query says "answer this for me; do whatever work is necessary, including asking other servers, and come back with a final answer." Stubs ask their resolver recursive queries.

An iterative query says "answer this if you can; if you can't, tell me who to ask next." Recursors ask authoritative servers iterative queries — the authoritative server either answers authoritatively or refers them to another server "closer" to the answer.

Most modern recursive resolvers refuse recursive queries from clients that aren't on their authorized list (otherwise they'd be open resolvers — useful as DDoS amplifiers and almost nothing else).

The full walk, step by step

Imagine your laptop wants the IPv4 address for shop.example.co.uk.. Your resolv.conf lists 1.1.1.1 as your resolver. Cache is cold everywhere.

  1. Your stub builds a UDP query: "A record for shop.example.co.uk.?". Sends it to 1.1.1.1 on port 53.
  2. 1.1.1.1 checks its cache. Cache miss. It needs to walk the tree. It always knows the addresses of the root servers (from a built-in root hints file).
  3. 1.1.1.1 picks a root server, say a.root-servers.net., and sends the same iterative query: "A record for shop.example.co.uk.?".
  4. The root server doesn't know about shop.example.co.uk. — that's far below it — but it knows who's authoritative for uk.. It sends a referral: "ask one of the uk. name servers", together with NS records for the uk. zone and glue A/AAAA records for them.
  5. 1.1.1.1 caches the uk. NS records and picks one of the uk. servers. Sends the same query.
  6. The uk. server doesn't know about shop.example.co.uk. either, but it knows who's authoritative for co.uk.. Another referral, this time to the co.uk. name servers.
  7. Same dance. The co.uk. server refers to the example.co.uk. name servers.
  8. 1.1.1.1 finally asks an example.co.uk. authoritative server for the A record. The authoritative server has the answer and returns it, with the aa (authoritative answer) flag set and a TTL.
  9. 1.1.1.1 caches the answer for the TTL and forwards it back to your stub.
  10. Your stub passes it to getaddrinfo(), which passes it to your application, which can now connect().

If the same query happens again within the TTL, step 2 is a cache hit and the whole walk is short-circuited. Even partial caching helps — once 1.1.1.1 knows the co.uk. name servers, future queries for any *.co.uk. name skip the root and uk. lookups.

Recursion vs. forwarding

Some recursive resolvers don't walk the tree themselves; they forward every query to a designated upstream resolver and trust that resolver to do the recursion. In a corporate network, a small local resolver inside a branch office often forwards everything to a central resolver inside HQ, which does the actual recursion. Forwarding is configured per-zone or globally:

options {
  forwarders { 192.0.2.10; 192.0.2.11; };
  forward only;          # or "first" — see below
};

forward only means "always send to the forwarders; if they don't answer, return failure". forward first means "send to the forwarders first, but if they're unreachable, fall back to walking the tree yourself."

Forwarders are most useful when:

  • You have one upstream resolver with a much larger cache than yours, so forwarding amortises the recursion work.
  • The authoritative servers you need to reach are accessible only from one specific resolver (DNS over a VPN, for example).
  • Internal "split-horizon" naming where some names should resolve inside the company differently from outside.

Round-trip time and server selection

When a recursor has multiple authoritative servers to choose from (every zone has at least two), it remembers the round-trip time (RTT) to each and prefers the fastest. Until it has measured RTT for a server, it gives that server a small random initial RTT, so the first few queries fan out across the available servers and build up real measurements. After a few queries, the recursor "locks on" to whichever authoritative server is fastest from its network position.

This is why a single misbehaving authoritative server in a set is often hard to detect — the resolver quietly stops querying it. The fix is monitoring (probe each authoritative server directly with dig @server name) rather than relying on resolver behaviour.

Negative caching and NXDOMAIN

If a name doesn't exist, the authoritative server returns NXDOMAIN (status code 3). Resolvers cache NXDOMAIN answers too, so a hammering of queries for a non-existent name doesn't keep hitting the authoritative servers. The TTL on negative cache entries comes from the SOA record's minimum (now repurposed as the negative-caching TTL — see SOA discussion below).

If the name exists but the requested type doesn't (e.g. you ask for an MX record on a name that only has A records), you get NOERROR with no answer records and an empty ANSWER section plus an AUTHORITY section containing the SOA. This is "no data, domain exists", different from NXDOMAIN.

Truncation and the move to TCP

DNS over UDP has a 512-byte limit per response (without EDNS — see below). If the answer is bigger, the server sets the TC (truncated) flag in the UDP response and the client should retry over TCP, which has no such limit. Most modern DNS uses EDNS0 (Extension Mechanisms for DNS) to negotiate a larger UDP payload size — typically 4096 bytes — but firewalls that block fragmented UDP or large DNS-over-UDP packets break this and force fallback to TCP. Make sure both UDP/53 and TCP/53 are open to and from your authoritative servers; some old firewall rules only permit UDP/53 on the assumption that "DNS is UDP", which is no longer accurate.

Modern transports: DoT and DoH

Two newer transports encrypt DNS traffic between stub and recursor:

  • DNS over TLS (DoT) — DNS over a TLS tunnel on TCP/853.
  • DNS over HTTPS (DoH) — DNS queries inside HTTPS POST or GET on TCP/443.

Both protect the stub-to-recursor leg from passive observation. Neither encrypts the recursor-to-authoritative leg (that's "authoritative DoT/DoH" and is a separate, less widely deployed proposal). DoH is mostly visible to operators because some browsers (Firefox, Chrome) ship with DoH on by default, bypassing the operating system's resolver entirely. That can be a benefit (privacy in hostile networks) or a problem (corporate DNS-based filtering and split-horizon stop working).

Stub resolver configuration

On Linux, the classic configuration file is /etc/resolv.conf:

nameserver 1.1.1.1
nameserver 8.8.8.8
search corp.example.com example.com
options timeout:2 attempts:2 rotate
  • nameserver — IP of a recursive resolver. Up to three are honored. Queried in order, with retries on the second and third if the first times out.
  • search — list of suffixes to append to single-label names. ssh foo with the search list above will try foo.corp.example.com., then foo.example.com., then foo. if both fail.
  • domain — older alternative to search, equivalent to search ONE_DOMAIN.
  • options — per-resolver tweaks. timeout:N is per-server, attempts:N is rounds across all servers, rotate round-robins across servers, ndots:N controls how many dots a name must have before it's tried as-is before search-list expansion.

On modern systemd Linux, /etc/resolv.conf is often a symlink to something systemd-resolved manages, and the real configuration lives in /etc/systemd/resolved.conf and per-link settings. resolvectl status shows what's actually in effect per network link.

On macOS, network DNS configuration is in System Settings → Network → (interface) → DNS. You can override via scutil --dns to inspect, or by writing files into /etc/resolver/ for per-domain overrides (handy for development when a corporate VPN's name servers should only handle the company's domain).

On Windows, DNS configuration is per-adapter in Network and Sharing Center and visible from PowerShell with Get-DnsClient and Get-DnsClientServerAddress.


Resource records, in detail

A resource record (RR) is one row of DNS data: a name, a class, a type, a TTL, and a type-specific RDATA payload. Records are grouped into resource record sets (RRsets) — all records with the same name, class, and type live in the same RRset and are signed and served as a unit.

The class is almost always IN (Internet); the other historic classes (CH for Chaosnet, HS for Hesiod) are essentially gone in 2025 production. You can omit IN from zone files; it's the default.

A few records you'll see all the time, in approximate order of how often you'll edit them.

A — IPv4 address

Maps a name to a 32-bit IPv4 address.

www.example.com.    300    IN  A     192.0.2.10
www.example.com.    300    IN  A     192.0.2.11
www.example.com.    300    IN  A     192.0.2.12

A name can have multiple A records. Resolvers receive all of them and the application picks one (typically just the first, but better clients try each in turn on connection failure). Most authoritative servers rotate the order of A records on each response — the round-robin effect — to spread load across endpoints. Modern deployments use a load balancer or a service mesh in front rather than relying on round-robin DNS, but the latter is still common for geo-DNS and simple two-or-three-IP setups.

AAAA — IPv6 address

Maps a name to a 128-bit IPv6 address. Pronounced "quad-A". The record type was originally proposed as A6 with a more elaborate mechanism for prefix delegation, then simplified to AAAA.

www.example.com.    300    IN  AAAA  2001:db8::1
www.example.com.    300    IN  AAAA  2001:db8::2

Modern dual-stack deployments publish both A and AAAA for a given service. Clients that have IPv6 connectivity prefer AAAA (per RFC 6724); if the IPv6 path is broken, "Happy Eyeballs" (RFC 8305) lets the client race the v4 and v6 paths and use whichever wins.

CNAME — canonical name (alias)

Aliases one name to another.

www.example.com.    300    IN  CNAME corporate-lb.example.com.

A query for www.example.com. returns the CNAME plus the records the resolver looked up at the canonical name. Most stub resolvers follow the chain transparently and the application sees a final A/AAAA answer.

The CNAME rules to commit to memory:

  • A name with a CNAME record may not have any other records. So you can't put a CNAME and an MX on the same name.
  • The apex of a zone may not be a CNAME. The apex must have an SOA and at least one NS, and CNAME excludes coexistence. Some hosted DNS providers offer "alias", "ANAME", or "flattened CNAME" records that look like an apex CNAME from the outside but resolve into A/AAAA records server-side.
  • CNAME chains work, but each hop costs another lookup. Three deep is fine; ten deep is a smell.
  • Don't use CNAME for an MX target or an NS target. Both must point to names with concrete A/AAAA records, not aliases.

MX — mail exchanger

Names a mail-receiving host for a domain, with a preference value that orders multiple MX records.

example.com.    3600    IN  MX  10  mail1.example.com.
example.com.    3600    IN  MX  20  mail2.example.com.
example.com.    3600    IN  MX  20  mail3.example.com.

Lower preference value = higher priority. Senders try the lowest first; on failure or among equals, they pick from the next bracket. Equal values let the sender pick freely (a form of load balancing). The MX target must be a name with A/AAAA records — never an IP literal, never a CNAME.

A name with no MX records but with A/AAAA records will, in practice, still receive mail at the A/AAAA address (most senders will fall back to the address record). Even so, publish at least one MX to make your intent explicit and to save senders an extra lookup.

A "null MX" record — MX 0 . — explicitly declares that a domain does not accept mail (RFC 7505). Useful for service-only domains that should reject mail rather than have it silently bounce or deliver somewhere wrong.

NS — name server

Lists an authoritative name server for the zone. Required at the zone apex; also used at delegation points to point to the child zone's name servers.

example.com.    86400   IN  NS  ns1.example.com.
example.com.    86400   IN  NS  ns2.example.com.

prod.example.com.  86400  IN  NS  ns1.prod.example.com.   ; delegation
prod.example.com.  86400  IN  NS  ns2.prod.example.com.   ; delegation

The set of NS records at the apex of a zone must agree with the NS records held in the parent for the delegation. Mismatches between parent and child name server lists cause partial reachability problems that are infuriating to diagnose. The parent's NS records are what resolvers actually use to find the zone; the child's NS records show up in answers and should be the same list.

SOA — start of authority

The single most important record at the apex of every zone. Exactly one SOA per zone.

example.com.   86400   IN  SOA   ns1.example.com.  hostmaster.example.com.  (
                                  2025051601    ; serial
                                  3600          ; refresh
                                  600           ; retry
                                  604800        ; expire
                                  300           ; minimum / negative TTL
                                  )

Field by field:

  • MNAME (ns1.example.com.) — the primary master name server for the zone. Conventionally one of your authoritative servers, typically the one with the live editable copy. Used by NOTIFY messages to tell secondaries to refresh.
  • RNAME (hostmaster.example.com.) — administrator email. Replace the first dot with @ to read it: in this case hostmaster@example.com. The convention is to publish a generic alias rather than a person's address.
  • SERIAL — version number of the zone. Bump it whenever you change anything. Secondaries compare their stored serial to the primary's serial during refresh; if it's different, they request a transfer. Two common formats: YYYYMMDDnn (date plus a two-digit counter for multiple changes per day) and a Unix epoch timestamp. Either works. Just bump it.
  • REFRESH — how often (seconds) secondaries should poll the primary for changes. With NOTIFY in modern BIND this is mostly cosmetic, but it's still the fallback if NOTIFY messages don't arrive.
  • RETRY — if a refresh fails (primary unreachable), how long before the secondary retries.
  • EXPIRE — if a secondary can't reach the primary for this long, the secondary stops serving the zone. Set to a value comfortably longer than any plausible primary outage; one week is a common default, two weeks is generous.
  • MINIMUM — historical name; today this is the negative TTL. Resolvers cache NXDOMAIN and NODATA answers from this zone for this many seconds. RFC 2308 redefined the field this way; before BIND 8.2 it set the default record TTL, which is now controlled by $TTL at the top of the zone file.

A common, sane SOA looks like:

@   IN  SOA  ns1   hostmaster   ( 2025051601   1h   15m   2w   1h )

(short form using @ for the apex and unqualified MNAME/RNAME, both appended by the current $ORIGIN.)

PTR — pointer

Maps an address back to a name. Lives in the reverse-mapping zones (in-addr.arpa., ip6.arpa.).

10.15.16.192.in-addr.arpa.   3600   IN   PTR   www.example.com.

A PTR record should point to one name — the canonical name — not to multiple names or to aliases. Many things break or behave oddly when reverse-DNS returns multiple PTRs for one address; mail acceptance systems in particular are picky about forward-confirmed reverse DNS (FCrDNS), where PTR(IP) = name and the name's A/AAAA resolves back to that same IP.

TXT — arbitrary text

Holds opaque text strings. Originally meant for human-readable metadata, now overloaded as the place every protocol publishes its policy:

  • SPF — v=spf1 ip4:192.0.2.0/24 include:_spf.example.net -all
  • DKIM — selector._domainkey.example.com TXT "v=DKIM1; k=rsa; p=MIIBIjANB..."
  • DMARC — _dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"
  • Domain-control verification — google-site-verification=..., MS=..., apple-domain-verification=..., etc.
  • CA validation challenges — _acme-challenge.example.com. used by Let's Encrypt and other ACME-based CAs.

A TXT record's RDATA is one or more character strings, each up to 255 bytes. Multiple strings in one record are concatenated by parsers (some servers concatenate without a separator; others preserve the separator — read the relevant RFC for the protocol you're publishing).

SRV — service location

Locates a host and port for a named service.

_sip._tcp.example.com.   3600   IN   SRV   10  60  5060  sip1.example.com.
_sip._tcp.example.com.   3600   IN   SRV   10  40  5060  sip2.example.com.
_sip._tcp.example.com.   3600   IN   SRV   20   0  5060  sip3.example.com.

Format: _service._proto.name. The values are priority (lower preferred, like MX), weight (relative weighting among equal priorities), port, and target. Used by SIP, XMPP, LDAP, Active Directory, Kerberos, Minecraft, and a long tail of services that want to abstract their port number from the consumer.

CAA — Certification Authority Authorization

Tells public CAs which CAs are allowed to issue certificates for your domain.

example.com.   3600   IN   CAA   0  issue       "letsencrypt.org"
example.com.   3600   IN   CAA   0  issue       "digicert.com"
example.com.   3600   IN   CAA   0  iodef       "mailto:security@example.com"

CAs are required (by the CA/Browser Forum baseline requirements) to check CAA records and refuse issuance if the requested CA isn't authorized. The first numeric field is a flag byte (0 or 128); the second is the property tag (issue, issuewild, iodef); the third is the value.

Less common but useful records

  • DNAME — like CNAME but for a whole subtree. A DNAME at old.example.com. pointing at new.example.com. makes foo.old.example.com. resolve as if it were foo.new.example.com. Useful for renaming subdomains.
  • NAPTR — Naming Authority Pointer. Used in ENUM (telephone-number-to-URI) and a few other regex-based service discovery schemes.
  • TLSA — DANE record. Pins TLS certificates by hash via DNS; needs DNSSEC to be useful.
  • SSHFP — pins SSH host keys by hash; needs DNSSEC similarly.
  • OPENPGPKEY — publishes an OpenPGP key by email address; niche.
  • HTTPS / SVCB — modern records (RFC 9460) that let a client discover service parameters (alpn, ipv4hint, ipv6hint) before contacting the server. Replacing the historical use of CNAME at the apex for HTTP services in the long run.
  • DNSKEY, RRSIG, NSEC, NSEC3, DS — DNSSEC plumbing. See the DNSSEC section.
  • TSIG — transaction signature, used during zone transfers and dynamic updates; not a record you publish in zone files but a meta-record used between cooperating servers.

TTL discipline

Every record has a TTL. The TTL on a record is the maximum time a resolver may cache it; resolvers typically cache for the full TTL. Choose TTLs based on how often the data changes and how quickly changes need to propagate:

  • 60–300 seconds — load balancers, frequently changing endpoints, failover targets. Pricey because every cache miss hits your authoritative servers.
  • 3600 (1 hour) — sane default for most records.
  • 86400 (1 day) — long-lived infrastructure (your own NS records, your zone's apex A/AAAA).
  • 172800 (2 days) and up — NS records at the parent for your delegation, where you almost never want changes to propagate fast because you almost never make them.

The disciplined way to make a high-TTL change quickly is to lower the TTL well in advance: change the TTL to (say) 300 seconds, wait out the previous high TTL value worldwide (so all caches now have the 300-second version), then make the actual change. Once the change has stabilised, raise the TTL back to its long value.


Zone files: master file format in detail

The format authoritative servers use to load zone data is the master file format (sometimes called zone file format). It dates back to the 1980s and is line-oriented, ASCII, and minimally structured. Every authoritative DNS server can read it; many hosted DNS services still let you import and export it.

Anatomy of a zone file

; this is a comment from a semicolon to end of line
$TTL 1h
$ORIGIN example.com.

@        IN  SOA   ns1.example.com.  hostmaster.example.com. (
                    2025051601    ; serial
                    1h            ; refresh
                    15m           ; retry
                    2w            ; expire
                    1h            ; minimum/negative TTL
                    )
@        IN  NS    ns1.example.com.
@        IN  NS    ns2.example.com.

@        IN  MX    10  mail.example.com.
@        IN  A     192.0.2.10
@        IN  AAAA  2001:db8::10

www      IN  A     192.0.2.10
www      IN  AAAA  2001:db8::10

mail     IN  A     192.0.2.20
mail     IN  AAAA  2001:db8::20

api      IN  CNAME api-prod.example.net.

ns1      IN  A     192.0.2.2
ns2      IN  A     192.0.2.3

_acme-challenge   IN  TXT   "abc123abc123abc123"

Conventions and rules:

  • One record per line (the SOA's parenthesised form spans multiple lines, but it's still one logical record).
  • Records start in column one. Whitespace at the start of a line means "use the same name as the previous record" — see the section on the @ and "ditto" rules below.
  • Comments begin with ; and run to the end of the line.
  • Strings in TXT records are quoted with double quotes; embedded quotes are escaped with backslashes.
  • TTL can be given on each record, but most zone files set a default with $TTL at the top and rely on it.
  • Class is IN for everything you'll write in 2025; you can omit it.

Control statements

The four $ control statements (and their behaviour):

  • $TTL TIME — the default TTL for any record below this line that doesn't specify its own. Required at the top of the file in modern zone files.

  • $ORIGIN NAME — set the origin used when expanding relative names below this line. Use it when a single zone file contains records from multiple subdomains, although doing this is unusual.

  • $INCLUDE FILE [ORIGIN] — splice another file in at this point. Useful for breaking a huge zone into manageable chunks.

  • $GENERATE RANGE LHS [TYPE] RHS — BIND extension that produces a series of records. Often used to generate large reverse-zone PTR ranges:

    $GENERATE 1-254 $ PTR host-$.example.com.
    

    produces 254 PTR records for 1.example.com. through 254.example.com. (in the appropriate in-addr.arpa. zone).

@ and the "ditto" rule

@ stands for the current $ORIGIN. Used most often on the SOA and apex records:

@   IN  SOA  ns1.example.com.  hostmaster.example.com. ( … )
@   IN  NS   ns1.example.com.
@   IN  NS   ns2.example.com.
@   IN  A    192.0.2.10

A line that starts with whitespace inherits the name from the previous record:

www      IN  A      192.0.2.10
         IN  A      192.0.2.11
         IN  AAAA   2001:db8::10

All three records belong to www.example.com.. This is the classic compact form for multi-record names; whether to use it or to repeat the name is a style choice. Repeating the name is more verbose but more grep-friendly.

Time-value syntax

Time fields (TTL, SOA refresh/retry/expire/min) accept either a plain number of seconds or a number with a unit:

  • s — seconds
  • m — minutes
  • h — hours
  • d — days
  • w — weeks

So 1h, 60m, and 3600 are equivalent. You can combine in some implementations (1h30m = 5400 seconds).

Trailing dots — once more, with feeling

The trailing-dot rule is so common as a source of bugs that it's worth writing again:

  • mail.example.com. (with the trailing dot) is absolute.
  • mail.example.com (no trailing dot) is relative to the current $ORIGIN. If $ORIGIN is example.com., this becomes mail.example.com.example.com..
  • A single label like www (with no dots at all) is also relative, but typically what you want — it becomes www.example.com. under the same origin.
  • The MNAME and RNAME of an SOA record, and the targets of NS, MX, CNAME, PTR, and SRV records, all need to be either absolute (with the trailing dot) or short labels you intend to qualify.

If you ever see a record like MX 10 mail.example.com and the MX target resolves to a name like mail.example.com.example.com., this is the bug you're looking at. named-checkzone will catch the obvious cases (the MX target won't resolve to an A record).

Reverse zone files

The forward and reverse zones are separate files with their own SOAs. A reverse zone file might look like:

$TTL 1h
$ORIGIN 0.2.0.192.in-addr.arpa.

@   IN  SOA   ns1.example.com.  hostmaster.example.com. (
              2025051601 1h 15m 2w 1h
              )
@   IN  NS    ns1.example.com.
@   IN  NS    ns2.example.com.

10  IN  PTR   www.example.com.
11  IN  PTR   web2.example.com.
20  IN  PTR   mail.example.com.
21  IN  PTR   mail2.example.com.

Without $ORIGIN, you'd write the names out fully:

10.0.2.0.192.in-addr.arpa.   IN   PTR   www.example.com.

Most teams set $ORIGIN to the reverse zone and use the short form. The PTR target (the right-hand side) is always a full, absolute name with the trailing dot.

Pre-flight: linting before pushing

named-checkconf /etc/named.conf
named-checkzone example.com /var/named/example.com.zone

These two commands catch the overwhelming majority of zone-file bugs:

  • Syntax errors (typos, missing fields).
  • Unresolvable MX or NS targets within the zone.
  • Records that fall outside the zone.
  • Missing SOA or NS at the apex.
  • Forgotten trailing dots that produce nonsense names.

Run them before every reload, ideally as a pre-commit hook on the zone-files repository.


BIND configuration

BIND is the reference open-source name server. The named daemon reads named.conf at startup, loads its zones, and starts answering queries. Most of what you'll do as an operator either edits a zone file or edits named.conf. The configuration syntax is its own thing — similar to C in superficial appearance, but with no relation to any DNS standard.

File layout

Common defaults vary by distribution but the shape is:

  • /etc/named.conf (RHEL, CentOS, Fedora) or /etc/bind/named.conf (Debian, Ubuntu) — the main configuration.
  • /var/named/ (RHEL family) or /var/cache/bind/ (Debian) — the working directory; zone files often live here, as do the serialized journal files (*.jnl) and statistics dumps.
  • /var/log/named/ — log files, if you've configured the logging channel to write to disk rather than syslog.
  • /etc/rndc.key — TSIG key for rndc to talk to the server.

Comments

Three syntaxes work, all equivalent:

/* C-style block comment */
// C++-style line comment
# shell-style line comment

The basic statements

A named.conf is a sequence of statements. The ones you'll use most:

  • options { ... } — global options. Exactly one.
  • zone "NAME" IN { ... } — declare a zone you serve. One per zone.
  • acl "NAME" { ... } — define a named address match list.
  • key "NAME" { ... } — declare a TSIG key.
  • controls { ... } — configure the rndc control channel.
  • logging { ... } — configure logging channels and categories.
  • view "NAME" { ... } — split-horizon DNS (separate views of the same name to different clients).
  • server IP { ... } — per-remote-server overrides.
  • include "FILE"; — splice another configuration file in.

A minimal authoritative-only named.conf

options {
    directory       "/var/named";
    listen-on       { 127.0.0.1; 192.0.2.10; };
    listen-on-v6    { ::1; 2001:db8::10; };
    allow-query     { any; };
    recursion       no;
    version         "no";
    minimal-responses yes;
};

zone "example.com" IN {
    type primary;        # was "master" pre-BIND 9.16
    file "primary/example.com.zone";
    allow-transfer { 198.51.100.20; };   // ns2's IP
    also-notify    { 198.51.100.20; };
    notify         yes;
};

zone "0.2.0.192.in-addr.arpa" IN {
    type primary;
    file "primary/192.0.2.rev";
};

logging {
    channel main {
        file "/var/log/named/named.log" versions 3 size 10m;
        severity info;
        print-time     yes;
        print-category yes;
        print-severity yes;
    };
    category default { main; };
};

A few things to note about this skeleton:

  • recursion no makes this server authoritative-only — it won't walk the tree on behalf of arbitrary clients. The modern best practice for any internet-facing authoritative server.
  • version "no" doesn't actually set the version; it tells the server to refuse to disclose its version. Cosmetic but reduces fingerprinting.
  • minimal-responses yes strips the AUTHORITY and ADDITIONAL sections from responses where they're not required, reducing payload size and amplification potential.
  • listen-on/listen-on-v6 restrict which interfaces the server binds to. Without these, BIND listens on all interfaces.
  • The notify and also-notify statements inside the zone block are how the primary tells secondaries to refresh after a change.

A minimal recursive resolver named.conf

options {
    directory       "/var/named";
    listen-on       { 127.0.0.1; 192.0.2.5; };
    listen-on-v6    { ::1; };
    allow-query     { localnets; };
    allow-recursion { localnets; };
    recursion       yes;
    forwarders      { 1.1.1.1; 8.8.8.8; };
    forward         first;
    dnssec-validation auto;
    minimal-responses yes;
};

zone "." IN {
    type hint;
    file "named.ca";   # the root hints file
};

zone "0.0.127.in-addr.arpa" IN {
    type primary;
    file "primary/127.0.0.zone";
};

Notes:

  • localnets is a built-in ACL that matches any address on a network the server itself has an interface in. It's a cheap way to limit recursion to "the network this server is on".
  • forwarders plus forward first makes this resolver use the upstream resolvers when possible but fall back to walking the tree itself if the forwarders fail. forward only is the alternative if you never want it to walk the tree.
  • dnssec-validation auto turns on DNSSEC validation using the built-in trust anchors. With this on, the resolver will return SERVFAIL for DNSSEC-broken zones — which is correct behaviour but occasionally surprising when an external zone has botched its DNSSEC.

Address match lists and ACLs

Many statements take an address match list. A match list is one or more of:

  • An IP address: 192.0.2.10
  • A network in CIDR: 192.0.2.0/24
  • The negation of any of the above: !192.0.2.66
  • Another ACL by name: localnets, localhost, any, none, or any user-defined name
  • A key reference: key "tsig-name" (so transfers/updates can be authorized by signature rather than IP)

Order matters; the first match wins.

acl "trusted" {
    127.0.0.1;
    ::1;
    192.0.2.0/24;
    !192.0.2.66;        // explicitly excluded inside the trusted block
};

options {
    allow-query     { trusted; };
    allow-recursion { trusted; };
};

Zone types

  • primary (or master) — this server holds the editable copy.
  • secondary (or slave) — this server fetches a read-only copy from a primary via zone transfer.
  • hint — used only for the root zone; the file lists the root servers' addresses, used to bootstrap the recursor's understanding of where to start a tree walk.
  • forward — declares a zone whose queries should be forwarded to a specific set of servers, regardless of the global forwarders.
  • stub — a partial copy of another zone's NS records, used when you want to be a slave for the delegation but not the data. Rare in modern setups.
  • static-stub, redirect, mirror — newer specialised types, used in particular operational scenarios.

A secondary zone block looks like:

zone "example.com" IN {
    type secondary;
    masters { 192.0.2.10; };
    file "secondary/example.com.zone";
    allow-transfer { none; };       // unless you have tertiaries
};

Secondaries should allow-transfer { none; } unless they're themselves serving as a master for further servers down the chain.

NOTIFY and incremental zone transfers

When you change a zone on the primary and bump the SOA serial, the primary sends a NOTIFY message to each name server listed in the zone's NS records (and to anything in also-notify). On NOTIFY, the secondary immediately polls the primary, compares serials, and issues an AXFR (full zone transfer) or IXFR (incremental zone transfer, RFC 1995) to fetch the new data.

IXFR exchanges only the records that have changed since the secondary's last serial, which is dramatically more efficient on large zones. Modern BIND uses IXFR by default if it can; if a secondary's stored serial is too old or the journal has been truncated, it falls back to AXFR.

rndc reload and rndc reload ZONE ask the server to re-read its configuration / zone data; with NOTIFY plus IXFR, propagation to secondaries is typically a few seconds.

Operational tools

named-checkconf [-z] [/etc/named.conf]
    # validate the config; -z also tries to load every zone

named-checkzone example.com /var/named/example.com.zone
    # validate one zone file

rndc reload
rndc reload example.com
rndc reconfig
rndc retransfer example.com
rndc flush                 # clear the recursive cache
rndc flushname NAME        # clear one name from the recursive cache
rndc dumpdb -all           # dump the cache and zone data to a file
rndc dumpdb -cache         # cache only
rndc dumpdb -zones         # zones only
rndc stats                 # write statistics to a file
rndc status                # quick health summary
rndc trace [LEVEL]         # set debug level
rndc notrace               # turn off debug output
rndc querylog on           # log every query (expensive)
rndc querylog off
rndc stop                  # graceful shutdown
rndc halt                  # immediate shutdown without sync

systemctl status named
systemctl reload named
systemctl restart named
journalctl -u named -f

rndc talks to named over a control channel, authenticated by a TSIG key in /etc/rndc.key. The control statement in the config:

controls {
    inet 127.0.0.1 port 953
        allow { 127.0.0.1; }
        keys  { "rndc-key"; };
};

key "rndc-key" {
    algorithm hmac-sha256;
    secret "abcd1234...==";
};

rndc-confgen generates the key file. Distribution packages typically set this up out of the box.

Logging

The logging statement defines channels (where messages go) and categories (which kinds of messages). Useful categories:

  • default — anything not matched by another category.
  • general — generic messages.
  • queries — every query (loud; only enable when debugging).
  • query-errors — only failed queries (useful in production).
  • client — client connections.
  • network — network-level events.
  • resolver — recursive resolver activity.
  • xfer-in, xfer-out — zone-transfer events.
  • notify — NOTIFY messages.
  • update, update-security — dynamic update events.
  • dnssec — DNSSEC validation, signing, and key events.
  • lame-servers — broken delegations the resolver has found.
  • security — denied queries / transfers / updates.

Channels are file or syslog destinations with severity filters:

logging {
    channel security_log {
        file "/var/log/named/security.log" versions 5 size 5m;
        severity info;
        print-time     yes;
        print-severity yes;
        print-category yes;
    };
    channel xfer_log {
        file "/var/log/named/xfer.log" versions 5 size 5m;
        severity info;
        print-time yes;
    };

    category security  { security_log; };
    category xfer-in   { xfer_log; };
    category xfer-out  { xfer_log; };
    category notify    { xfer_log; };
};

Views (split-horizon DNS)

Sometimes you want internal clients to see different DNS data than external clients. BIND views let one server present different zone contents based on who's asking.

view "internal" {
    match-clients { 192.0.2.0/24; };
    recursion yes;
    zone "example.com" IN {
        type primary;
        file "primary/example.com.internal";
    };
};

view "external" {
    match-clients { any; };
    recursion no;
    zone "example.com" IN {
        type primary;
        file "primary/example.com.external";
    };
};

match-clients is evaluated in declaration order; the first matching view wins. Once views are in use, every zone must be inside a view.

Views are powerful but operationally costly — you have two zone files to keep consistent, two sets of records to update on changes, two transfer relationships to manage. Modern alternatives include hosting internal records in a separate, dedicated internal zone (e.g. example.internal.) or using a DNS-aware split tunnel on the client side.

Forwarders revisited

Per-zone forwarding is configured with a forward zone:

zone "internal.corp." IN {
    type forward;
    forwarders { 10.0.0.5; 10.0.0.6; };
    forward only;
};

This is the common pattern when one zone lives on a different DNS infrastructure (e.g. a Windows AD domain) but you want one resolver endpoint for everything.


Query tools: dig, host, nslookup, delv

You will spend more time staring at dig output than at any other DNS tool. It's the operational gold standard: explicit, verbose, no hidden behaviour, no platform variations worth worrying about. The others are useful in narrower situations.

dig

dig [@server] [domain] [type] [class] [+flags] [-options]

Defaults: query the resolver in /etc/resolv.conf for the A record of the given name in the IN class.

Common forms

dig www.example.com                  # default A query, default resolver
dig www.example.com AAAA             # specifically IPv6 address
dig example.com MX
dig example.com NS
dig example.com SOA
dig example.com TXT
dig example.com ANY                  # all types (commonly filtered)
dig @8.8.8.8 example.com             # query a specific resolver
dig @ns1.example.com example.com SOA # ask the authoritative server directly
dig +short example.com               # just the answers, no headers
dig -x 192.0.2.10                    # reverse: ask for the PTR
dig +trace example.com               # walk from the root yourself
dig +tcp example.com                 # force TCP
dig +tries=1 +timeout=2 …            # don't retry, fail fast
dig -p 5353 @ns1.local example.com   # non-default port
dig -y key-name:secret example.com   # TSIG-signed query

Reading dig output

A typical dig output has four sections you actually care about, plus some commentary at the top and bottom.

; <<>> DiG 9.18.x <<>> www.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.example.com.    IN   A

;; ANSWER SECTION:
www.example.com.   300  IN   A   192.0.2.10
www.example.com.   300  IN   A   192.0.2.11

;; Query time: 18 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Sat May 17 09:22:13 PDT 2025
;; MSG SIZE  rcvd: 73

The pieces:

  • HEADER line — opcode (QUERY, occasionally UPDATE), status (NOERROR, NXDOMAIN, SERVFAIL, REFUSED, FORMERR, NOTIMP, …), and a transaction ID.
  • flags — qr (this is a response), aa (authoritative answer), tc (truncated), rd (recursion desired by client), ra (recursion available from server), ad (DNSSEC authenticated), cd (checking disabled — DNSSEC validation bypassed by client request).
  • counts — how many records in each section.
  • OPT PSEUDOSECTION — EDNS0 metadata.
  • QUESTION SECTION — what was asked. Always one record.
  • ANSWER SECTION — the real records, when there are any.
  • AUTHORITY SECTION — the NS records for the zone the answer came from, or for the zone where the answer wasn't found.
  • ADDITIONAL SECTION — extra records the server thought you might want (often glue, OPT pseudosection details, EDNS metadata).
  • Query time / SERVER / WHEN / MSG SIZE — diagnostics.

Status codes you'll see

  • NOERROR — the query succeeded. If the answer section is empty, that's "name exists, but has no records of the requested type" (NODATA).
  • NXDOMAIN — the name doesn't exist.
  • SERVFAIL — the server failed to produce an answer. Common causes: DNSSEC validation failure, broken authoritative server, recursion failure.
  • REFUSED — the server is unwilling to answer (often recursion turned off for non-trusted clients, or a query for a zone the server isn't authoritative for and isn't recursing on behalf of you).
  • FORMERR — the query was malformed. Almost always a tooling bug rather than something the user wrote.
  • NOTIMP — the server doesn't implement the requested operation.

Useful +options

  • +short — strip everything except the answer values.
  • +noall +answer — same idea, slightly more readable for multi-record answers.
  • +nocmd — suppress the leading "<<>> DiG …" version line.
  • +stats — print the trailing stats block.
  • +nostats — suppress it.
  • +trace — perform an iterative resolution from the root, showing each delegation. Equivalent to walking the tree by hand.
  • +norecurse — don't ask the server to recurse; useful when pinging an authoritative server with @server and you want to prove it answers authoritatively.
  • +dnssec (or +do) — set the DO bit; ask for DNSSEC RRs.
  • +nodnssec — clear it.
  • +cdflag — set the CD (checking-disabled) bit; ask the resolver not to validate DNSSEC.
  • +adflag — display the AD flag handling.
  • +nsid — request the server's NSID (a server-identifier EDNS option, useful when the same anycast IP is many servers).
  • +subnet=CIDR — send EDNS Client Subnet (RFC 7871). Useful when testing geo-aware DNS.
  • +bufsize=N — UDP payload size to advertise via EDNS0.
  • +tcp — force TCP.
  • +notcp — disable fallback to TCP on truncation.
  • +retry=N, +tries=N, +timeout=S — retry/timeout tuning.
  • +keepopen — reuse one TCP connection across multiple queries on the same dig command line.

You can pass several names on one command line and dig will query each in turn:

dig +noall +answer example.com mail.example.com www.example.com

Zone transfers with dig

dig @ns1.example.com example.com AXFR              # request a full zone transfer
dig @ns1.example.com example.com IXFR=2025051601   # incremental from this serial

This is a quick smoke-test for whether a server allows transfers from your IP. Production transfers should always be authenticated with TSIG.

Bulk lookups

dig -f names.txt +noall +answer

reads names from a file, one per line, and runs each query. Combined with xargs and awk, useful for auditing thousands of records.

host

host is dig for the impatient — terse, sensible defaults, nothing to read.

host example.com                 # A and AAAA
host -t MX example.com
host -t NS example.com
host -t TXT example.com
host -a example.com              # all (verbose) — shows every type the server returns
host -v example.com              # verbose; closer to dig output
host 192.0.2.10                  # reverse lookup
host example.com 1.1.1.1         # use a specific resolver

host is fine for one-line probes. For anything operational, use dig.

nslookup

nslookup is the historical Unix DNS tool, present on every platform including Windows. It has two modes: command-line and interactive.

nslookup example.com
nslookup -type=MX example.com
nslookup -type=NS example.com
nslookup example.com 8.8.8.8       # specific resolver

nslookup
> set type=mx
> example.com
> set type=ns
> example.com
> exit

Practical notes:

  • nslookup's output is not stable across implementations (BSD vs GNU vs Windows). Don't parse it in scripts.
  • The - form on Windows / older Unix sometimes triggers an interactive mode if you forget the trailing query string.
  • For modern operational work, prefer dig everywhere except Windows servers where you don't have BIND tools installed. Windows ships Resolve-DnsName in PowerShell, which is generally a better choice than nslookup.

delv

delv ("Domain Entity Lookup & Validation") is a DNSSEC-aware client. It does its own validation against the configured trust anchors and tells you whether the answer is fully validated, partially validated, or unvalidated. Use it to test whether a zone's DNSSEC chain is intact.

delv www.example.com
delv +rtrace www.example.com           # show validation steps
delv +cd www.example.com               # disable validation
delv -a /etc/bind/bind.keys example.com

kdig and drill

kdig ships with the Knot DNS toolkit; drill ships with NSD/Unbound. Both are functionally similar to dig with slightly different option flags. Worth knowing they exist; dig is fine unless you have a specific reason to use one of these.

Resolve-DnsName (PowerShell)

Modern Windows operators use Resolve-DnsName instead of nslookup:

Resolve-DnsName example.com
Resolve-DnsName example.com -Type MX
Resolve-DnsName example.com -Server 8.8.8.8
Resolve-DnsName example.com -DnssecOk
Resolve-DnsName -Name example.com -Type ANY -Server ns1.example.com

Returns objects, not text — pipeable to Where-Object, Select-Object, etc.

Network-level tooling for DNS troubleshooting

DNS lives on TCP and UDP port 53. Sometimes the answer to "DNS isn't working" is at the network layer:

nc -uvz host 53                    # UDP port-knock
nc -vz  host 53                    # TCP port-knock
tcpdump -ni any port 53            # see DNS packets fly by
tcpdump -ni any port 53 -w dns.pcap  # capture for later analysis
tshark -ni any port 53             # decoded DNS traffic
ss -tnlp | grep :53                # what's listening locally?
ss -unlp | grep :53                # UDP listeners

Combine these with dig +trace and authoritative-server probes (dig @ns1 …) to localise where the failure is happening.


Operational tasks

These are the recipes you reach for repeatedly. Most are "recipe-shaped" — same five steps every time, with the values filled in.

Add a record

  1. Edit the zone file.
  2. Bump the SOA serial. Always.
  3. named-checkzone ZONE FILE to validate.
  4. rndc reload ZONE (or rndc reload for everything).
  5. dig @LOCAL_NS NEW_NAME TYPE to confirm the record is served.
  6. After notify propagates, query each authoritative server listed in NS records: dig @nsX NEW_NAME TYPE +short.
  7. Once all secondaries agree, you're done.

Remove a record

Same process as add. The thing to watch is whatever depends on the record — if any cache (yours, your customers', a CDN) still holds a copy, the record will continue to resolve until the TTL expires. To remove a record promptly, lower the TTL well in advance:

  1. Lower the TTL to (e.g.) 300 seconds. Bump SOA. Reload.
  2. Wait for more than the previous TTL — every cache in the world that still has the old record now has the short-TTL version.
  3. Remove the record. Bump SOA. Reload.
  4. Within the new TTL, every cache will discard it.

Move a service to a new IP

The "TTL game". Plan it in advance:

  1. T-2 days: Lower the record's TTL to 300 (or whatever short value you can stand). Bump SOA. Reload. Verify the lowered TTL is what resolvers worldwide are now caching by querying external resolvers and watching the TTL count down to 300.
  2. T-0: Change the record to the new IP. Bump SOA. Reload.
  3. Watch the new IP receive traffic; watch the old IP empty out over the next 5 minutes.
  4. Once traffic at the old IP is at zero, decommission it.
  5. T+ a week: Raise the TTL back to its original value once you're confident no further changes are coming.

Decommission a host

Remove every record that points to it (forward A/AAAA, reverse PTR, any MX listing it, any SRV listing it) before you turn the host off. Drop the TTLs first (TTL game above) so caches expire promptly. Don't pull the host until DNS has caught up; otherwise you'll have a window where queries succeed and connections fail.

Stand up a new secondary

  1. On the new secondary, configure a secondary zone block pointing at the primary's IP, with the matching TSIG key if you're using one (you should be — see TSIG below).
  2. On the primary, add the new secondary's IP to allow-transfer and also-notify.
  3. Add the new secondary's hostname to the zone's NS records and bump the SOA serial.
  4. rndc reconfig on both ends.
  5. Watch the secondary log: it should issue an AXFR, then start responding.
  6. Test with dig @new-secondary ZONE SOA +norecurse — you should see the same SOA serial as the primary.
  7. Lastly, ask the parent zone's administrator to update the delegation NS records to include the new secondary. Until the delegation is updated, public traffic still won't reach the new secondary; but cache and existing zone-transfer subscribers will notice it.

Retire a secondary

Reverse the above process. Remove the NS record, wait for delegation TTL, remove from allow-transfer / also-notify, shut down the secondary.

Rotate a TSIG key

  1. Generate a new key alongside the old.
  2. Distribute the new key to both ends.
  3. Configure both primary and secondary to accept either key (BIND supports multiple keys per server statement).
  4. rndc reconfig both ends.
  5. Verify zone transfers work using the new key (force one with rndc retransfer).
  6. Remove the old key from both configurations.
  7. rndc reconfig again.

Force a zone transfer

rndc retransfer ZONE

on the secondary. The secondary contacts the primary and refetches the zone. Useful when you've changed something the secondary hasn't picked up; usually a sign that NOTIFY isn't reaching it (firewall, IP mismatch in also-notify, etc.).

Flush the cache (recursor)

rndc flush                # everything
rndc flushname EXAMPLE.com.

After fixing data on the authoritative side, this lets the recursor re-fetch instead of waiting for TTL expiration.

Inspect the cache

rndc dumpdb -cache /var/named/cache_dump.txt
less /var/named/cache_dump.txt

Useful for "is the bad answer in the cache, or is the upstream giving me bad answers?".

Check that all authoritatives agree

for ns in $(dig +short example.com NS); do
  echo "=== $ns ==="
  dig @"$ns" example.com SOA +short
  dig @"$ns" www.example.com +short
done

If the SOA serials disagree, you have a propagation problem — one secondary is stale. Investigate NOTIFY and allow-transfer.

Rolling out a new authoritative server while keeping uptime

  1. Set up the new server as a secondary, fetching from your current primary.
  2. Wait until it has the zone fully and is responding correctly.
  3. Add it to the zone's NS records (so the parent zone learns about it).
  4. Wait out the parent's delegation TTL.
  5. Remove the old server from NS records, wait again, then turn it off.

This avoids any window where a queried name server is offline.

The dangerous changes

A handful of operational changes have a long blast radius if you get them wrong. Treat them with extra care:

  • Delegation changes. Updating which name servers are authoritative for your zone with the parent. Wrong NS data here can take your domain offline for hours or days.
  • DNSSEC key rolls. A botched roll can SERVFAIL your zone worldwide. Dry-run with a test zone first; let dnssec-policy manage the timing for you in modern BIND.
  • Apex record changes. The apex is what most external consumers actually use; change windows here should respect TTLs on the order of 24h, not 1h.
  • Cutting a zone. Splitting a zone into a parent + delegated child is more invasive than it looks; the new child needs its own NS records, the parent's records inside that subdomain need to move to the child, and the cut moment needs careful coordination.

Security: TSIG, DNSSEC, and the threat model

DNS has two distinct security stories, addressing two different threats.

TSIG authenticates transactions between cooperating servers. It guarantees that a zone transfer or dynamic update came from the expected source and wasn't tampered with in flight. The shared secret means TSIG works only between servers that have already exchanged keys out of band; it doesn't protect against an attacker on the path between a client and a recursor, and it doesn't help an arbitrary resolver verify that the answer it got from an authoritative server is genuine.

DNSSEC authenticates the data itself, end to end, so that any resolver — anywhere on the Internet — can prove cryptographically that an answer it received originated from the zone owner and wasn't modified in transit. DNSSEC defends against cache poisoning, on-path tampering, and any other attempt to substitute false answers. It does not protect query confidentiality (use DoT or DoH for that) and it does not stop denial-of-service.

Production zones in 2025 typically use both: TSIG for admin-to-admin and primary-to-secondary, DNSSEC for the public- facing answers.

TSIG — Transaction SIGnatures

TSIG (RFC 8945, originally 2845) attaches an HMAC to a DNS message, keyed on a shared secret known to both ends.

Generating a key

tsig-keygen -a hmac-sha256 example-tsig > tsig.key
cat tsig.key

Output looks like:

key "example-tsig" {
    algorithm hmac-sha256;
    secret "abcd1234abcd1234abcd1234abcd1234abcd1234abcd==";
};

Keep this file readable only to root and the named user.

Using the key on the primary

include "/etc/bind/tsig.key";

zone "example.com" IN {
    type primary;
    file "primary/example.com.zone";
    allow-transfer { key "example-tsig"; };
    also-notify    { 198.51.100.20 key "example-tsig"; };
    notify yes;
};

Now the primary requires that any AXFR/IXFR request be TSIG-signed with the named key. The IP-based allow-transfer check is replaced (or augmented) by a cryptographic one — a much stronger guarantee.

Using the key on the secondary

include "/etc/bind/tsig.key";

server 192.0.2.10 {
    keys { "example-tsig"; };
};

zone "example.com" IN {
    type secondary;
    masters { 192.0.2.10 key "example-tsig"; };
    file   "secondary/example.com.zone";
    allow-transfer { none; };
};

The server clause says "when talking to this IP, sign messages with this key", and the masters clause restates it for clarity.

TSIG-signed dynamic updates

A separate use of TSIG: signing nsupdate messages so the authoritative server will accept dynamic updates from authorized clients only.

zone "example.com" IN {
    type primary;
    file "primary/example.com.zone";
    update-policy {
        grant updater-key zonesub *.example.com. A AAAA TXT;
    };
};

A client signs its update with the matching updater-key. See "Dynamic updates" below.

TSIG errors

The BADTIME and BADKEY errors are common when a primary and secondary's clocks are skewed too far apart (TSIG signatures have a built-in time window) or when the keys disagree:

  • BADTIME — clock skew exceeds the fudge factor (default 300s on each side). Run NTP/chrony.
  • BADKEY — the signing key isn't recognised by the receiver.
  • BADSIG — the signature didn't verify (data tampered, or wrong key on one side).

tsig verify failure in named's logs is one of these.

DNSSEC — DNS Security Extensions

DNSSEC adds cryptographic signatures to every record set in a zone, plus the metadata to walk a chain of trust from the root to your zone. The pieces:

  • DNSKEY — the public keys for the zone. There are two roles: the Key Signing Key (KSK), which signs DNSKEY RRsets, and the Zone Signing Key (ZSK), which signs everything else. The separation lets you roll ZSKs frequently and KSKs rarely.
  • RRSIG — a signature record over each RRset.
  • NSEC / NSEC3 — proof of non-existence. When a name doesn't exist, the authoritative server returns an NSEC record saying "the names that do exist are X and Y, and the queried name falls between them, so it provably doesn't exist". NSEC3 hashes the names to make zone enumeration harder.
  • DS (Delegation Signer) — a hash of the child's KSK, published in the parent zone. This is the link in the chain of trust between parent and child.

A client validating a name follows the chain: root → TLD → your zone, verifying signatures at each step. The root's keys are configured as the trust anchor in resolvers like BIND (dnssec-validation auto).

Signing a zone

Modern BIND uses dnssec-policy to manage the entire signing lifecycle automatically. A minimal policy:

dnssec-policy "default" {
    keys {
        ksk lifetime unlimited algorithm 13;
        zsk lifetime P30D      algorithm 13;
    };
};

zone "example.com" IN {
    type primary;
    file "primary/example.com.zone";
    dnssec-policy "default";
    inline-signing yes;
};

inline-signing yes keeps an unsigned working copy of the zone and lets BIND maintain the signed copy automatically. With dnssec-policy, BIND generates keys, rolls them according to the policy's lifetimes, and re-signs records before signatures expire. You don't have to think about the details if you trust the defaults.

The older, manual workflow (dnssec-keygen, dnssec-signzone) still works but is worth replacing with dnssec-policy if you can.

Bootstrapping the chain to the parent

After your zone is signed, the parent (your TLD or your delegation provider) needs your DS record to complete the chain of trust. Steps:

  1. After signing, retrieve your DS record from your zone:

    dig @localhost example.com DS
    # or
    dnssec-dsfromkey -2 KSK_KEY_FILE
    
  2. Submit that DS record to your registrar/parent. Most modern registrar control panels have a DNSSEC tab where you paste in the DS values.

  3. Wait for the parent to publish the DS in their zone (delays vary; some are minutes, some are hours).

  4. Validate end-to-end with a DNSSEC-aware resolver:

    delv example.com
    

    You should see ; fully validated in the output.

Key rolls

A KSK roll changes the parent's DS record, and is therefore more involved than a ZSK roll which changes only signatures inside your zone. With dnssec-policy, BIND coordinates the roll: it publishes the new key, waits the configured time for old caches to expire, swaps the key in service, then retires the old key. For KSK rolls specifically, an automated mechanism called CDS/CDNSKEY (RFC 7344, RFC 8078) lets BIND publish the new DS in your zone and have the parent automatically pick it up. Most modern registrars support this; check yours.

When DNSSEC goes wrong

A misconfigured or expired DNSSEC zone returns SERVFAIL to validating resolvers. Symptoms:

  • Some users (those whose recursors validate) can't resolve your domain at all; others (whose recursors don't validate, or have CD set) work fine.
  • dig +dnssec shows RRSIG records but the SERVFAIL still comes back when validation runs.
  • delv shows the validation step that failed.

Common causes:

  • The DS record at the parent doesn't match your KSK (you rolled but didn't update the parent).
  • A signature expired (the dnssec-policy machinery wasn't running, or the zone was forgotten). RRSIGs have an expiry built in; if the signing daemon stops, your zone breaks silently after the longest signature lifetime.
  • The chain of trust from the root has a break above you (rare, but newsworthy when it happens).

The emergency fix when your own DNSSEC has failed: turn off DNSSEC on the zone (or, if you're a recursor and a critical external zone is broken, turn on dnssec-must-be-secure no for that zone, or use a "negative trust anchor" — rndc managed-keys with appropriate options).

DANE and TLSA

A DNSSEC-secured benefit: you can publish a hash of your TLS certificate in DNS via a TLSA record, and a DANE-aware client will verify the certificate against that hash before trusting the CA chain. This protects against rogue CAs. Not yet broadly deployed, but the standard is mature.

_443._tcp.www.example.com. IN TLSA 3 1 1
   <SHA256 hash of cert>

Other security hardening

  • Hide your version: version "no"; in options.
  • Refuse recursion on authoritative servers: recursion no;.
  • Limit recursion on resolvers: allow-recursion { trusted; };.
  • Rate limiting: rate-limit { responses-per-second 10; }; — blunt-instrument protection against DDoS amplification.
  • Run as a dedicated user, not root.
  • Consider chroot: older BIND packaging shipped a bind-chroot variant; modern systems with systemd sandboxing achieve similar isolation through unit-file directives.
  • Monitor: SOA serial mismatches between primary and secondaries, SERVFAIL spikes, NXDOMAIN spikes (often reconnaissance), DNSSEC validation failures.
  • Patch promptly when CVEs come out. BIND has a regular cadence of security advisories.

Modern privacy: DoT, DoH, query minimisation

DNS queries are sent in cleartext on UDP/53 by default. Anyone on the path can see what names you're resolving. Two transports encrypt this:

  • DoT (DNS-over-TLS, RFC 7858) — uses TCP/853.
  • DoH (DNS-over-HTTPS, RFC 8484) — uses TCP/443.

Both protect the stub-to-resolver leg. The resolver-to-authoritative leg is still cleartext in most deployments, though there are RFCs (DoT to authoritative, RFC 9103) addressing this. Query name minimisation (RFC 9156, implemented in modern recursors) sends only the relevant label of the query name to each authoritative server during a tree walk, limiting how much information any single authoritative server sees.

For an end-user resolver (laptop, phone), pointing at a DoH or DoT-capable resolver like 1.1.1.1 is a no-cost privacy improvement. For an enterprise resolver, DoT inside the organisation plus regular DNS to authoritatives is a common compromise.


Troubleshooting recipes

The vast majority of DNS issues fall into a handful of patterns. Knowing the patterns means recognising them in seconds rather than debugging from first principles.

"The site is down"

First, isolate whether DNS is the problem at all. Three quick queries from the affected machine, in order:

dig +short example.com                          # answer from default resolver
dig +short example.com @8.8.8.8                 # answer from an external resolver
dig +short example.com @ns1.example.com         # answer straight from the authoritative

Outcomes:

  • All three agree, and the IP is reachable → DNS is fine; the problem is at the application layer, the network, or the load balancer. Move on to curl -vk https://..., nc, ping.

  • All three agree, and the IP is not reachable → DNS is fine; the problem is the host or its network.

  • Authoritative agrees with itself but external resolvers disagree → propagation problem. The cache somewhere has stale data; wait out the TTL or, if it's your own resolver, flush.

  • Authoritative disagrees with itself across NS records → one of your secondaries didn't pull the latest zone. Check the SOA serial on each NS:

    for ns in $(dig +short example.com NS); do
      s=$(dig +short @"$ns" example.com SOA | awk '{print $3}')
      echo "$ns -> serial $s"
    done
    
  • External resolver returns SERVFAIL but the authoritative works → DNSSEC validation failure on the external resolver's side, OR an authoritative server is unreachable from that resolver's network.

  • Default resolver returns nothing but @ns1 works → your resolver is down, or a forwarder it depends on is down, or there's a path problem from your machine to the resolver.

"The new record isn't visible"

dig +short newhost.example.com @ns1.example.com  # is it actually in the zone?

If yes:

  • Check that all secondaries have the latest SOA serial:

    for ns in $(dig +short example.com NS); do
      dig @"$ns" example.com SOA +short
    done
    
  • Wait out the TTL of the NXDOMAIN that was previously cached, or flush the local resolver:

    sudo systemd-resolve --flush-caches            # systemd-resolved
    sudo killall -HUP mDNSResponder                # macOS
    ipconfig /flushdns                             # Windows
    sudo rndc flush                                # local BIND recursor
    

    Browsers cache DNS independently of the OS — restart the browser, or test with curl to bypass the browser cache.

If the record isn't in the zone on the authoritative server, you forgot to bump the SOA, didn't reload, or edited the wrong file.

"DNSSEC says SERVFAIL"

dig +dnssec +cd example.com           # bypass validation
delv example.com                       # validating client

If +cd works and delv shows a chain failure, the DNSSEC chain is broken. Common causes:

  • The parent zone's DS record doesn't match your KSK (registrar not updated after a roll).
  • Your zone's signatures expired (signing daemon stopped, or policy hasn't run).
  • An RRset has been added that wasn't signed.

Look at the delv output for the specific failure. The fastest fix is usually rolling the zone's keys forward (re-signing) and re-publishing the DS to the parent.

"Wrong server is answering"

The stub resolver is sending queries somewhere different from where you think. Check:

cat /etc/resolv.conf
resolvectl status                  # systemd-resolved
networksetup -getdnsservers Wi-Fi  # macOS

Common causes:

  • VPN client added a name server when connecting and didn't remove it cleanly.
  • DHCP is overriding /etc/resolv.conf.
  • macOS per-domain /etc/resolver/ files are routing some names to a corporate resolver.
  • The browser is using DoH directly to a public resolver, bypassing the OS.

"The secondary won't pull the zone"

dig @primary-ip example.com AXFR         # try a transfer manually

Transfer failed. → check allow-transfer ACL on the primary, TSIG keys on both ends, time skew if TSIG is in use, firewall between primary and secondary on TCP/53.

journalctl -u named -f                    # both ends
tail -F /var/log/named/named.log

Look for xfer-in:, xfer-out:, transfer of, notify, refused, and bad signature messages.

"Why is this query slow?"

dig +stats example.com                    # see Query time
dig +trace example.com                    # see each step

A slow trace usually points at a slow upstream (root → TLD → zone) or at a misconfigured authoritative server that's timing-out on UDP and forcing TCP fallback. Use dig +tcp to test that path independently.

"Weird intermittent failures"

Several common patterns:

  • DNSSEC at the recursor is dropping responses too large for UDP and the path filters TCP/53. Test with dig +tcp.
  • Anycast routing change: queries that used to go to instance A now go to instance B, which doesn't have the same data (because zone replication isn't perfect). Use +nsid to identify which instance is answering.
  • One secondary out of N is stale: queries that hit it return old data, queries that hit the others return new data.
  • EDNS firewall mishandling: a firewall doesn't pass DNS packets larger than 512 bytes. Test with dig +bufsize=512 vs +bufsize=4096.

"Cache poisoning concern"

If you're worried someone is injecting forged answers into your recursor:

  • Run a DNSSEC-validating recursor (dnssec-validation auto or similar in your platform).
  • Make sure your recursor uses random source ports for outbound queries (BIND has done this since 9.5; you almost certainly don't need to configure it).
  • Restrict inbound queries to known clients (allow-recursion), or your recursor becomes the cache for the world and an open amplifier for DDoS.

Useful one-liners for diagnosis

# Compare what every authoritative server returns for a name
for ns in $(dig +short example.com NS); do
  echo "== $ns =="
  dig @"$ns" www.example.com +short
done

# Walk the tree by hand
dig +trace +nodnssec example.com

# What's my recursor and what TTL is it caching?
dig www.example.com | head -20

# Strip everything except the answer
dig +short www.example.com

# Get the SOA serial of every NS for a zone
for ns in $(dig +short example.com NS); do
  printf '%-35s %s\n' "$ns" "$(dig @"$ns" example.com SOA +short)"
done

# Detect a misconfigured PTR (forward and reverse don't agree)
ip=$(dig +short www.example.com)
ptr=$(dig +short -x "$ip")
fwd=$(dig +short "$ptr")
echo "$ip -> $ptr -> $fwd"
# Should: $ip == $fwd. If not, you have a forward-confirmed-reverse-DNS bug.

Dynamic updates

DNS records can be added, modified, or removed at runtime via dynamic update (RFC 2136). The protocol uses an UPDATE opcode (as opposed to QUERY) and is most commonly used for:

  • DHCP servers updating PTR records as leases come and go.
  • Active Directory clients registering themselves with the AD-DNS.
  • ACME/DNS-01 certificate validation, where Certbot or similar briefly creates a _acme-challenge TXT record to prove control of the domain.
  • Cloud automation that maintains records for ephemeral compute.

Configuring a zone for updates

zone "example.com" IN {
    type primary;
    file "primary/example.com.zone";
    update-policy {
        grant updater-key zonesub * A AAAA TXT;
    };
    notify yes;
};

key "updater-key" {
    algorithm hmac-sha256;
    secret "abcd...==";
};

update-policy is more granular than the older allow-update (which is purely IP-based). The grant syntax allows specifying exactly what records the holder of a given key may change. The zonesub qualifier limits updates to subdomains of the zone; other qualifiers (name, subdomain, wildcard, self, selfsub, external) constrain in different ways.

Using nsupdate from the command line

nsupdate -k /etc/bind/updater.key
> server ns1.example.com
> zone example.com
> update add foo.example.com 300 A 192.0.2.99
> update add foo.example.com 300 AAAA 2001:db8::99
> send
> quit

To remove a record:

nsupdate -k /etc/bind/updater.key << EOF
server ns1.example.com
zone example.com
update delete foo.example.com A
send
EOF

To add and remove atomically (in one update message):

nsupdate -k /etc/bind/updater.key << EOF
server ns1.example.com
zone example.com
update delete foo.example.com A
update add foo.example.com 300 A 192.0.2.100
send
EOF

When to use dynamic update

  • Yes for DHCP-driven DNS, ACME challenges, ephemeral compute registration.
  • Maybe for application-driven DNS (a deployment tool adding records as part of a release). Worth keeping the human-edited zones separate from the dynamically-updated ones — sub-zone delegation is common ("dyn.example.com. is for automation, everything else is hand-edited").
  • No for one-off changes you'll forget about. Edit the zone file, bump the serial, reload — the audit trail is your VCS.

journal files

When dynamic updates are in use, BIND maintains a *.jnl file alongside the zone file. The journal records updates so they survive a restart. Don't delete .jnl files casually — if they get out of sync with the zone file, you may lose updates or fail to start.

To safely "freeze" a zone for hand-editing:

rndc freeze ZONE             # stop accepting updates, flush journal to file
# edit the zone file
rndc thaw ZONE               # resume updates

This is the right way to make a one-off manual edit on a dynamically-updated zone.


Networking utilities you'll reach for alongside DNS

Sometimes the answer to "DNS isn't working" is "DNS is fine, the network isn't". A small toolbox of networking utilities lives next to dig in your operator brain.

Reachability

ping -c 4 host                  # ICMP echo
ping6 host                      # IPv6 (or `ping -6`)
ping -c 4 1.1.1.1               # IP literal: bypass DNS entirely
fping -c 4 host1 host2 host3    # parallel
mtr host                        # interactive ping+traceroute (install separately)
mtr --report -c 50 host         # batch mode, useful in scripts

mtr is invaluable when you suspect a network path issue. It shows per-hop loss and latency, and you can read the pattern in the table: a single hop with high loss followed by clean hops is that hop's problem; rising loss from a hop onward is everything beyond that hop being affected.

Path tracing

traceroute host                 # UDP-based by default on Linux
traceroute -T host              # TCP traceroute (firewalls often pass TCP/80)
traceroute -I host              # ICMP traceroute
traceroute -p 443 -T host       # specific TCP port
tcptraceroute host 443          # purpose-built TCP traceroute

Different probe types reach different paths because routers and firewalls treat them differently. If UDP traceroute hits a black hole at hop 4, try TCP — it might get further.

Port reachability

nc -zv host 443                  # TCP port-knock
nc -uvz host 53                  # UDP port-knock (less reliable)
nc -lvp 9000                     # listen on TCP/9000 (test harness)
nmap -p 53,80,443 host           # multi-port scan
nmap -sU -p 53,123 host          # UDP scan

nc -zv host 443 is the fastest way to answer "can I reach this port from here?".

Listening sockets

ss -tnlp                         # listening TCP sockets, with PIDs
ss -tnp                          # established TCP, with PIDs
ss -unlp                         # listening UDP, with PIDs
ss -unp                          # active UDP, with PIDs
ss -s                            # summary: counts of sockets
ss -tnp '( dport = :443 )'       # filter by port
netstat -tnlp                    # older alternative; ss is preferred
lsof -iTCP -sTCP:LISTEN          # everything listening on TCP
lsof -i :53                      # whatever is on port 53

ss -tnlp | grep :53 answers "is named listening?".

Local interfaces and routing

ip a                             # all interfaces and addresses
ip -br a                         # one-line-per-interface summary
ip r                             # routing table
ip route get 1.1.1.1             # which interface and gateway would be used
ip neigh                         # ARP / IPv6 neighbour cache
ip -s link                       # per-interface stats
ifconfig                         # legacy (Linux); still everywhere on macOS/BSD
arp -an                          # legacy (Linux); ip neigh on modern systems

ip route get $IP is the right tool to answer "if I send a packet to this address, which interface does it leave on?".

TLS and HTTPS

curl -vk https://host/path
curl --resolve host:443:1.2.3.4 -v https://host/path
                                 # use this IP, but pretend the SNI is host
openssl s_client -connect host:443 -servername host </dev/null 2>&1 | head
                                 # the full TLS handshake
openssl s_client -connect host:443 -showcerts </dev/null 2>&1
                                 # all the certs in the chain
nmap --script ssl-cert -p 443 host

--resolve is invaluable for testing a new endpoint before you've cut DNS over to it: it lets curl connect to the new IP while sending the right SNI/Host header, so the load balancer routes correctly.

Packet capture

tcpdump -ni any port 53                          # DNS, all interfaces
tcpdump -ni eth0 host 1.1.1.1                    # to/from a specific peer
tcpdump -ni any 'port 53 or port 853'            # DNS + DoT
tcpdump -ni any -w dns.pcap port 53              # save for later
tshark -ni any port 53                           # decoded DNS
tshark -nr dns.pcap -Y 'dns.flags.rcode != 0'    # filter packets in a saved capture
wireshark dns.pcap                               # GUI, the same data

A capture of one minute of port-53 traffic answers most "why is DNS slow / failing" questions definitively.

Connectivity tests for specific protocols

# SMTP banner
nc -v mail.example.com 25
openssl s_client -starttls smtp -connect mail.example.com:25

# IMAP
openssl s_client -connect imap.example.com:993

# SSH host key fingerprint
ssh-keyscan host

Gotchas

A collection of foot-guns that catch experienced operators.

Trailing dots

Already covered, but worth reiterating: a missing trailing dot in a zone file appends $ORIGIN to your name, silently. Pre-flight every zone change with named-checkzone. The places this bites hardest:

  • The MNAME and RNAME of the SOA record.
  • The target of NS, MX, CNAME, PTR, and SRV records.
  • Anything you're copying from external documentation that uses the bare-name shortcut.

Forgotten serial bumps

After vim example.com.zone, rndc reload will silently succeed without re-publishing the change to secondaries if you didn't bump the SOA serial. Symptom: primary serves new data, secondaries serve old data. Fix: bump the serial to a higher value than the last one served and reload again.

Since the serial is a 32-bit unsigned integer, any value works; common conventions are YYYYMMDDnn (date plus two-digit revision) or Unix epoch. Both make it obvious which serial is newest.

Apex CNAMEs

Zone apex (the bare zone name) cannot be a CNAME. Period. If you need an apex pointing at, say, a CDN's hostname, you have three options:

  • Use an A/AAAA record at the apex pointing at the CDN's IPs directly. Risky if the CDN's IPs change.
  • Use a hosted DNS provider that offers an "ALIAS"/"ANAME"/ "flattened CNAME" record. Their server resolves the target internally and serves an A/AAAA answer.
  • Move the public-facing name to a non-apex name (e.g. www.example.com. instead of example.com.) and 301-redirect from the apex. This is the cleanest from a DNS-purist perspective.

Glue records out of sync with reality

If the parent zone has glue (A/AAAA records for your name servers) and you change the IPs, you must update both the glue and the records inside your zone. The parent's glue is what public resolvers actually use to find you; the records inside your zone are what your zone says.

"Lame" delegations

When the parent's NS records list a name server that doesn't know about the zone (the secondary was retired without updating delegation, or the new server hasn't been configured yet), that's a lame delegation. Public resolvers eventually try every name server in the delegation; some get unhelpful answers from the lame ones. The symptom is occasional slow resolution of your domain. Fix: keep delegation in sync with reality.

Round-robin isn't load balancing

Multiple A records for one name are returned in (typically rotated) order, but the client decides which to use. Most stub resolvers use the first one, fall back to subsequent ones only on connection failure. There's no health checking. For real load balancing or HA, use a load balancer or a CDN.

TTL planning takes time

You can't drop TTL from 86400 to 60 and expect the world to have the new TTL within an hour. Resolvers that already have your record cached will keep it for the old TTL. So if you're going to make a change that depends on a short TTL, drop the TTL at least one full TTL period in advance.

Negative cache TTL bites you

If a name doesn't exist, the resolver caches the NXDOMAIN for the SOA's negative TTL (the last field of the SOA). If you create a new name and it doesn't immediately resolve everywhere, that's because some resolvers are still in the negative-cache window. Wait it out, or flush, or pre-emptively keep the negative TTL low on zones where you'll be adding new names frequently.

DNS over HTTPS bypasses your resolver

A laptop browsing the web on your network may bypass your corporate resolver entirely, sending queries via DoH directly to dns.google or cloudflare-dns.com. Effects: corporate DNS-based filtering doesn't apply, split-horizon for internal names doesn't work in the browser, and you can't see what names are being queried. Mitigations: corporate-managed browsers can disable DoH, or you can serve a "Canary domain" (RFC 8484) to signal that DoH should be off, or you can implement DoH at your own resolver and require its use.

127.0.0.1 and ::1 aren't symmetric

If your zone defines localhost.example.com. to point at 127.0.0.1, define an AAAA pointing at ::1 too. Otherwise IPv6-preferring clients may try the v6 path, get NXDOMAIN, and behave oddly.

/etc/hosts short-circuits DNS

Don't forget that /etc/hosts (or C:\Windows\System32\drivers\etc\hosts) can override DNS for specific names. This is sometimes the cause of "weird local DNS opinions". Always check it when "this one machine sees a different answer than the others".

IDN and Punycode

Internationalised domain names (αβγ.example) are encoded for DNS as Punycode (xn--mxac0bs.example). Browsers do this automatically; your zone file should contain the Punycode form. If you publish a literal Unicode label, things will break.

Browsers cache DNS independently

Restarting the browser, or testing with curl, is often necessary to bypass the browser's DNS cache after fixing a record.

CNAMEs in places they shouldn't be

  • A CNAME on the same name as another record → illegal, will cause inconsistent answers.
  • A CNAME at the zone apex → illegal.
  • A CNAME as the target of an MX, NS, or PTR record → legal in the protocol but problematic in practice. Mail systems particularly may reject mail to MX targets that are CNAMEs.

Expired DNSSEC signatures

RRSIG records have an expiry. If your signing daemon crashes and nobody notices, your signatures will expire and your zone will SERVFAIL for validating resolvers. Monitor signature expiry as a proactive metric, not just as a reactive outage signal.

Time skew breaks TSIG

TSIG signatures include a timestamp. If primary and secondary clocks are off by more than the fudge factor (default 300s), zone transfers fail with BADTIME. Run NTP on both ends.

Zone transfer failures often look like configuration drift

If your secondary "isn't getting updates", check in this order: NOTIFY (is the primary actually telling the secondary?), allow-transfer ACL (does the primary let the secondary fetch?), TSIG (do the keys match?), firewall (is TCP/53 open between them?), reachability (dig @primary . SOA).

Old nslookup lies

The output format of nslookup differs across implementations. The fields that look like authoritative answers may actually be cached answers. The fields labelled "Server:" tell you which recursor was queried, not where the data came from. Use dig for anything you'll need to interpret accurately.


Version notes and ecosystem

BIND versions in the wild

  • BIND 9.18 LTS — current long-term support series. Stable, fully featured, what most modern Linux distributions ship.
  • BIND 9.16 — the previous LTS. End-of-life as of mid-2023; stragglers exist in long-running enterprise deployments.
  • BIND 9.11 and earlier — explicitly out of support. If you're on these, the upgrade path is the most important task on your backlog. Older versions have unpatched CVEs.

The configuration syntax has been stable across recent versions; the most visible changes:

  • master/slave zone types have been deprecated in favour of primary/secondary (both still work).
  • Manual DNSSEC management is being phased out in favour of dnssec-policy. Modern configurations should use the latter.
  • dnssec-must-be-secure and a few other DNSSEC operator knobs have been refined.
  • geoip ACLs use the MaxMind DB format (the older geoip-directory style was retired in 9.16).
  • serve-stale (RFC 8767) is on by default in many configurations — return cached answers past TTL when authoritatives are unreachable, to keep the network usable during outages.

Alternatives to BIND

  • Knot DNS (CZ.NIC) — fast authoritative server, used heavily in TLDs. Comes with kdig.
  • NSD (NLnet Labs) — minimalist authoritative server, used notably for the root zone's distributions.
  • Unbound (NLnet Labs) — recursive-only resolver, lightweight and security-focused. Many modern setups pair Unbound (recursor) with NSD (authoritative).
  • PowerDNS — separate authoritative and recursor packages, with database backends (so zone data lives in MySQL/Postgres rather than text files). Popular for hosted DNS.
  • CoreDNS — DNS server written in Go; common in Kubernetes ecosystems as the in-cluster DNS.
  • systemd-resolved — the default on most modern Linux desktops, a small recursive resolver glued into the systemd init system.
  • dnsmasq — small server doing DHCP + DNS, common on home routers and embedded devices. Easy configuration, limited features.
  • Microsoft DNS Server — bundled with Windows Server, AD-integrated. The default on most Windows-shop networks. Uses AD-replicated storage rather than zone files; can serve plain zones too.

The protocol is the same across all of them. Configuration syntax differs, monitoring and operational tools differ, but the zone data and the wire-protocol behaviour are interoperable.

Hosted DNS providers

  • Route 53 (AWS) — feature-rich, integrated with AWS, supports alias records pointing at AWS resources.
  • Cloud DNS (GCP) — similar story for Google Cloud.
  • Azure DNS — for Azure.
  • Cloudflare — popular, with extensive features for caching, DDoS protection, and integration with their other services. ANAME-style apex flattening included.
  • Constellix, NS1, DNSimple, Hurricane Electric, etc. — smaller providers, each with their own feature mix.

The benefit is operational: you don't run name servers, you don't worry about availability, you get a control panel and an API. The cost is the lock-in (your DNS data lives in their proprietary format, even if they accept zone-file imports/exports), and the cost of egress queries (some charge per query, some flat).

Modern best practices

  • Authoritative-only servers, no recursion. Recursion belongs in dedicated recursive resolvers, not on your authoritative servers.
  • Multiple authoritative servers in different networks (and ideally different providers). DNS-only outages take whole applications down.
  • DNSSEC for any zone that holds public data. The chain of trust eliminates a wide class of cache-poisoning attacks.
  • Query-name minimisation at recursors. Reduces information leakage to authoritative servers.
  • Monitoring: SOA serial agreement across NS, response-time graphs from external probes, signature expiry alerts, NXDOMAIN spikes, SERVFAIL spikes, transfer-success metrics.
  • Version control your zone files. Zone files live in git the same way infrastructure-as-code does. Pre-commit hooks run named-checkzone. CI pushes the validated files to the primary via SSH or your provider's API.
  • Separate zones for stable infrastructure and dynamic data. ACME challenges, DHCP-driven PTRs, ephemeral compute should live in their own subzones (dyn.example.com., _acme-challenge.example.com.) so the dynamically-updated data doesn't interfere with hand-edited records.
  • Secure transfers and updates with TSIG, not IP-based ACLs alone.
  • Plan for IPv6. AAAA records, IPv6-reachable name servers, reverse zones in ip6.arpa., dual-stack everything.

Useful RFCs

When you need the formal answer, the relevant RFCs are typically:

  • RFC 1034, 1035 — DNS concepts and protocol. The 1987 originals; still authoritative.
  • RFC 2181 — clarifications on the DNS spec.
  • RFC 2308 — negative caching, repurposing of SOA minimum.
  • RFC 2845, 8945 — TSIG.
  • RFC 4033, 4034, 4035 — DNSSEC core.
  • RFC 5155 — NSEC3.
  • RFC 6891 — EDNS0.
  • RFC 7344, 8078 — automated DS publication for child zones.
  • RFC 7858 — DNS over TLS.
  • RFC 8484 — DNS over HTTPS.
  • RFC 8767 — serve-stale.
  • RFC 9156 — query name minimisation.
  • RFC 9460 — SVCB and HTTPS records.

For day-to-day work, you almost never need to read an RFC. For debugging weird interop issues with another implementation, sometimes nothing else will do.


Recipe collection: programming against the resolver

Most ops work touches DNS through dig, host, or whatever the OS gives you. When you need DNS inside an application — config discovery, SRV-driven service location, custom health checks — you reach for a resolver library. This section is a tour of the major options and the gotchas that come up most.

C — the system resolver

The classic C path is the libc resolver. The interesting calls live in <resolv.h>:

  • res_init() — initialise the resolver state from /etc/resolv.conf.
  • res_query() / res_search() — issue a query, return the raw DNS response packet for you to parse.
  • getaddrinfo() — the modern, protocol-agnostic name → socket lookup. Almost always what you want for "give me an address to connect to."
  • getnameinfo() — the reverse: socket → name.
#include <netdb.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
    struct addrinfo hints = {0}, *res, *p;
    hints.ai_family = AF_UNSPEC;       // v4 or v6
    hints.ai_socktype = SOCK_STREAM;   // TCP

    int rc = getaddrinfo(argv[1], "https", &hints, &res);
    if (rc) { fprintf(stderr, "%s\n", gai_strerror(rc)); return 1; }

    for (p = res; p; p = p->ai_next) {
        char host[NI_MAXHOST];
        getnameinfo(p->ai_addr, p->ai_addrlen, host, sizeof host,
                    NULL, 0, NI_NUMERICHOST);
        printf("%s\n", host);
    }
    freeaddrinfo(res);
    return 0;
}

Gotchas worth memorising:

  • getaddrinfo is the right call for almost all application code; the older gethostbyname / gethostbyaddr are not thread-safe and only speak IPv4.
  • Always free the result with freeaddrinfo.
  • Iteration order of the returned list is policy-driven (RFC 6724); don't assume it.
  • res_query returns a raw DNS packet; if you want anything other than A/AAAA you are doing protocol-level work.

C — libldns and libunbound

When you actually need to parse arbitrary record types, validate DNSSEC, or do recursion yourself, the system resolver is too thin. Two well-maintained C libraries:

  • libldns (NLnet Labs) — high-level DNS protocol toolkit. Good at building, parsing, and signing zone data. The drill tool ships with it and is a useful debugging companion to dig.
  • libunbound — embeddable validating recursive resolver. If you want to do DNSSEC validation inside your application without trusting the upstream resolver, this is the standard answer.

Both ship on most distros (libldns-dev / libunbound-dev).

Perl, Python, Go, Rust — the modern picks

  • Perl: Net::DNS — the long-lived workhorse. Builds and parses any record type, talks UDP/TCP/TLS, supports TSIG, DNSSEC.
  • Python: dnspython (pip install dnspython). The de facto library; covers query, zone parsing, dynamic update, TSIG, DNSSEC.
  • Go: github.com/miekg/dns. A near-complete DNS protocol implementation; used by CoreDNS internally.
  • Rust: hickory-dns (formerly trust-dns). Async client + resolver + server crates.

The Python equivalent of the C example above:

import dns.resolver

answers = dns.resolver.resolve("example.com", "A")
for r in answers:
    print(r.address)

# Specific record types
for r in dns.resolver.resolve("example.com", "MX"):
    print(r.preference, r.exchange)

# Use a specific server
r = dns.resolver.Resolver(configure=False)
r.nameservers = ["1.1.1.1", "9.9.9.9"]
r.timeout = 2
r.lifetime = 5
answers = r.resolve("example.com", "AAAA")

Building queries by hand (when libraries get in the way)

When you need to test resolver behaviour or replay a pcap, you build the wire format directly. Concise idioms:

# dig has a +qr mode that prints the wire format
dig +qr example.com any | grep -E '^;.*HEADER|;.*flags:'

# Construct an arbitrary query with kdig (knot-dnsutils)
kdig +retry=0 +tries=1 +timeout=2 @1.1.1.1 example.com TYPE65

# Send a question over a non-standard port
dig @127.0.0.1 -p 5353 example.com

# Force TCP
dig +tcp example.com

# Force EDNS off (test legacy paths)
dig +noedns example.com

# DNSSEC: ask for the records and the chain
dig +dnssec +cd example.com    # +cd disables validation client-side
delv example.com               # libldns's validator-aware lookup

Recipe collection: less-common BIND configuration directives

A short reference for directives that show up rarely enough to forget their exact spelling but are critical when you need them.

Address-match-list and ACL composition

acl "internal" {
    127.0.0.1/32;
    10.0.0.0/8;
    192.168.0.0/16;
    fd00::/8;
};

acl "transfer-peers" { key "tsig-secondary"; };

options {
    allow-query        { internal; localhost; };
    allow-recursion    { internal; };
    allow-transfer     { transfer-peers; };
    allow-update       { none; };       # stop dynamic update by default
};

ACLs are evaluated top-to-bottom and the first match wins. ! negates an entry: allow-query { !blocked; any; }; permits everyone except the blocked ACL.

Sortlist and address sorting

sortlist reorders the answer-section addresses on a per-client basis to prefer "near" addresses. Largely superseded by client-side getaddrinfo policy, but still useful in older environments:

options {
    sortlist {
        { 192.168.1.0/24; { 192.168.1.0/24; 10.0.0.0/8; }; };
    };
};

Periodic intervals — when nameservers do work

options {
    cleaning-interval  60;     # cache cleaning, in minutes
    interface-interval 60;     # rescan interfaces
    statistics-interval 60;    # dump stats to log
    heartbeat-interval 60;     # dial-on-demand heartbeat (rare today)
    max-cache-ttl     86400;
    max-ncache-ttl    3600;
};

In modern environments leave the defaults alone unless you have a specific reason. The values above are illustrative, not recommended.

trusted-keys / managed-keys (DNSSEC)

The chain of DNSSEC trust starts with a key configured locally:

trusted-keys {
    "." 257 3 8 "AwEAA…";   # static trust anchor
};

# OR (recommended) — automatically tracked via RFC 5011
managed-keys {
    "." initial-key 257 3 8 "AwEAA…";
};

managed-keys is preferred because it follows root-zone key rollovers without manual intervention.

check-names — hostname validation

options {
    check-names master  fail;        # zone-load: refuse bad names
    check-names slave   warn;
    check-names response ignore;     # don't second-guess what we resolve
};

The defaults are usually fine; touch this only when an upstream is serving names that your master strictness rejects.

View clauses for split-horizon

view "internal" {
    match-clients { internal; };
    recursion yes;
    zone "corp.example.com" { type master; file "internal/corp.zone"; };
};

view "external" {
    match-clients { any; };
    recursion no;
    zone "corp.example.com" { type master; file "external/corp.zone"; };
};

Order matters: the first view whose match-clients matches the query source wins. Put narrow ACLs first, match-clients { any; }; last.

Logging channels

logging {
    channel "default_syslog" {
        syslog daemon;
        severity info;
    };
    channel "queries" {
        file "/var/log/named/queries.log" versions 5 size 100m;
        severity info;
        print-time yes;
    };
    category default  { default_syslog; };
    category queries  { queries; };
};

The versions N size M form does built-in log rotation. For high-traffic resolvers, log rotation that BIND owns is generally faster and more reliable than handing rotation to logrotate.

check_soa and other operational scripts

Several SOA-comparison helpers ship with BIND in contrib/:

  • check_soa <zone> — query each authoritative server for the zone and report SOA serial / mismatches.
  • nsdiff (third-party) — diff two zones in dynamic-update form, ready to feed to nsupdate.
  • named-checkconf -z — load-test every zone the config references.
  • named-checkzone <zone> <file> — validate a single zone file.

Cron-style health check:

#!/usr/bin/env bash
set -euo pipefail
zone=corp.example.com
expected_serial=$(dig +short SOA @master.ns "$zone" | awk '{print $3}')
for ns in slave1.ns slave2.ns slave3.ns; do
    actual=$(dig +short SOA @"$ns" "$zone" | awk '{print $3}')
    [[ "$actual" == "$expected_serial" ]] || \
        echo "DRIFT: $ns serial=$actual master=$expected_serial"
done

Miscellaneous record types worth knowing exist

TypeUse
AFSDBAFS database server location (legacy AFS / DCE)
RPResponsible person (mostly cosmetic)
LOCGeographic location (latitude/longitude)
NAPTRUsed by ENUM and SIP; pattern-substitution rules
CERTHolds a certificate inline in DNS
IPSECKEYPublic key material for IPsec opportunistic encryption
SSHFPSSH host-key fingerprint for verification at first connect
TLSADANE — bind a TLS cert to a DNS name
SVCB / HTTPSModern service binding; supersedes some SRV uses
URIRFC 7553 URI record

You will go your whole career without provisioning some of these. But recognising them in dig output is its own skill.

Avoiding common BIND pitfalls

  • Forgetting to bump the SOA serial after editing a zone. Slaves won't pull the new data. named-checkzone will not catch this. Use serial-update-method unixtime or a wrapper script that bumps automatically.
  • Trailing-dot mistakes in zone files. host.example.com with no trailing dot is parsed as host.example.com.example.com. — silently. Always use either fully-qualified-with-trailing-dot or bare-and-relative; never the in-between.
  • $ORIGIN reset by $INCLUDE. Set $ORIGIN after an $INCLUDE if the included file changed it.
  • Slave fails after master IP change. If also-notify and the slave's masters {} block both reference an old address, the zone goes stale; the symptom is a serial drift you can see with the check_soa recipe above.
  • DNSSEC chain breaks at parent. If you re-sign with a new KSK but forget to push the DS record to the parent, every validating resolver returns SERVFAIL until the parent catches up.
Saved skills