PublicDNS.info Live-tested public DNS
Retested every 72 hours.

How to Install and Configure BIND DNS Server on Linux

Install and configure BIND as an authoritative or recursive DNS server on Linux. Covers zone files, DNSSEC, ACLs, logging, and performance tuning.

DNS Resolution Chain

Client (Stub Resolver) 1 Recursive Resolver (BIND) 2 Root DNS Server ( . ) 3 TLD Server (.com, .net) 4 Authoritative Server (Zone Owner) 5 Recursive Resolver (BIND — caches) 6 Client (Gets Answer) Steps: 1 Query sent 2 Ask root servers 3 Referred to TLD 4 Referred to auth. 5 Answer returned to resolver 6 Cached answer returned to client Query Response

Introduction to BIND DNS

BIND (Berkeley Internet Name Domain) is the most widely used DNS server software on the internet. It was originally written at UC Berkeley in the 1980s and is now maintained by the Internet Systems Consortium (ISC). The majority of the root DNS servers ran BIND for decades, and it remains the reference implementation for DNS standards.

BIND can operate as an authoritative DNS server (hosting zone data for your domains), a recursive resolver (answering queries on behalf of clients by walking the DNS tree), or both simultaneously. In practice, running both roles on one instance is discouraged for security and performance reasons, but BIND supports it.

This guide covers installing BIND 9 on Linux, configuring it for both authoritative and recursive use cases, setting up DNSSEC, and tuning it for production. Every command and configuration block is copy-paste ready.

If you just need to point a Linux machine at a different DNS resolver rather than running your own, see our Linux DNS settings guide instead.

DNS architecture overview

DNS is a hierarchical, distributed database. When a client (your browser, your application) needs to resolve www.example.com, the query follows a chain:

  1. The stub resolver on the client sends the query to its configured recursive resolver.
  2. The recursive resolver checks its cache. If it has no answer, it starts at the DNS root.
  3. A root server (one of 13 clusters, labeled a.root-servers.net through m.root-servers.net) responds with a referral to the TLD servers for .com.
  4. The TLD server for .com responds with a referral to the authoritative nameservers for example.com.
  5. The authoritative server for example.com returns the actual A or AAAA record.
  6. The recursive resolver caches the answer (respecting TTL values) and returns it to the client.

Every layer caches aggressively. A record with a TTL of 3600 seconds will be served from cache for up to an hour before the resolver queries upstream again. This reduces load across the entire system.

BIND fits into this picture at two points: as the authoritative server (step 5) or as the recursive resolver (step 2). Which role you configure depends on your use case.

Authoritative vs recursive DNS

These are two fundamentally different jobs, even though BIND can do both.

Authoritative server

An authoritative server holds zone files containing the DNS records for domains you control. When a recursive resolver asks "what is the A record for www.example.com?", your authoritative server answers from its local data. It does not query other servers. It is the final authority for that zone.

Authoritative servers are typically public-facing. Your domain registrar points NS records at them, and anyone on the internet can query them for records in your zones.

Recursive resolver

A recursive resolver does not hold zone data (aside from caching). When a client sends it a query, it walks the DNS tree from root to authoritative server, assembles the answer, caches it, and returns it to the client. It acts as a proxy between your users and the DNS hierarchy.

Recursive resolvers are typically private-facing, serving your internal network or your customers. Public recursive resolvers like 1.1.1.1 and 8.8.8.8 exist, but running an open resolver on your own is a bad idea because it gets abused for DNS amplification attacks.

Why separate them?

An authoritative server should only answer for its own zones. A recursive resolver should only serve trusted clients. Combining them means the server accepts recursive queries from the internet, which opens you up to cache poisoning and amplification attacks. Run them on separate instances or at least separate BIND views.

BIND software overview

The BIND 9 distribution includes several components:

ComponentPurpose
namedThe DNS server daemon. This is the core process that listens on port 53 and answers queries.
rndcRemote Name Daemon Control. A command-line tool for managing a running named instance: reload zones, flush cache, dump stats, toggle query logging.
digDNS lookup tool. The standard utility for querying DNS servers and inspecting responses.
delvDNS Enhanced Lookup and Validation. Like dig, but with built-in DNSSEC validation output.
named-checkconfValidates the syntax of named.conf without starting the server.
named-checkzoneValidates zone file syntax and checks for common errors.
dnssec-keygenGenerates DNSSEC key pairs. Less relevant with dnssec-policy in BIND 9.18+, but still useful for TSIG keys.
nsupdateSends dynamic DNS update requests to a server (RFC 2136).

On Debian-based systems, named and the check tools come from the bind9 package, while dig and delv come from bind9-dnsutils. On RHEL-based systems, named is in the bind package and the tools are in bind-utils.

Supported operating systems

BIND 9 runs on most Unix-like operating systems. ISC officially tests against:

  • Debian 11, 12
  • Ubuntu 22.04 LTS, 24.04 LTS
  • RHEL 8, 9 (and derivatives: AlmaLinux, Rocky Linux, Oracle Linux)
  • Fedora (current releases)
  • FreeBSD 13, 14
  • Alpine Linux

BIND also compiles on macOS, OpenBSD, and other platforms, though these are not primary targets for ISC's QA process.

Minimum requirements

BIND itself is lightweight. A minimal recursive resolver runs fine on a single CPU core with 512 MB RAM. An authoritative server with a handful of zones uses even less. For production resolvers handling thousands of queries per second, plan for at least 2 GB RAM (cache grows with traffic) and fast storage for logging.

Installing BIND on Debian/Ubuntu

On Debian 12 (Bookworm) and Ubuntu 24.04 (Noble), the packages are in the default repositories.

sudo apt update
sudo apt install -y bind9 bind9-utils bind9-doc bind9-dnsutils

This installs:

  • bind9 — the named daemon, named-checkconf, named-checkzone
  • bind9-utils — rndc and related management tools
  • bind9-doc — man pages and documentation
  • bind9-dnsutils — dig, delv, nslookup, nsupdate

After installation, BIND starts automatically and listens on localhost. Verify it is running:

sudo systemctl status bind9

You should see active (running) in the output. Enable it to start at boot (usually already enabled by the package):

sudo systemctl enable bind9

Check the installed version:

named -v

Debian 12 ships BIND 9.18.x. Ubuntu 24.04 ships 9.18.x as well. Both are Extended Support versions.

Installing BIND on RHEL/AlmaLinux

On RHEL 9, AlmaLinux 9, and Rocky Linux 9, install from the base repositories:

sudo dnf install -y bind bind-utils

This installs:

  • bind — the named daemon, named-checkconf, named-checkzone, rndc
  • bind-utils — dig, delv, nslookup, nsupdate

On RHEL-based systems, the service name is named (not bind9):

sudo systemctl start named
sudo systemctl enable named
sudo systemctl status named

RHEL 9 ships BIND 9.16.x by default. For a newer version (9.18.x), check if the bind module stream offers it:

sudo dnf module list bind

If a 9.18 stream is available, you can switch to it. Otherwise, 9.16 is fine for most deployments and receives security patches from Red Hat.

Directory structure explanation

BIND uses different directory layouts on Debian and RHEL. Knowing where files live saves time when troubleshooting.

Debian/Ubuntu layout

PathPurpose
/etc/bind/Main configuration directory
/etc/bind/named.confTop-level config — includes other files
/etc/bind/named.conf.optionsGlobal options block
/etc/bind/named.conf.localYour custom zone definitions go here
/etc/bind/named.conf.default-zonesDefault zones: localhost, reverse localhost, root hints
/etc/bind/db.*Default zone files (db.local, db.127, db.0, db.255, db.empty)
/var/cache/bind/Working directory for BIND — dynamic zone files, journals, keys go here
/var/log/named/Log directory (you must create it; see Logging section)

RHEL/AlmaLinux layout

PathPurpose
/etc/named.confSingle configuration file (everything in one file by default)
/etc/named/Optional directory for includes and keys
/var/named/Zone files, root hints, and named working data
/var/named/data/Statistics and dump files
/var/named/dynamic/Dynamic zone files and journals
/var/log/named/Log directory (create it manually)

The key difference: Debian splits configuration across multiple files using include directives, while RHEL puts everything in a single /etc/named.conf. Both approaches work fine. The Debian layout is easier to manage when you have many zones.

Understanding named.conf

The named.conf file is the main configuration for BIND. It uses a C-like block syntax with nested statements terminated by semicolons.

Here are the main block types:

options block

Global settings that apply to the entire server.

options {
    directory "/var/cache/bind";         // working directory
    listen-on { 127.0.0.1; 10.0.0.1; }; // IPv4 addresses to listen on
    listen-on-v6 { ::1; };               // IPv6 addresses to listen on
    allow-query { localhost; 10.0.0.0/8; };
    recursion yes;                        // enable recursive queries
    dnssec-validation auto;              // validate DNSSEC signatures
    forwarders { 1.1.1.1; 8.8.8.8; };   // upstream resolvers (optional)
};

zone block

Defines a DNS zone (authoritative or delegated).

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { 192.168.1.2; };
};

acl block

Named address lists that you can reference elsewhere.

acl "trusted" {
    10.0.0.0/8;
    192.168.0.0/16;
    localhost;
};

logging block

Controls where BIND sends log messages and at what severity.

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

Semicolons matter. Every statement, every block closing brace, and every list item must end with a semicolon. Missing one causes named to refuse to start with a cryptic parse error.

Setting up a recursive resolver

A recursive resolver answers DNS queries for your network by querying the DNS hierarchy on behalf of clients. This is what you want if you are building a local caching resolver for your office, data centre, or home lab.

Debian configuration

Edit /etc/bind/named.conf.options:

options {
    directory "/var/cache/bind";

    // Listen on your server's LAN IP and localhost
    listen-on { 127.0.0.1; 10.0.0.1; };
    listen-on-v6 { ::1; };

    // Only allow your network to query
    allow-query { localhost; 10.0.0.0/24; };
    allow-recursion { localhost; 10.0.0.0/24; };

    // Enable recursion
    recursion yes;

    // DNSSEC validation
    dnssec-validation auto;

    // Optional: forward to upstream resolvers instead of querying root servers directly
    // forwarders { 1.1.1.1; 8.8.8.8; };
    // forward only;
};

RHEL configuration

Edit /etc/named.conf and modify the options block:

options {
    directory "/var/named";
    dump-file "/var/named/data/cache_dump.db";
    statistics-file "/var/named/data/named_stats.txt";

    listen-on port 53 { 127.0.0.1; 10.0.0.1; };
    listen-on-v6 port 53 { ::1; };

    allow-query { localhost; 10.0.0.0/24; };
    allow-recursion { localhost; 10.0.0.0/24; };

    recursion yes;
    dnssec-validation auto;
};

Forwarding mode vs full recursion

Without forwarders, BIND performs full recursion: it queries root servers, then TLD servers, then authoritative servers. This gives you complete control over resolution but generates more outbound DNS traffic.

With forwarders and forward only, BIND sends all queries to the specified upstream resolvers and caches the results locally. This is simpler and puts less load on the DNS hierarchy. Use forward first instead if you want BIND to attempt its own recursion when forwarders are unavailable.

Test the resolver

Restart BIND and query it:

sudo systemctl restart bind9   # Debian
sudo systemctl restart named   # RHEL

dig @127.0.0.1 example.com

You should get an A record back with a status: NOERROR response. The first query will be slow (cache miss). Repeat it and the response time drops to under 1 ms (served from cache).

Creating authoritative zones

An authoritative zone means BIND holds the definitive records for a domain. You are the source of truth.

Primary (master) zone

On Debian, add your zone to /etc/bind/named.conf.local. On RHEL, add it to /etc/named.conf.

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { 192.168.1.2; };   // secondary server IP
    notify yes;                         // send NOTIFY to secondaries on change
};

Create the zone file directory:

sudo mkdir -p /etc/bind/zones

Secondary (slave) zone

On your secondary server, define the zone as type secondary and point it at the primary:

zone "example.com" {
    type secondary;
    file "/var/cache/bind/db.example.com";
    primaries { 192.168.1.1; };        // primary server IP
};

The secondary will automatically pull the zone via AXFR when it starts, and again whenever the primary sends a NOTIFY or the SOA refresh interval expires.

Disable recursion for authoritative-only servers

If this server is purely authoritative, disable recursion in the options block:

options {
    recursion no;
    allow-query { any; };
};

This tells BIND to only answer queries for zones it hosts and refuse everything else. This is the correct posture for a public-facing authoritative server.

Forward DNS zones

A forward zone maps domain names to IP addresses. Here is a complete zone file for example.com:

$TTL 86400
$ORIGIN example.com.
@   IN  SOA   ns1.example.com. admin.example.com. (
                2026030501  ; serial (YYYYMMDDNN)
                3600        ; refresh (1 hour)
                900         ; retry (15 minutes)
                604800      ; expire (1 week)
                86400       ; minimum / negative cache TTL (1 day)
            )

; Name servers
    IN  NS    ns1.example.com.
    IN  NS    ns2.example.com.

; Mail servers
    IN  MX    10 mail.example.com.
    IN  MX    20 mail2.example.com.

; A records (IPv4)
ns1         IN  A     192.168.1.1
ns2         IN  A     192.168.1.2
mail        IN  A     192.168.1.3
mail2       IN  A     192.168.1.4
www         IN  A     192.168.1.10
@           IN  A     192.168.1.10

; AAAA records (IPv6)
ns1         IN  AAAA  2001:db8::1
ns2         IN  AAAA  2001:db8::2
www         IN  AAAA  2001:db8::10

; CNAME aliases
ftp         IN  CNAME www.example.com.
webmail     IN  CNAME mail.example.com.

; TXT records
@           IN  TXT   "v=spf1 mx a ~all"
_dmarc      IN  TXT   "v=DMARC1; p=quarantine; rua=mailto:admin@example.com"

SOA record breakdown

  • ns1.example.com. — the primary nameserver for the zone
  • admin.example.com. — the admin email address (the first dot replaces the @ sign, so this means admin@example.com)
  • 2026030501 — serial number, formatted as YYYYMMDDNN
  • 3600 — refresh: how often secondaries check the primary for updates
  • 900 — retry: how long to wait before retrying a failed refresh
  • 604800 — expire: how long a secondary keeps serving the zone without reaching the primary
  • 86400 — minimum: the negative cache TTL (how long resolvers cache NXDOMAIN responses)

Reverse DNS zones

Reverse DNS maps IP addresses back to hostnames using PTR records. This is required for mail servers (many receiving servers reject mail from IPs without valid reverse DNS) and useful for logging and diagnostics.

Reverse zones live in the in-addr.arpa namespace for IPv4. The octets are reversed. For the 192.168.1.0/24 network, the zone name is 1.168.192.in-addr.arpa.

Zone definition in named.conf

zone "1.168.192.in-addr.arpa" {
    type primary;
    file "/etc/bind/zones/db.192.168.1";
    allow-transfer { 192.168.1.2; };
};

Zone file

$TTL 86400
$ORIGIN 1.168.192.in-addr.arpa.
@   IN  SOA   ns1.example.com. admin.example.com. (
                2026030501  ; serial
                3600        ; refresh
                900         ; retry
                604800      ; expire
                86400       ; minimum
            )

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

1   IN  PTR   ns1.example.com.
2   IN  PTR   ns2.example.com.
3   IN  PTR   mail.example.com.
10  IN  PTR   www.example.com.

The number on the left is the last octet of the IP address. Record 1 maps 192.168.1.1 to ns1.example.com.

For IPv6 reverse DNS, the zone lives under ip6.arpa and uses nibble format. A /48 prefix like 2001:db8:abcd::/48 becomes zone d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa. The syntax is the same but the names are longer.

Zone file syntax

Zone file syntax has several rules that trip people up. These are the ones that cause the most errors.

Trailing dots

A fully qualified domain name (FQDN) must end with a dot. ns1.example.com. is absolute. Without the trailing dot, BIND appends the zone origin, so ns1.example.com inside zone example.com becomes ns1.example.com.example.com. — which is almost certainly not what you want.

This is the single most common mistake in zone files. If your records do not resolve, check for missing trailing dots first.

$TTL directive

$TTL 86400 sets the default TTL for all records that follow. Without it, BIND uses the SOA minimum field, and you will get a warning.

$ORIGIN directive

$ORIGIN example.com. sets the base domain. The @ symbol refers to the current origin. Relative names (without a trailing dot) get the origin appended.

Parentheses for multi-line records

The SOA record spans multiple lines using parentheses. BIND treats everything between ( and ) as a single logical line. You can use this for any long record.

Serial number format

The serial number must increase every time you edit the zone. The conventional format is YYYYMMDDNN, where NN is a revision counter that resets daily. For example, the third edit on March 5, 2026 would be 2026030503.

If you accidentally set the serial to a very large number (like a Unix timestamp), you can fix it by setting it to 2^32 minus 1 (4294967295), waiting for secondaries to sync, then setting it back to a sane value. This works because serial number arithmetic wraps around.

Comments

Use a semicolon to start a comment. Everything after ; on that line is ignored by BIND.

Record classes

IN is the internet class and is used for virtually all DNS records. You will always use IN. Other classes like CH (Chaosnet) and HS (Hesiod) exist but are rarely seen.

Testing zone files

Always validate before reloading. A syntax error in a zone file will cause BIND to refuse to load that zone, and your domain goes offline.

named-checkconf

Validates the overall named.conf structure:

sudo named-checkconf

No output means no errors. If there is a problem, it prints the file name, line number, and a description of the error.

On Debian, this checks /etc/bind/named.conf and all included files. On RHEL, it checks /etc/named.conf.

named-checkzone

Validates a specific zone file:

named-checkzone example.com /etc/bind/zones/db.example.com

The first argument is the zone name, the second is the file path. On success:

zone example.com/IN: loaded serial 2026030501
OK

Common errors this catches:

  • Missing trailing dot on FQDNs
  • SOA record not at the top of the zone
  • Missing NS records
  • Duplicate records
  • Invalid record syntax

Reload after validation

Once both checks pass, reload the zone without restarting BIND:

sudo rndc reload example.com

Or reload all zones:

sudo rndc reload

Check the BIND log for any load errors after reloading:

sudo journalctl -u bind9 --since "5 minutes ago"   # Debian
sudo journalctl -u named --since "5 minutes ago"    # RHEL

DNSSEC configuration

DNSSEC adds cryptographic signatures to your zone data so resolvers can verify that responses have not been tampered with. BIND 9.18+ handles key generation, signing, and key rollover automatically through the dnssec-policy statement.

Enabling DNSSEC validation (recursive resolver)

If BIND is acting as a recursive resolver, enable DNSSEC validation in the options block:

options {
    dnssec-validation auto;
};

The auto setting uses the built-in trust anchor for the root zone (managed by ISC). This is the default in most BIND packages and is all you need for validation.

Signing your own zones (authoritative server)

To sign an authoritative zone, add dnssec-policy to the zone block. The built-in default policy handles everything:

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    dnssec-policy default;
    inline-signing yes;
};

With inline-signing yes, BIND keeps your original zone file untouched and maintains a separate signed version internally. This means you edit the unsigned zone file as usual, and BIND re-signs automatically after each change.

Restart or reload BIND:

sudo rndc reload example.com

BIND generates KSK (Key Signing Key) and ZSK (Zone Signing Key) pairs automatically and stores them in the working directory (/var/cache/bind/ on Debian, /var/named/ on RHEL).

Publishing the DS record

After signing, you need to publish a DS record at your parent zone (your domain registrar). Extract the DS record:

dig @localhost example.com DNSKEY | dnssec-dsfromkey -f - example.com

Copy the DS record output and add it at your registrar's DNS settings page. Without this step, DNSSEC validation will fail for external resolvers.

Custom DNSSEC policy

If the default policy does not suit your needs, define a custom one:

dnssec-policy "custom" {
    keys {
        ksk key-directory lifetime unlimited algorithm ecdsap256sha256;
        zsk key-directory lifetime P90D algorithm ecdsap256sha256;
    };
    max-zone-ttl P1D;
    dnskey-ttl PT1H;
    zone-propagation-delay PT5M;
    parent-ds-ttl P1D;
    parent-propagation-delay PT1H;
};

This uses ECDSA P-256 keys (smaller and faster than RSA), rotates the ZSK every 90 days, and keeps the KSK indefinitely. Adjust the timings based on your zone's TTL values and how quickly your registrar propagates DS record changes.

Zone transfers

Zone transfers replicate zone data from a primary server to one or more secondaries. There are two types:

  • AXFR — Full zone transfer. The entire zone is sent.
  • IXFR — Incremental zone transfer. Only the changes since the last known serial are sent.

BIND uses IXFR by default when possible and falls back to AXFR when needed.

Primary server configuration

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { 192.168.1.2; 192.168.1.3; };
    also-notify { 192.168.1.2; 192.168.1.3; };
    notify yes;
};

allow-transfer restricts which IPs can pull the zone. Never use any here — it exposes your entire zone to anyone. also-notify sends NOTIFY messages to IPs in addition to those listed in NS records, which is useful when secondaries are not publicly listed as NS.

Secondary server configuration

zone "example.com" {
    type secondary;
    file "/var/cache/bind/db.example.com";
    primaries { 192.168.1.1; };
};

Securing transfers with TSIG

TSIG (Transaction Signature) authenticates zone transfers using a shared secret. Generate a TSIG key:

tsig-keygen -a hmac-sha256 xfer-key

This outputs a key block. Add it to both the primary and secondary named.conf:

key "xfer-key" {
    algorithm hmac-sha256;
    secret "base64-encoded-secret-here";
};

Then reference the key in your zone and server definitions:

// On primary
zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { key "xfer-key"; };
    notify yes;
};

// On secondary
server 192.168.1.1 {
    keys { "xfer-key"; };
};

zone "example.com" {
    type secondary;
    file "/var/cache/bind/db.example.com";
    primaries { 192.168.1.1; };
};

With TSIG in place, the secondary authenticates to the primary before the transfer begins. This prevents unauthorized zone pulls even if someone gains access to a permitted IP.

Access control lists

ACLs define named groups of IP addresses and networks that you reference throughout your configuration. They make your named.conf cleaner and easier to maintain.

Defining ACLs

Place ACL definitions at the top of named.conf, before the options block:

acl "trusted-nets" {
    10.0.0.0/8;
    192.168.0.0/16;
    172.16.0.0/12;
    localhost;
};

acl "secondaries" {
    192.168.1.2;
    192.168.1.3;
};

acl "bogon-nets" {
    0.0.0.0/8;
    127.0.0.0/8;
    169.254.0.0/16;
    224.0.0.0/4;
    240.0.0.0/4;
};

Using ACLs

options {
    allow-query { "trusted-nets"; };
    allow-recursion { "trusted-nets"; };
    blackhole { "bogon-nets"; };
};

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { "secondaries"; };
};

Built-in ACL keywords

KeywordMatches
anyAll hosts
noneNo hosts
localhostAll addresses on the loopback interface
localnetsAll networks directly connected to the server

ACL ordering

ACLs use first-match logic. Once an address matches an entry (positive or negated), BIND stops checking. This means order matters. If you want to deny a specific host within an allowed network:

acl "trusted-except-printer" {
    !10.0.0.50;       // deny this IP first
    10.0.0.0/24;      // then allow the rest of the subnet
};

Putting the negation after the broader match would have no effect because 10.0.0.50 already matches 10.0.0.0/24.

Firewall configuration

BIND needs ports open for DNS traffic and management. The exact commands depend on your firewall.

firewalld (RHEL/AlmaLinux/Fedora)

sudo firewall-cmd --permanent --add-service=dns
sudo firewall-cmd --reload

The dns service opens both TCP and UDP port 53. If you also need the rndc management port (953/tcp) accessible from a remote management host:

sudo firewall-cmd --permanent --add-port=953/tcp
sudo firewall-cmd --reload

ufw (Ubuntu/Debian)

sudo ufw allow 53/tcp
sudo ufw allow 53/udp

Or use the Bind9 application profile if available:

sudo ufw allow Bind9

iptables (manual)

sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 953 -s 127.0.0.1 -j ACCEPT

Note that rndc port 953 should only be accessible from localhost (or your management network). Never expose it to the internet.

SELinux (RHEL)

On RHEL-based systems with SELinux enabled, BIND runs in its own security context. If you store zone files outside the default /var/named/ directory, you need to set the correct SELinux labels:

sudo semanage fcontext -a -t named_zone_t "/etc/bind/zones(/.*)?"
sudo restorecon -Rv /etc/bind/zones

AppArmor (Ubuntu/Debian)

On Ubuntu, BIND runs under an AppArmor profile (/etc/apparmor.d/usr.sbin.named). If you add custom directories for zones or logs, you may need to add them to the profile:

# Add to /etc/apparmor.d/usr.sbin.named:
/etc/bind/zones/** r,
/var/log/named/** rw,

Then reload the profile:

sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.named

Logging configuration

BIND logging uses two concepts: channels (where logs go) and categories (what gets logged). You combine them to route specific types of messages to specific destinations.

Create the log directory

# Debian
sudo mkdir -p /var/log/named
sudo chown bind:bind /var/log/named

# RHEL
sudo mkdir -p /var/log/named
sudo chown named:named /var/log/named

Production logging configuration

Add this to your named.conf (or on Debian, create /etc/bind/named.conf.log and include it):

logging {
    // General operational messages
    channel default_log {
        file "/var/log/named/default.log" versions 5 size 10m;
        severity info;
        print-time yes;
        print-severity yes;
        print-category yes;
    };

    // Query log (can be very large; enable temporarily for debugging)
    channel query_log {
        file "/var/log/named/query.log" versions 3 size 50m;
        severity info;
        print-time yes;
    };

    // Zone transfer log
    channel xfer_log {
        file "/var/log/named/xfer.log" versions 3 size 10m;
        severity info;
        print-time yes;
        print-severity yes;
    };

    // Security events
    channel security_log {
        file "/var/log/named/security.log" versions 3 size 10m;
        severity dynamic;
        print-time yes;
        print-severity yes;
    };

    // Route categories to channels
    category default { default_log; };
    category queries { query_log; };
    category xfer-in { xfer_log; };
    category xfer-out { xfer_log; };
    category security { security_log; };
    category notify { xfer_log; };
};

Key categories

CategoryWhat it logs
defaultCatch-all for uncategorized messages
queriesEvery DNS query received (very verbose)
xfer-inInbound zone transfers
xfer-outOutbound zone transfers
securityApproved and denied requests
notifyNOTIFY protocol messages
dnssecDNSSEC validation and signing events
updateDynamic DNS update requests
lame-serversLame server detections (misconfigured delegations)

Keep query logging disabled in production unless you are debugging. On a busy resolver, it generates gigabytes of output per day. Toggle it on temporarily with rndc querylog on and off with rndc querylog off.

Performance tuning

Out of the box, BIND works fine for small deployments. At scale, a few settings make a significant difference.

Cache size

options {
    max-cache-size 512m;    // limit cache to 512 MB (default is 90% of RAM)
};

On a dedicated resolver, you can let BIND use most of available RAM. On a shared host, cap it so it does not starve other services. BIND 9.20+ uses a SIEVE-based eviction algorithm that performs well under pressure.

Minimal responses

options {
    minimal-responses yes;
};

This omits additional (authority and additional) sections from responses when the client did not ask for them. It reduces packet size and slightly improves performance. Recommended for recursive resolvers.

TCP and recursive client limits

options {
    recursive-clients 10000;   // max simultaneous recursive lookups (default: 1000)
    tcp-clients 1000;          // max simultaneous TCP connections (default: 150)
    tcp-listen-queue 10;       // TCP listen backlog
};

Increase recursive-clients if your resolver handles heavy traffic and you see "no more recursive clients" warnings in the log. The default of 1000 is low for a resolver serving thousands of users.

Rate limiting (for authoritative servers)

options {
    rate-limit {
        responses-per-second 10;
        window 5;
    };
};

Response rate limiting (RRL) protects your authoritative server from being used in DNS amplification attacks. It throttles excessive identical responses to the same client prefix. Do not enable RRL on recursive resolvers; it would throttle your own users.

File descriptor limits

BIND needs one file descriptor per open socket and log file. Under heavy load, you may hit the OS default limit. Increase it in the systemd unit file:

sudo systemctl edit bind9    # Debian
sudo systemctl edit named    # RHEL

Add:

[Service]
LimitNOFILE=65536

Then restart BIND.

Monitoring DNS queries

BIND provides several ways to inspect what it is doing at runtime.

rndc stats

sudo rndc stats

This writes statistics to the dump file (default: /var/named/data/named_stats.txt on RHEL, /var/cache/bind/named.stats on Debian). The file contains counters for queries received, responses sent, cache hits, NXDOMAIN responses, SERVFAIL responses, and more.

Toggle query logging

sudo rndc querylog on
# ... watch queries in the log ...
sudo rndc querylog off

Each query is logged with the client IP, query name, type, and class. Useful for debugging specific resolution failures.

Statistics channel (HTTP)

BIND can expose statistics via an HTTP endpoint for monitoring tools. Add to named.conf:

statistics-channels {
    inet 127.0.0.1 port 8053 allow { 127.0.0.1; };
};

After reloading, access http://127.0.0.1:8053/ for an XML or JSON report of server statistics. You can feed this into Prometheus (using bind_exporter), Grafana, Zabbix, or similar monitoring systems.

Cache dump

sudo rndc dumpdb -cache

Dumps the entire cache to a file. Useful for investigating what is cached and what the TTL values are. The output file path is set by the dump-file option in named.conf.

Useful dig checks against your server

You can also test your BIND server using our DNS dig tool from the browser, which queries public DNS servers and shows the response with full details.

Testing with dig and nslookup

dig is the standard tool for querying DNS servers. nslookup is simpler but shows less detail.

Basic dig queries

# Query your local BIND server for an A record
dig @127.0.0.1 example.com A

# Query for MX records
dig @127.0.0.1 example.com MX

# Query for any record type
dig @127.0.0.1 example.com ANY

# Short output (just the answer)
dig @127.0.0.1 example.com +short

# Show the full trace from root to authoritative
dig @127.0.0.1 example.com +trace

DNSSEC verification

# Check if DNSSEC signatures are present
dig @127.0.0.1 example.com A +dnssec

# Use delv for detailed DNSSEC validation
delv @127.0.0.1 example.com A

delv shows whether the answer is "fully validated", "unsigned", or has validation failures. It is more useful than dig +dnssec for debugging DNSSEC problems because it performs its own validation and reports the results clearly.

Zone transfer test

# Attempt an AXFR from your server (should work from permitted IPs only)
dig @192.168.1.1 example.com AXFR

If the transfer is allowed, you will see the full zone contents. If denied, you get Transfer failed.

nslookup

# Basic lookup
nslookup example.com 127.0.0.1

# Specify record type
nslookup -type=MX example.com 127.0.0.1

# Reverse lookup
nslookup 192.168.1.10 127.0.0.1

nslookup is available everywhere and works fine for quick checks. For anything more detailed, use dig.

Response time check

dig @127.0.0.1 example.com | grep "Query time"

The first query (cache miss) will show a higher time (e.g., 50-200 ms). Repeat the same query and it should drop to 0-1 ms (cache hit).

Troubleshooting BIND configuration

When BIND misbehaves, the answers are almost always in the logs. Start there.

BIND refuses to start

Check the journal for errors:

sudo journalctl -u bind9 -e --no-pager   # Debian
sudo journalctl -u named -e --no-pager    # RHEL

Run the configuration validator:

sudo named-checkconf

The most common causes: missing semicolons, mismatched braces, referencing a zone file that does not exist, or a permissions problem on the working directory.

Queries refused

If dig @your-server example.com returns status: REFUSED:

  • Check allow-query — is the client IP included?
  • Check allow-recursion — for recursive queries, the client must be in this list
  • Check that recursion yes is set if you want recursive lookups
  • Check that the query is arriving on an interface BIND is listening on (listen-on)

SERVFAIL responses

A SERVFAIL means BIND tried to resolve the query but failed. Common causes:

  • DNSSEC validation failure — the upstream zone has broken DNSSEC. Test with dig +cd (checking disabled) to confirm. If the query succeeds with +cd, DNSSEC is the issue.
  • Upstream server unreachable — check that BIND can reach the internet on port 53 (outbound)
  • Broken delegation — the zone's NS records point to servers that do not respond

Zone not loading

If rndc reload succeeds but the zone is not serving:

named-checkzone example.com /etc/bind/zones/db.example.com

Check file permissions — the BIND user (bind on Debian, named on RHEL) must be able to read the zone file. Also verify that the zone is defined in named.conf and that the file path matches.

Slow resolution

If queries take several seconds:

  • Check if IPv6 is causing timeouts. If your server does not have IPv6 connectivity, BIND still tries AAAA queries and waits for them to time out. Disable IPv6 transport: listen-on-v6 { none; }; and add -4 to named startup options.
  • Check your forwarders — if an upstream resolver is down, queries queue behind the timeout. Remove dead forwarders.
  • Check if you are hitting recursive-clients limits — increase the value if you see "no more recursive clients" in the log.

Permission denied errors

On RHEL with SELinux, BIND cannot write to directories that lack the named_zone_t label. On Ubuntu, AppArmor restricts BIND to specific paths. Check with:

# SELinux
sudo ausearch -m avc -ts recent | grep named

# AppArmor
sudo dmesg | grep apparmor | grep named

Backup and maintenance

Backing up BIND

Back up everything BIND needs to recover from a bare reinstall:

# Debian
sudo tar czf /root/bind-backup-$(date +%Y%m%d).tar.gz \
    /etc/bind/ \
    /var/cache/bind/ \
    /var/log/named/

# RHEL
sudo tar czf /root/bind-backup-$(date +%Y%m%d).tar.gz \
    /etc/named.conf \
    /etc/named/ \
    /var/named/ \
    /var/log/named/

Run this from cron daily or before any configuration change.

Freezing zones before editing

If a zone allows dynamic updates, freeze it before making manual edits:

sudo rndc freeze example.com
# Edit the zone file
sudo rndc thaw example.com

Freezing stops BIND from writing to the zone file while you edit it. Thawing reloads the zone and resumes dynamic updates. If you skip the freeze, your edits may conflict with the journal file and cause data loss.

Log rotation

BIND's built-in versions and size options in the logging channel handle rotation. However, if you prefer system-wide log management with logrotate, create /etc/logrotate.d/named:

/var/log/named/*.log {
    weekly
    missingok
    rotate 8
    compress
    delaycompress
    notifempty
    postrotate
        /usr/sbin/rndc reopen 2>/dev/null || true
    endscript
}

The rndc reopen command tells BIND to close and reopen its log files after rotation.

Updating BIND

Keep BIND updated for security patches:

# Debian/Ubuntu
sudo apt update && sudo apt upgrade bind9

# RHEL/AlmaLinux
sudo dnf update bind

After updating, check the release notes for any configuration syntax changes, then restart the service. ISC publishes security advisories at https://www.isc.org/bind/.

Serial number management

When you edit a zone file, always increment the serial number. Forgetting this is the number one cause of "I updated the zone but nothing changed." Secondaries compare the serial in the SOA record to decide whether to transfer. If the serial has not increased, they ignore the change.

Health check script

A simple cron script to alert you if BIND stops responding:

#!/bin/bash
if ! dig @127.0.0.1 example.com +short +time=2 +tries=1 > /dev/null 2>&1; then
    echo "BIND DNS is not responding on $(hostname)" | \
        mail -s "DNS ALERT" admin@example.com
fi

Run it from cron every 5 minutes. Replace example.com with a domain your server is authoritative for, or any domain if it is a recursive resolver.

Frequently asked questions

What is the difference between BIND as an authoritative server and a recursive resolver?

An authoritative server holds zone data for specific domains and answers queries about those domains from its own records. A recursive resolver does not hold zone data. It takes queries from clients and walks the DNS tree, starting from root servers, to find the answer. BIND can run as either or both, but mixing both roles on one instance is discouraged in production.

Which BIND version should I use in production?

Use the current Extended Support Version (ESV). As of early 2026, that is BIND 9.20.x. The 9.18.x branch also receives security patches. Avoid the development branch (9.21.x) in production.

Do I need to manually generate DNSSEC keys with dnssec-keygen?

Not if you use BIND 9.18 or later. The dnssec-policy statement handles key generation, signing, and rollover automatically. Manual key management with dnssec-keygen is only needed on older versions or in special cases.

How do I check if my BIND configuration is valid before restarting?

Run named-checkconf to validate named.conf syntax and named-checkzone to validate individual zone files. Fix any errors they report before running rndc reload or restarting the service.

Can BIND handle both IPv4 and IPv6 DNS queries?

Yes. BIND listens on both IPv4 and IPv6 by default. Use the listen-on and listen-on-v6 directives to control which addresses and ports it binds to.

Why does BIND refuse queries from remote clients?

By default, BIND only allows queries from localhost. You need to add your client networks to the allow-query and allow-recursion options. Check your ACLs, verify the client IP is covered, and check that no firewall is blocking port 53.

How do I update the serial number in a zone file?

The serial number must increase every time you edit the zone. The standard convention is YYYYMMDDNN, where NN is a daily revision counter (01, 02, etc.). Forgetting to increment the serial is the most common cause of zone transfer failures.

Is BIND suitable for high-traffic public DNS?

Yes, but it requires tuning. Set appropriate values for max-cache-size, recursive-clients, tcp-clients, and use minimal-responses. For very high query rates, consider running multiple instances behind a load balancer.

Finding reliable DNS servers

Whether you are configuring BIND as a forwarding resolver or just looking for reliable upstream servers to use, you can browse our DNS directory to find live-tested public DNS servers filtered by country, speed, and reliability.