BIND9 Authoritative DNS Server: Zone Configuration Guide - 夜莺博客

BIND9 Authoritative DNS Server: Zone Configuration Guide

Hosting your own authoritative DNS with BIND9 means your domain answers come from servers you control instead of a registrar's panel - essential for glue records, split-horizon views and DNSSEC. The setup has three parts: global options that disable recursion (so your server cannot be abused as an open resolver), zone declarations in named.conf.local, and zone files holding the SOA, NS and address records. This guide walks through an authoritative-only BIND9 configuration on Ubuntu/Debian with the RHEL path noted, including validation with named-checkconf and named-checkzone.

Authoritative vs Recursive: Know Which Server You Are Building

A recursive resolver answers questions for any name on the internet; an authoritative nameserver answers for specific zones you own and refuses everything else. Mixing the two on a public host is the classic open-resolver mistake that turns your server into a DNS amplification weapon. The configuration below is deliberately authoritative-only: recursion no; means the server will return REFUSED or an authoritative answer, never a cached lookup for someone else's domain.

Two record types do the structural work. The SOA declares the zone's identity, its primary nameserver and the timers secondaries use to refresh. The NS records name the servers responsible for the zone. If your zone is a subdomain delegated from a parent, the parent needs both NS records and glue A/AAAA records for any nameserver inside the zone itself, otherwise the delegation is circular and unresolvable.

Step 1: Install BIND9

sudo apt install bind9 bind9utils dnsutils       # Debian/Ubuntu
sudo dnf install bind bind-utils                 # RHEL/Rocky

Confirm what you actually got, because BIND 9.18 and later ship the older configuration layout only for compatibility:

named -v
systemctl status named 2>/dev/null || systemctl status bind9
ls -l /etc/bind/
ls -l /var/cache/bind/

On Debian the interesting files are /etc/bind/named.conf.options (global options), /etc/bind/named.conf.local (your zone declarations), and /etc/bind/named.conf.default-zones (root hints and localhost zones, which you leave alone). On RHEL everything lives in the single /etc/named.conf.

Step 2: Harden the Global Options

In /etc/bind/named.conf.options (RHEL: /etc/named.conf), turn recursion off so the server only answers for zones it is authoritative for:

options {
    directory "/var/cache/bind";
    listen-on { any; };
    listen-on-v6 { none; };
    recursion no;                // prevents open-resolver abuse
    allow-query { any; };
    allow-transfer { none; };    // restrict zone transfers
    dnssec-validation auto;
    version "not available";
};

A few additions are worth making while you are here. allow-query should ideally be narrowed to the networks that legitimately ask you questions rather than any — a hidden-master setup, for example, only needs to answer secondaries and internal resolvers:

acl trusted-nets { 203.0.113.0/24; 10.20.0.0/16; localhost; };

options {
    directory "/var/cache/bind";
    listen-on port 53 { any; };
    recursion no;
    allow-query { any; };
    allow-transfer { trusted-nets; };
    allow-notify { 10.20.0.2; };
    allow-update { none; };
    minimal-responses yes;
    notify yes;
    rate-limit {
        responses-per-second 10;
        window 5;
    };
};

minimal-responses yes stops BIND padding answers with redundant authority and additional sections, which cuts response size and reduces amplification surface. The rate-limit block caps how many identical responses a single client can elicit per second — cheap insurance if your name is ever forged as a source address in a reflection attack. allow-update { none; } is the default and should stay that way unless you deliberately enable dynamic updates with TSIG keys, discussed below.

Step 3: Declare Forward and Reverse Zones

Append to /etc/bind/named.conf.local:

zone "example.com" {
    type master;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { 10.20.0.2; };   // secondary nameserver
    notify yes;
};

zone "0.20.10.in-addr.arpa" {
    type master;
    file "/etc/bind/zones/db.10.20.0";
};

The matching secondary configuration on the other host uses type slave; (or type secondary; in BIND 9.16+) with the same file path and a masters { 203.0.113.53; }; list. Note the reverse zone naming rule: it is the network portion of the prefix written backwards, with the host bits dropped and in-addr.arpa appended. A /24 of 10.20.0.0 becomes 0.20.10.in-addr.arpa. For IPv6 the equivalent suffix is ip6.arpa and the zone is written in nibble-reversed hexadecimal.

If you need split-horizon answers — the same name resolving differently for internal and external clients — wrap the zone declarations in view blocks and give each view its own set of files. The two views must be mutually exclusive and every client must match exactly one:

view "internal" {
    match-clients { 10.20.0.0/16; localhost; };
    recursion no;
    zone "example.com" { type master; file "/etc/bind/zones/internal/db.example.com"; };
};

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

Step 4: Write the Zone Files

; /etc/bind/zones/db.example.com
$TTL 604800
@   IN  SOA ns1.example.com. admin.example.com. (
        2026050101 ; Serial (YYYYMMDDNN)
        604800     ; Refresh
        86400      ; Retry
        2419200    ; Expire
        604800 )   ; Negative cache TTL
    IN  NS  ns1.example.com.
    IN  NS  ns2.example.com.
ns1 IN  A   203.0.113.53
ns2 IN  A   198.51.100.53
@   IN  A   203.0.113.80
www IN  CNAME example.com.
mail IN  A   203.0.113.25
@   IN  MX  10 mail.example.com.

Remember the trailing dots on fully qualified names, and bump the serial (YYYYMMDDNN format) on every change so secondaries and caches pick it up.

The reverse zone mirrors the pattern with PTR records, which must use fully qualified names with trailing dots:

; /etc/bind/zones/db.10.20.0
$TTL 604800
@   IN  SOA ns1.example.com. admin.example.com. (
        2026050101 604800 86400 2419200 604800 )
    IN  NS  ns1.example.com.
    IN  NS  ns2.example.com.
1   IN  PTR gateway.example.com.
25  IN  PTR mail.example.com.
80  IN  PTR www.example.com.

Beyond A and CNAME, the records you will add most often are worth having to hand:

@       IN  AAAA    2001:db8:100::80
@       IN  TXT     "v=spf1 mx -all"
_dmarc  IN  TXT     "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
@       IN  CAA     0 issue "letsencrypt.org"
_sip._tcp       IN SRV 10 20 5060 sip.example.com.
_acme-challenge IN TXT "token-value"
*.dev           IN A   203.0.113.81
api             IN CNAME web01.example.com.

Three rules prevent most zone-file pain. A CNAME may not coexist with any other record for the same name — so never point a CNAME at @ if @ also holds MX or TXT records; use A records instead. Wildcards only match names that do not otherwise exist, so an explicit dev record silently overrides *.dev. And $TTL at the top of the file applies to every record that does not state its own TTL, which is why the SOA's final field (negative caching TTL) is often set lower than the positive TTL.

Step 5: Validate and Restart

sudo named-checkconf                    # empty output = good
sudo named-checkzone example.com /etc/bind/zones/db.example.com
sudo named-checkzone 0.20.10.in-addr.arpa /etc/bind/zones/db.10.20.0
sudo systemctl restart bind9
dig @127.0.0.1 example.com A +short

Never restart without running named-checkzone first. A single missing dot or an unbalanced parenthesis in the SOA will make BIND refuse to load the zone, and depending on the failure mode it may serve SERVFAIL for the whole zone while the process itself stays up — the worst possible combination, because monitoring that only checks "is named running" reports green.

Step 6: Query, Verify and Reload

Validate the answers from the server's own perspective and then from a client's:

# Authoritative answer, no recursion attempted
dig @203.0.113.53 example.com SOA +norecurse
# Serial and timers as the secondaries will see them
dig @203.0.113.53 example.com SOA +short
# NS set actually published
dig @203.0.113.53 example.com NS +short
# Reverse lookup
dig @203.0.113.53 -x 10.20.0.80 +short
# Recursion must be refused for foreign names
dig @203.0.113.53 google.com A +norecurse
# Authority section confirms this server owns the zone
dig @203.0.113.53 www.example.com A +noall +authority +answer

The recursion test is the important one. A query for a name you do not host should come back with status REFUSED or an empty answer flagged non-authoritative — never with a resolved address. If it resolves, your server is an open resolver and you should fix recursion no; before anything else.

To apply changes without dropping the process, use rndc:

sudo rndc status
sudo rndc reload example.com
sudo rndc reload
sudo rndc reconfig
sudo rndc flush

rndc reload <zone> rereads one zone, rndc reconfig re-reads named.conf and only loads new zones, and rndc flush clears the cache. Reloading beats restarting: a restart drops in-flight queries and, on a secondary, may force a full zone transfer.

DNSSEC Signing Basics

Authoritative BIND can sign zones inline, generating and rotating keys automatically while it serves:

dnssec-keygen -a ECDSAP256SHA256 -f KSK example.com
dnssec-keygen -a ECDSAP256SHA256 example.com
cat K*.key >> /etc/bind/zones/db.example.com
dnssec-signzone -o example.com -k K*.key db.example.com
rndc signing -list example.com

Signing produces a .signed file plus RRSIG and NSEC/NSEC3 records; you then publish a DS record at your registrar so the parent can chain trust down to your keys. Enable dnssec-validation auto; only on validating resolvers — an authoritative-only server merely serves the signatures. Signing is easy to get half-right, so test with an external validator such as dig +dnssec against a public validating resolver and confirm the AD flag appears before announcing the deployment.

Dynamic Updates, TSIG and Secure Transfers

When another system must write records — a DHCP server registering clients, or an automation tool adding service records — enable dynamic updates only for a specific zone and authenticate with a shared secret. TSIG keys go in named.conf, never in the zone file:

key "dhcp-update-key" {
    algorithm hmac-sha256;
    secret "base64isharedsecretgoeshere==";
};

zone "0.20.10.in-addr.arpa" {
    type master;
    file "/etc/bind/zones/db.10.20.0";
    allow-update { key "dhcp-update-key"; };
};

Secondaries should likewise authenticate their transfers: replace the bare IP list in allow-transfer with key "transfer-key"; and configure the same key in masters on the secondary. An unauthenticated zone transfer hands a complete copy of your namespace to anyone who asks.

Logging and Troubleshooting

BIND's logging is configured per-category with severity and destination:

logging {
    channel default_log {
        file "/var/log/named/named.log" versions 5 size 20m;
        severity info;
        print-time yes;
        print-category yes;
    };
    category default { default_log; };
    category queries { default_log; };
    category xfer-out { default_log; };
    category notify { default_log; };
};
rndc querylog

Turn query logging on only while diagnosing — on a busy server it writes a line per query and will fill a disk. When something does not resolve, work outward from the RCODE:

  • REFUSED — the query arrived for a zone this server is not authoritative for, or it hit allow-query. Expected for external names on an authoritative-only server.
  • SERVFAIL — the zone failed to load, is DNSSEC-broken, or a secondary cannot reach its master. Check named-checkzone output and the load log lines.
  • NXDOMAIN when the name should exist — a typo, a missing trailing dot making BIND append example.com twice, or a wildcard being shadowed by an explicit record.
  • Secondaries not updating — serial was not incremented, notify is blocked on TCP/53 outbound, or allow-transfer does not include the secondary. Compare serials across servers with dig @ns2 +short SOA.

Also keep the server's clock accurate. A drift of minutes is enough to break TSIG-signed transfers with a "tsig verify failure", trip DNSSEC signature validity windows, and confuse expiry accounting on secondaries. Monitor NTP like you monitor the service.

Operating Notes and Maintenance

Plan TTLs deliberately. Long positive TTLs (hours) reduce query load but make changes slow to propagate; short ones let you migrate quickly but multiply traffic. The SOA refresh/retry/expire values form the secondary's safety net — if a secondary cannot reach the master for longer than expire, it stops answering for the zone entirely, which is why expire is measured in weeks while retry is measured in hours.

Finally, treat the zone files as code. Keep them in version control, apply changes through a review, run named-checkzone in CI, and deploy with rndc reload from a script that can roll back. A DNS change pushed by hand at 02:00 with a typo is a service outage with a very public failure mode.

Related Guides on This Site

Debug resolution problems with dig and nslookup troubleshooting, understand what a broken resolver looks like from the client side with systemd-resolved and resolvectl, keep the server's clock honest with chrony NTP, and secure inbound queries with nftables. If the same network also hands out addresses, the companion guide is ISC DHCP Server on Linux.

原文链接:https://oneuptime.com/blog/post/2026-03-20-configure-bind-authoritative-dns/view