$linuxjunkies
>

How to Set Up a DNS Server with BIND

Install and configure BIND 9 as a caching resolver and authoritative nameserver, with forward/reverse zones, common resource records, and firewall rules.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • Root or sudo access on the target server
  • A static IP address assigned to the server
  • Basic familiarity with DNS concepts (zones, records, TTL)
  • Firewall management tools installed (firewalld, ufw, or nftables)

BIND (Berkeley Internet Name Domain) remains the most widely deployed DNS server software on the internet. This guide walks through installing and configuring BIND 9 as both a caching resolver and an authoritative nameserver for a primary zone. You will configure forward and reverse zones, add common resource records, lock down recursion, and verify everything resolves correctly. Commands are shown for Debian/Ubuntu, Fedora/RHEL, and Arch Linux where they differ.

Install BIND 9

Debian / Ubuntu

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

Fedora / RHEL 9 / Rocky Linux 9

sudo dnf install bind bind-utils

Arch Linux

sudo pacman -S bind

The primary configuration directory is /etc/bind/ on Debian-family systems and /etc/named/ (with the main file at /etc/named.conf) on RHEL-family. Arch uses /etc/named.conf directly. Zone data typically lives under /var/lib/bind/ (Debian) or /var/named/ (RHEL/Arch).

Configure the Caching Resolver

Edit the main named options file. On Debian/Ubuntu this is /etc/bind/named.conf.options; on RHEL/Arch it is the options block in /etc/named.conf.

sudo nano /etc/bind/named.conf.options   # Debian/Ubuntu
# sudo nano /etc/named.conf             # RHEL/Arch

Replace or merge the options block with the following. Substitute your actual trusted networks for the ACL.

acl "trusted" {
    127.0.0.1;
    ::1;
    192.168.1.0/24;   # your LAN subnet
};

options {
    directory "/var/cache/bind";   # /var/named on RHEL/Arch

    recursion yes;
    allow-recursion { trusted; };
    allow-query     { trusted; };
    allow-transfer  { none; };     # lock down zone transfers

    forwarders {
        1.1.1.1;
        8.8.8.8;
    };
    forward only;                  # remove this line to fall back to root hints

    dnssec-validation auto;

    listen-on      { any; };
    listen-on-v6   { any; };
};

Security note: setting recursion yes without the ACL guard creates an open resolver that can be abused for amplification attacks. The ACL above ensures only your trusted hosts can recurse.

Define Authoritative Zones

On Debian/Ubuntu, zone declarations belong in /etc/bind/named.conf.local. On RHEL/Arch, add them directly to /etc/named.conf. The example domain is example.lan with the subnet 192.168.1.0/24.

sudo nano /etc/bind/named.conf.local   # Debian/Ubuntu
# Forward zone
zone "example.lan" {
    type master;
    file "/var/lib/bind/db.example.lan";   # /var/named/db.example.lan on RHEL/Arch
    allow-update { none; };                 # no dynamic updates for now
};

# Reverse zone for 192.168.1.0/24
zone "1.168.192.in-addr.arpa" {
    type master;
    file "/var/lib/bind/db.192.168.1";     # /var/named/db.192.168.1 on RHEL/Arch
    allow-update { none; };
};

Create Zone Files

Forward Zone

sudo nano /var/lib/bind/db.example.lan
$TTL 86400
@   IN  SOA  ns1.example.lan. hostmaster.example.lan. (
                2024060101  ; Serial  (YYYYMMDDnn)
                3600        ; Refresh
                900         ; Retry
                604800      ; Expire
                300 )       ; Negative TTL

; Name servers
@       IN  NS   ns1.example.lan.
@       IN  NS   ns2.example.lan.

; A records
ns1     IN  A    192.168.1.10
ns2     IN  A    192.168.1.11
@       IN  A    192.168.1.10
www     IN  A    192.168.1.20
mail    IN  A    192.168.1.30

; MX record
@       IN  MX   10 mail.example.lan.

; CNAME example
ftp     IN  CNAME  www.example.lan.

; TXT record (SPF example)
@       IN  TXT  "v=spf1 mx -all"

Increment the Serial every time you edit the zone file — secondary nameservers use it to detect changes. The format YYYYMMDDnn is conventional but any monotonically increasing 32-bit integer works.

Reverse Zone

sudo nano /var/lib/bind/db.192.168.1
$TTL 86400
@   IN  SOA  ns1.example.lan. hostmaster.example.lan. (
                2024060101
                3600
                900
                604800
                300 )

@       IN  NS   ns1.example.lan.
@       IN  NS   ns2.example.lan.

; PTR records  (last octet only)
10      IN  PTR  ns1.example.lan.
11      IN  PTR  ns2.example.lan.
20      IN  PTR  www.example.lan.
30      IN  PTR  mail.example.lan.

Set File Permissions

BIND runs as the bind user on Debian/Ubuntu and named on RHEL/Arch. Zone files must be readable by that user.

# Debian/Ubuntu
sudo chown bind:bind /var/lib/bind/db.example.lan /var/lib/bind/db.192.168.1
sudo chmod 640 /var/lib/bind/db.example.lan /var/lib/bind/db.192.168.1
# RHEL / Arch
sudo chown named:named /var/named/db.example.lan /var/named/db.192.168.1
sudo chmod 640 /var/named/db.example.lan /var/named/db.192.168.1

Validate Configuration and Enable the Service

Check syntax before starting

sudo named-checkconf
sudo named-checkzone example.lan /var/lib/bind/db.example.lan
sudo named-checkzone 1.168.192.in-addr.arpa /var/lib/bind/db.192.168.1

A clean run produces no output from named-checkconf and a short OK line from each named-checkzone call. Fix any reported errors before proceeding.

Enable and start named

sudo systemctl enable --now named   # RHEL/Arch name
# OR
sudo systemctl enable --now bind9   # Debian/Ubuntu name

Open the Firewall

DNS uses TCP and UDP port 53.

firewalld (Fedora / RHEL / Rocky)

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

ufw (Debian / Ubuntu)

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

nftables (Arch or manual setup)

sudo nft add rule inet filter input udp dport 53 accept
sudo nft add rule inet filter input tcp dport 53 accept

Verify Resolution

# Query the local server directly
dig @127.0.0.1 www.example.lan A
dig @127.0.0.1 example.lan MX
dig @127.0.0.1 -x 192.168.1.20          # reverse lookup

# Test external recursion (should work from trusted subnet)
dig @192.168.1.10 google.com A

# Confirm an external IP cannot recurse
dig @ google.com A      # should return REFUSED

Expected output from dig @127.0.0.1 www.example.lan A includes a ANSWER SECTION with www.example.lan. 86400 IN A 192.168.1.20 and a status of NOERROR. Actual TTL and flags will vary slightly.

Troubleshooting

  • Service fails to start: Run sudo journalctl -u named -xe (or bind9) for detailed errors. Most startup failures are syntax errors caught by named-checkconf.
  • SERVFAIL on external names: Check that your forwarders are reachable and that outbound UDP/TCP 53 is not blocked by your upstream firewall. Temporarily remove forward only; to fall back to root hints.
  • Zone not loading: Confirm the file path in named.conf matches the actual file, the serial is a valid 32-bit integer, and the file is readable by the bind/named user.
  • PTR queries return NXDOMAIN: Verify the reverse zone name matches your subnet exactly (e.g., 1.168.192.in-addr.arpa for 192.168.1.0/24) and that PTR records use only the last octet.
  • SELinux denials on RHEL/Rocky: If zone files are stored outside /var/named/, restore the correct context: sudo restorecon -Rv /var/named/. Alternatively move files to the expected path.
tested on:Ubuntu 24.04Debian 12Rocky 9Arch rolling

Frequently asked questions

What is the difference between an authoritative server and a caching resolver?
An authoritative server holds the master copy of zone data and answers definitively for its own domains. A caching resolver queries other servers on behalf of clients, stores the results temporarily, and is not authoritative for any zone.
How do I reload zone changes without restarting BIND?
After incrementing the serial and editing the zone file, run 'sudo rndc reload example.lan' to reload just that zone, or 'sudo rndc reload' for all zones. No service interruption occurs.
Why does named-checkzone pass but the zone still fails to load at runtime?
The most common reasons are a file permission problem (the named/bind user cannot read the file) or, on SELinux-enforcing systems, a missing file context. Check journalctl output and run restorecon on the zone directory.
Can I run BIND as both a caching resolver and an authoritative server on the same instance?
Yes, and this guide does exactly that. BIND handles both roles simultaneously: it answers authoritatively for declared zones and recurses to external servers for everything else.
How should I set up a secondary (slave) nameserver?
On the primary, add the secondary's IP to the allow-transfer option for each zone. On the secondary, declare the same zones with 'type slave;' and a 'masters { <primary-ip>; };' directive. BIND will automatically pull zone transfers via AXFR.

Related guides