ISP’s default DNS — gets the job done, but you control nothing
I manage the network for a 50-person office and a small datacenter. For the first few years I just let 8.8.8.8 and 1.1.1.1 handle everything — the router assigns DHCP, clients use the ISP’s DNS, “everyone does it this way anyway.” Then the problems started piling up:
- Internal hostnames wouldn’t resolve — every SSH session required typing an IP address instead of
server1.office.local - Every DNS query went out to the internet, averaging 30–40ms. Local cache is under 1ms.
- No split-DNS — internal and external queries had to return the same IP
- No logs. When the network had issues, there was no idea where to start debugging
That’s when I decided to set up Bind9. With over two decades running on production servers around the world, it’s the oldest DNS software still being actively maintained — reliable enough to trust in production.
Before installing: a few concepts you can’t skip
Bind9 can do many things: authoritative server (the authority for a domain), recursive resolver (queries on behalf of clients), or both. In this article I’m only using authoritative + forwarder mode — sufficient for an office network, no need to complicate things further.
Core concepts you need to understand
- Zone: The DNS management scope. For example:
office.localorexample.com - A record: Maps hostname → IPv4. For example:
server1.office.local → 192.168.1.10 - PTR record: Reverse DNS — maps IP → hostname, the opposite direction of an A record
- NS record: Declares the nameserver for a zone
- SOA record: Zone metadata — serial, refresh, retry, expire
- Forwarder: When Bind doesn’t know the answer, it forwards the query to another DNS server (like 8.8.8.8)
I’ll set up an internal DNS for the 192.168.1.0/24 range, domain office.local. This is enough to demonstrate the complete flow from beginning to end.
Installing and Configuring Bind9
Step 1: Install Bind9
On Ubuntu/Debian:
sudo apt update
sudo apt install -y bind9 bind9utils bind9-doc dnsutils
After installation, the named service starts automatically. Quick check:
sudo systemctl status named
Step 2: Configure named.conf.options
The /etc/bind/named.conf.options file — where you declare forwarders and access control:
sudo nano /etc/bind/named.conf.options
acl "trusted" {
192.168.1.0/24; // Internal network
localhost;
localnets;
};
options {
directory "/var/cache/bind";
// Only allow trusted network to query
allow-query { trusted; };
allow-recursion { trusted; };
// Forwarder — when unknown, ask Cloudflare
forwarders {
1.1.1.1;
8.8.8.8;
};
forward only;
dnssec-validation auto;
listen-on { any; };
listen-on-v6 { any; };
};
The forward only line is important: Bind won’t recurse outbound on its own but instead hands the query entirely to the forwarder to handle. For an office network, this approach is faster and simpler. Want Bind to resolve completely on its own, without depending on 8.8.8.8? Remove that line.
Step 3: Declare zones in named.conf.local
sudo nano /etc/bind/named.conf.local
// Forward zone
zone "office.local" {
type master;
file "/etc/bind/zones/db.office.local";
};
// Reverse zone for 192.168.1.0/24
zone "1.168.192.in-addr.arpa" {
type master;
file "/etc/bind/zones/db.192.168.1";
};
Step 4: Create the forward zone file
sudo mkdir /etc/bind/zones
sudo nano /etc/bind/zones/db.office.local
;
; Forward zone file for office.local
;
$TTL 604800
@ IN SOA ns1.office.local. admin.office.local. (
2026022801 ; Serial (format: YYYYMMDDNN)
604800 ; Refresh (7 days)
86400 ; Retry (1 day)
2419200 ; Expire (28 days)
604800 ; Negative Cache TTL
)
; Nameserver
@ IN NS ns1.office.local.
; A records
ns1 IN A 192.168.1.1
gateway IN A 192.168.1.1
server1 IN A 192.168.1.10
server2 IN A 192.168.1.11
nas IN A 192.168.1.20
printer IN A 192.168.1.30
Serial number follows the YYYYMMDDNN format: today’s date + sequence number within the day. Today is 28/02/2026, first edit → 2026022801. You must increment the serial every time you modify a zone — forget this step, and slave DNS will silently fail to pick up the update without any error message. I once lost an entire morning debugging because of this.
Step 5: Create the Reverse zone file
sudo nano /etc/bind/zones/db.192.168.1
;
; Reverse zone file for 192.168.1.0/24
;
$TTL 604800
@ IN SOA ns1.office.local. admin.office.local. (
2026022801
604800
86400
2419200
604800
)
@ IN NS ns1.office.local.
; PTR records (host part only, not the full IP)
1 IN PTR ns1.office.local.
10 IN PTR server1.office.local.
11 IN PTR server2.office.local.
20 IN PTR nas.office.local.
30 IN PTR printer.office.local.
Step 6: Validate config before restarting
Don’t skip this step. Once I restarted named without checking — a typo in the zone file took down DNS for the entire office for nearly 10 minutes:
# Check named.conf
sudo named-checkconf
# Check each zone file
sudo named-checkzone office.local /etc/bind/zones/db.office.local
sudo named-checkzone 1.168.192.in-addr.arpa /etc/bind/zones/db.192.168.1
If the output says OK, restart:
sudo systemctl restart named
sudo systemctl enable named
Step 7: Test from a client
From a machine in the 192.168.1.0/24 range, point DNS to 192.168.1.1 and test:
# Forward lookup
dig @192.168.1.1 server1.office.local
nslookup server1.office.local 192.168.1.1
# Reverse lookup
dig @192.168.1.1 -x 192.168.1.10
# Test forwarder — query external domain
dig @192.168.1.1 google.com
A successful forward lookup returns:
;; ANSWER SECTION:
server1.office.local. 604800 IN A 192.168.1.10
Query time is typically under 1ms for internal hostnames after the first lookup (already cached), compared to 30–40ms when querying 8.8.8.8 directly.
Common troubleshooting
- SERVFAIL: Zone file syntax error — run
named-checkzoneagain - REFUSED: Client IP not in the
trustedACL — checkallow-query - Timeout: Firewall blocking port 53 UDP/TCP —
ufw allow 53 - Slave not picking up new zones: Serial hasn’t been incremented — the most common mistake, and it fails silently
# View real-time log for debugging
sudo tail -f /var/log/syslog | grep named
Conclusion
Setting up Bind9 takes about 30 minutes. Real-world results: internal hostnames resolve, query time drops from 30–40ms to under 1ms for internal domains, and for the first time you have proper logs to debug when something goes wrong on the network.
My next step is split-DNS — server1.example.com returns a private IP when queried internally, and a public IP when queried externally. I’ll cover that in the next post.
Managing a network for a team of 10 or more? Don’t wait until you hit a problem to set this up. Internal DNS is a one-time setup with lasting benefits — in the most literal sense.
