BIND (Berkeley Internet Name Domain) is the most widely deployed DNS server software on the internet. It handles authoritative DNS for domains and recursive resolution for clients. If you manage your own domain and want full control over DNS records, running your own BIND server is the standard approach.
This guide covers installing and configuring BIND 9 as both a primary (master) and secondary (slave) DNS server on Debian 13 and Debian 12. You will set up forward and reverse zones, configure zone transfers between primary and secondary servers, open firewall ports, and verify everything with dig and nslookup. For the official BIND documentation, see the BIND 9 Administrator Reference Manual.
Prerequisites
- Two servers running Debian 13 (Trixie) or Debian 12 (Bookworm) with static IP addresses
- Root or sudo access on both servers
- A domain name you want to host DNS for (this guide uses
example.lanas the example domain) - Port 53 (TCP and UDP) open on both servers
- Static IPs assigned – DHCP will cause problems when the address changes
Our lab environment uses these values throughout the guide – replace them with your actual IPs and domain:
| Server | Hostname | IP Address |
|---|---|---|
| Primary DNS | ns1.example.lan | 10.0.1.10 |
| Secondary DNS | ns2.example.lan | 10.0.1.11 |
| Web Server | www.example.lan | 10.0.1.20 |
| Mail Server | mail.example.lan | 10.0.1.30 |
Step 1: Install BIND 9 DNS Server on Debian
Run these commands on both the primary and secondary servers. Start by updating the package index:
sudo apt update
Install BIND 9 and the DNS utilities package:
sudo apt install -y bind9 bind9utils bind9-doc dnsutils
Confirm that BIND installed and is running:
systemctl status bind9
The output should show the service as active (running):
● named.service - BIND Domain Name Server
Loaded: loaded (/lib/systemd/system/named.service; enabled; preset: enabled)
Active: active (running)
Main PID: 1234 (named)
Tasks: 5 (limit: 4915)
Memory: 28.0M
CPU: 85ms
CGroup: /system.slice/named.service
└─1234 /usr/sbin/named -f -u bind
Check the installed BIND version:
named -v
You should see the BIND version string confirming a 9.18+ or 9.19+ release depending on your Debian version:
BIND 9.18.28-1~deb12u2-Debian (Extended Support Version) <id:...>
Step 2: Configure BIND Options (named.conf.options)
The global BIND options file controls forwarders, recursion, and access control. Edit it on the primary server:
sudo vi /etc/bind/named.conf.options
Replace the default contents with the following configuration. This sets up DNS forwarders, enables recursion for your network, and listens on all interfaces:
options {
directory "/var/cache/bind";
// Forwarders - upstream DNS servers for queries this server cannot resolve
forwarders {
8.8.8.8;
1.1.1.1;
};
// Listen on all interfaces
listen-on { any; };
listen-on-v6 { any; };
// Allow queries from your network
allow-query { localhost; 10.0.1.0/24; };
// Enable recursion for local clients
recursion yes;
allow-recursion { localhost; 10.0.1.0/24; };
// Disable zone transfers by default (override per-zone)
allow-transfer { none; };
// DNSSEC validation
dnssec-validation auto;
};
Key settings explained:
- forwarders – upstream resolvers for queries outside your authoritative zones
- allow-query – restricts who can send DNS queries to this server
- recursion – lets the server resolve names on behalf of clients (disable on public-facing authoritative-only servers)
- allow-transfer { none; } – blocks zone transfers globally; we allow them per-zone for the secondary server
- dnssec-validation auto – validates DNSSEC signatures using the built-in trust anchor
Step 3: Create Forward and Reverse Zones on the Primary Server
Zone declarations tell BIND which domains it is authoritative for. Edit the local configuration file:
sudo vi /etc/bind/named.conf.local
Add the forward zone and reverse zone definitions. The allow-transfer and also-notify directives enable zone transfers to the secondary server:
// Forward zone
zone "example.lan" IN {
type master;
file "/etc/bind/zones/db.example.lan";
allow-transfer { 10.0.1.11; }; // Secondary server IP
also-notify { 10.0.1.11; }; // Notify secondary on changes
allow-update { none; };
};
// Reverse zone for 10.0.1.0/24
zone "1.0.10.in-addr.arpa" IN {
type master;
file "/etc/bind/zones/db.10.0.1";
allow-transfer { 10.0.1.11; };
also-notify { 10.0.1.11; };
allow-update { none; };
};
Create the directory for zone files:
sudo mkdir -p /etc/bind/zones
Step 4: Create the Forward Zone File
The forward zone file maps hostnames to IP addresses. Create it:
sudo vi /etc/bind/zones/db.example.lan
Add the following DNS records. Adjust the domain name, IPs, and hostnames for your environment:
$TTL 604800
@ IN SOA ns1.example.lan. admin.example.lan. (
2026032201 ; Serial (YYYYMMDDNN format)
604800 ; Refresh (7 days)
86400 ; Retry (1 day)
2419200 ; Expire (28 days)
604800 ) ; Negative Cache TTL (7 days)
; Name servers
@ IN NS ns1.example.lan.
@ IN NS ns2.example.lan.
; A records for name servers
ns1 IN A 10.0.1.10
ns2 IN A 10.0.1.11
; Mail exchanger
@ IN MX 10 mail.example.lan.
; A records for hosts
www IN A 10.0.1.20
mail IN A 10.0.1.30
; CNAME records
ftp IN CNAME www.example.lan.
Important points about the zone file format:
- All fully qualified domain names (FQDNs) must end with a dot (
.) - The serial number must be incremented every time you modify the zone – the secondary server uses this to detect changes
- The SOA
admin.example.lan.represents the admin email ([email protected] – the first dot replaces the @)
Step 5: Create the Reverse Zone File
The reverse zone maps IP addresses back to hostnames (PTR records). Create the reverse zone file:
sudo vi /etc/bind/zones/db.10.0.1
Add the reverse DNS records. The numbers on the left represent the last octet of each IP address:
$TTL 604800
@ IN SOA ns1.example.lan. admin.example.lan. (
2026032201 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
; Name servers
@ IN NS ns1.example.lan.
@ IN NS ns2.example.lan.
; PTR records
10 IN PTR ns1.example.lan.
11 IN PTR ns2.example.lan.
20 IN PTR www.example.lan.
30 IN PTR mail.example.lan.
Step 6: Validate BIND Configuration and Zone Files
Before restarting BIND, check for syntax errors in the configuration:
sudo named-checkconf
If there are no errors, the command returns silently to the prompt. Any syntax issues will be printed with line numbers.
Validate the forward zone file:
sudo named-checkzone example.lan /etc/bind/zones/db.example.lan
A valid zone file returns “OK” with the serial number:
zone example.lan/IN: loaded serial 2026032201
OK
Validate the reverse zone file:
sudo named-checkzone 1.0.10.in-addr.arpa /etc/bind/zones/db.10.0.1
You should see the same “OK” confirmation:
zone 1.0.10.in-addr.arpa/IN: loaded serial 2026032201
OK
Restart BIND and enable it to start at boot:
sudo systemctl restart bind9
sudo systemctl enable bind9
Verify the service is running after restart:
systemctl status bind9 --no-pager
Step 7: Configure the Secondary (Slave) DNS Server
A secondary DNS server provides redundancy and load distribution. It automatically receives zone data from the primary through zone transfers. All commands in this section run on the secondary server (10.0.1.11).
After installing BIND 9 (Step 1), configure the options file on the secondary server:
sudo vi /etc/bind/named.conf.options
Use the same options as the primary server, but adjust allow-query to match your network:
options {
directory "/var/cache/bind";
forwarders {
8.8.8.8;
1.1.1.1;
};
listen-on { any; };
listen-on-v6 { any; };
allow-query { localhost; 10.0.1.0/24; };
recursion yes;
allow-recursion { localhost; 10.0.1.0/24; };
allow-transfer { none; };
dnssec-validation auto;
};
Now configure the secondary zones. Edit the local configuration:
sudo vi /etc/bind/named.conf.local
Add the slave zone definitions pointing to the primary server’s IP. The type slave directive tells BIND to request zone data from the master rather than reading local files:
// Forward zone (secondary)
zone "example.lan" IN {
type slave;
file "/var/cache/bind/db.example.lan";
masters { 10.0.1.10; }; // Primary server IP
};
// Reverse zone (secondary)
zone "1.0.10.in-addr.arpa" IN {
type slave;
file "/var/cache/bind/db.10.0.1";
masters { 10.0.1.10; };
};
The secondary stores received zone data in /var/cache/bind/. These files are created automatically after a successful zone transfer – you do not need to create them manually.
Check the configuration syntax and restart BIND on the secondary:
sudo named-checkconf
sudo systemctl restart bind9
sudo systemctl enable bind9
Check the BIND logs to confirm zone transfers completed successfully:
sudo journalctl -u bind9 --no-pager -n 20
Look for lines indicating successful zone transfers like “transfer of ‘example.lan/IN’ from 10.0.1.10: Transfer status: success”.
Step 8: Open Firewall Ports for DNS (Port 53)
DNS uses port 53 on both TCP and UDP. TCP is required for zone transfers and large responses, while UDP handles standard queries. If you are running firewalld on Debian, open port 53 on both servers:
sudo apt install -y firewalld
sudo systemctl enable --now firewalld
Add the DNS service to the firewall and reload:
sudo firewall-cmd --permanent --add-service=dns
sudo firewall-cmd --reload
Verify port 53 is open:
sudo firewall-cmd --list-services
The output should include dns in the active services list. If you use ufw instead of firewalld, run sudo ufw allow 53/tcp and sudo ufw allow 53/udp.
If you use nftables directly, add these rules:
sudo nft add rule inet filter input tcp dport 53 accept
sudo nft add rule inet filter input udp dport 53 accept
Step 9: Test DNS Resolution with dig and nslookup
From any client machine on the same network, test the primary DNS server. First, set the DNS resolver to point to the primary server by editing /etc/resolv.conf:
sudo vi /etc/resolv.conf
Add the primary server as the nameserver:
nameserver 10.0.1.10
nameserver 10.0.1.11
Forward Lookup Test
Query the primary server for the A record of www.example.lan:
dig www.example.lan @10.0.1.10
The ANSWER SECTION should return the IP address 10.0.1.20:
;; ANSWER SECTION:
www.example.lan. 604800 IN A 10.0.1.20
Reverse Lookup Test
Test reverse DNS resolution by looking up an IP address:
dig -x 10.0.1.20 @10.0.1.10
The PTR record should return the hostname:
;; ANSWER SECTION:
20.1.0.10.in-addr.arpa. 604800 IN PTR www.example.lan.
nslookup Test
You can also use nslookup for a quick check:
nslookup www.example.lan 10.0.1.10
The response should show the server address and the resolved IP:
Server: 10.0.1.10
Address: 10.0.1.10#53
Name: www.example.lan
Address: 10.0.1.20
Step 10: Test Zone Transfer Between Primary and Secondary
Verify that the secondary server received the zone data from the primary. Query the secondary server directly:
dig www.example.lan @10.0.1.11
The response should match what the primary server returns – the same A record with IP 10.0.1.20.
You can also request a full zone transfer (AXFR) from the primary to verify it works:
dig axfr example.lan @10.0.1.10
This should return all records in the zone. If it shows “Transfer failed”, check the allow-transfer directive on the primary server and ensure the secondary’s IP is listed.
To test that zone updates propagate, modify a record on the primary server and increment the serial number. Then run on the secondary:
sudo rndc retransfer example.lan
This forces an immediate zone transfer without waiting for the refresh interval.
Step 11: DNSSEC Basics for BIND DNS Server
DNSSEC adds cryptographic signatures to DNS records, protecting against cache poisoning and man-in-the-middle attacks. BIND 9.18+ on Debian ships with DNSSEC validation enabled by default (dnssec-validation auto).
To sign your own zones with DNSSEC, you need to generate keys and configure BIND to use them. For a detailed walkthrough of zone signing and key management, see our guide on securing BIND DNS with DNSSEC keys.
At minimum, verify that DNSSEC validation is working on your server:
dig +dnssec cloudflare.com @10.0.1.10
Look for the ad flag (Authenticated Data) in the response header flags. If present, DNSSEC validation is working correctly.
Step 12: Common BIND DNS Troubleshooting
If DNS resolution fails or zones do not transfer, these commands help diagnose the issue.
Check BIND logs for errors:
sudo journalctl -u bind9 --no-pager -n 50
Verify BIND is listening on port 53:
sudo ss -tulnp | grep named
You should see named listening on port 53 for both TCP and UDP:
udp UNCONN 0 0 0.0.0.0:53 0.0.0.0:* users:(("named",pid=1234,fd=512))
tcp LISTEN 0 10 0.0.0.0:53 0.0.0.0:* users:(("named",pid=1234,fd=513))
Reload zones without restarting the service (useful after editing zone files):
sudo rndc reload
Check the status of all loaded zones:
sudo rndc status
If you run into “REFUSED” responses, verify that the querying client’s IP is included in the allow-query directive. If zone transfers fail, check that the secondary’s IP is in allow-transfer on the primary. You can also monitor BIND DNS performance with tools like Prometheus and Grafana for BIND to track query rates, cache hit ratios, and zone transfer events.
Conclusion
You now have a working BIND 9 DNS setup on Debian 13 / Debian 12 with primary and secondary servers, forward and reverse zones, zone transfers, and firewall rules in place. The secondary server provides redundancy – if the primary goes down, DNS resolution continues from the secondary’s cached zone data.
For production environments, consider implementing DNSSEC zone signing, enabling query logging for auditing, and setting up monitoring to track DNS performance. If you need a DNS management web interface, PowerDNS with PowerDNS-Admin on Debian is a solid alternative. For lightweight internal DNS, see our guide on Dnsmasq installation and configuration.