executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
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":[]}}
dns-networkingOperator'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
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
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.
A few sentences you can hold in your head and lean on for everything else:
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).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./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.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).mail.example.com. into
mail.example.com.example.com..The rest of this card is the long form of those seven sentences.
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.
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:
$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.resolv.conf or
the platform's equivalent), which appends each search-list entry
in turn until something resolves.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.
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.
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.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 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.
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.
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.
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.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).a.root-servers.net. through
m.root-servers.net.), each of which is in fact many physically
distributed instances behind anycast.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).
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.
shop.example.co.uk.?". Sends it to 1.1.1.1 on port 53.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).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.?".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.1.1.1.1 caches the uk. NS records and picks one of the
uk. servers. Sends the same query.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.co.uk. server refers to the example.co.uk.
name servers.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.1.1.1.1 caches the answer for the TTL and forwards it back to
your stub.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.
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:
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.
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.
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.
Two newer transports encrypt DNS traffic between stub and recursor:
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).
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.
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.
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.
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.
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:
CNAME record may not have any other records. So
you can't put a CNAME and an MX on the same name.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.CNAME for an MX target or an NS target. Both must
point to names with concrete A/AAAA records, not aliases.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.
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.
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:
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.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.YYYYMMDDnn (date plus a
two-digit counter for multiple changes per day) and a Unix epoch
timestamp. Either works. Just bump it.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.)
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.
Holds opaque text strings. Originally meant for human-readable metadata, now overloaded as the place every protocol publishes its policy:
v=spf1 ip4:192.0.2.0/24 include:_spf.example.net -allselector._domainkey.example.com TXT "v=DKIM1; k=rsa; p=MIIBIjANB..."_dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"google-site-verification=...,
MS=..., apple-domain-verification=..., etc._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).
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.
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.
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.alpn, ipv4hint, ipv6hint)
before contacting the server. Replacing the historical use of
CNAME at the apex for HTTP services in the long run.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:
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.
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.
; 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:
@ and "ditto" rules below.; and run to the end of the line.$TTL at the top and rely on it.IN for everything you'll write in 2025; you can
omit it.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 fields (TTL, SOA refresh/retry/expire/min) accept either a plain number of seconds or a number with a unit:
s — secondsm — minutesh — hoursd — daysw — weeksSo 1h, 60m, and 3600 are equivalent. You can combine in
some implementations (1h30m = 5400 seconds).
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..www (with no dots at all) is also relative,
but typically what you want — it becomes www.example.com.
under the same origin.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).
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.
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:
Run them before every reload, ideally as a pre-commit hook on the zone-files repository.
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.
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.Three syntaxes work, all equivalent:
/* C-style block comment */
// C++-style line comment
# shell-style line comment
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.named.confoptions {
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.notify and also-notify statements inside the zone block
are how the primary tells secondaries to refresh after a change.named.confoptions {
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.Many statements take an address match list. A match list is one or more of:
192.0.2.10192.0.2.0/24!192.0.2.66localnets, localhost, any, none,
or any user-defined namekey 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; };
};
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.
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.
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.
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; };
};
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.
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.
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 [@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.
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
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:
QUERY, occasionally UPDATE),
status (NOERROR, NXDOMAIN, SERVFAIL, REFUSED,
FORMERR, NOTIMP, …), and a transaction ID.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).+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
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.
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 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 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.- form on Windows / older Unix sometimes triggers an
interactive mode if you forget the trailing query string.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 ("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 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.
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.
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.
These are the recipes you reach for repeatedly. Most are "recipe-shaped" — same five steps every time, with the values filled in.
named-checkzone ZONE FILE to validate.rndc reload ZONE (or rndc reload for everything).dig @LOCAL_NS NEW_NAME TYPE to confirm the record is served.notify propagates, query each authoritative server
listed in NS records: dig @nsX NEW_NAME TYPE +short.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:
The "TTL game". Plan it in advance:
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.
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).allow-transfer
and also-notify.rndc reconfig on both ends.dig @new-secondary ZONE SOA +norecurse — you should
see the same SOA serial as the primary.Reverse the above process. Remove the NS record, wait for
delegation TTL, remove from allow-transfer / also-notify,
shut down the secondary.
rndc reconfig both ends.rndc retransfer).rndc reconfig again.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.).
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.
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?".
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.
This avoids any window where a queried name server is offline.
A handful of operational changes have a long blast radius if you get them wrong. Treat them with extra care:
SERVFAIL your zone
worldwide. Dry-run with a test zone first; let dnssec-policy
manage the timing for you in modern BIND.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 (RFC 8945, originally 2845) attaches an HMAC to a DNS message, keyed on a shared secret known to both ends.
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.
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.
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.
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.
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:
tsig verify failure in named's logs is one of these.
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:
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).
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.
After your zone is signed, the parent (your TLD or your delegation provider) needs your DS record to complete the chain of trust. Steps:
After signing, retrieve your DS record from your zone:
dig @localhost example.com DS
# or
dnssec-dsfromkey -2 KSK_KEY_FILE
Submit that DS record to your registrar/parent. Most modern registrar control panels have a DNSSEC tab where you paste in the DS values.
Wait for the parent to publish the DS in their zone (delays vary; some are minutes, some are hours).
Validate end-to-end with a DNSSEC-aware resolver:
delv example.com
You should see ; fully validated in the output.
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.
A misconfigured or expired DNSSEC zone returns SERVFAIL to validating resolvers. Symptoms:
dig +dnssec shows RRSIG records but the SERVFAIL still
comes back when validation runs.delv shows the validation step that failed.Common causes:
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 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).
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>
version "no"; in options.recursion no;.allow-recursion { trusted; };.rate-limit { responses-per-second 10; }; —
blunt-instrument protection against DDoS amplification.bind-chroot
variant; modern systems with systemd sandboxing achieve similar
isolation through unit-file directives.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:
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.
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.
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.
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.
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:
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.
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:
/etc/resolv.conf./etc/resolver/ files are routing some names
to a corporate resolver.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.
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.
Several common patterns:
dig +tcp.+nsid to
identify which instance is answering.dig +bufsize=512
vs +bufsize=4096.If you're worried someone is injecting forged answers into your recursor:
dnssec-validation auto or
similar in your platform).allow-recursion), or your recursor becomes the cache for
the world and an open amplifier for DDoS.# 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.
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:
_acme-challenge TXT record to prove control
of the domain.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.
nsupdate from the command linensupdate -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
dyn.example.com. is for automation,
everything else is hand-edited").journal filesWhen 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.
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.
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.
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.
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?".
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?".
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?".
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.
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.
# 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
A collection of foot-guns that catch experienced operators.
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:
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.
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:
A/AAAA record at the apex pointing at the CDN's IPs
directly. Risky if the CDN's IPs change.A/AAAA answer.www.example.com. instead of example.com.) and 301-redirect
from the apex. This is the cleanest from a DNS-purist
perspective.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.
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.
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.
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.
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.
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 symmetricIf 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 DNSDon'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".
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.
Restarting the browser, or testing with curl, is often necessary
to bypass the browser's DNS cache after fixing a record.
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.
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.
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).
nslookup liesThe 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.
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).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.kdig.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.
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).
named-checkzone. CI pushes the validated files to the primary
via SSH or your provider's API.dyn.example.com.,
_acme-challenge.example.com.) so the dynamically-updated
data doesn't interfere with hand-edited records.ip6.arpa., dual-stack everything.When you need the formal answer, the relevant RFCs are typically:
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.
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.
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.freeaddrinfo.res_query returns a raw DNS packet; if you want anything other
than A/AAAA you are doing protocol-level work.libldns and libunboundWhen 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).
Net::DNS — the long-lived workhorse. Builds and parses
any record type, talks UDP/TCP/TLS, supports TSIG, DNSSEC.dnspython (pip install dnspython). The de facto
library; covers query, zone parsing, dynamic update, TSIG, DNSSEC.github.com/miekg/dns. A near-complete DNS protocol
implementation; used by CoreDNS internally.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")
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
A short reference for directives that show up rarely enough to forget their exact spelling but are critical when you need them.
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 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; }; };
};
};
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 validationoptions {
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 "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 {
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 scriptsSeveral 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
| Type | Use |
|---|---|
AFSDB | AFS database server location (legacy AFS / DCE) |
RP | Responsible person (mostly cosmetic) |
LOC | Geographic location (latitude/longitude) |
NAPTR | Used by ENUM and SIP; pattern-substitution rules |
CERT | Holds a certificate inline in DNS |
IPSECKEY | Public key material for IPsec opportunistic encryption |
SSHFP | SSH host-key fingerprint for verification at first connect |
TLSA | DANE — bind a TLS cert to a DNS name |
SVCB / HTTPS | Modern service binding; supersedes some SRV uses |
URI | RFC 7553 URI record |
You will go your whole career without provisioning some of these. But
recognising them in dig output is its own skill.
named-checkzone will not catch this.
Use serial-update-method unixtime or a wrapper script that
bumps automatically.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.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.