Learn to set up a fast, secure DNS server on Ubuntu using BIND9 with step-by-step commands and troubleshooting tips. Get DNS running in minutes—follow now! #CentLinux #LinuxTutorial #LinuxServer
Table of Contents
Understanding DNS and Why You Need a Local DNS Server
Imagine you’re trying to call a friend, but instead of typing their name into your phone, you have to memorize and enter their 10-digit phone number every single time. That’s exactly what computers face without DNS—except instead of phone numbers, they’re dealing with IP addresses like 192.168.1.10 or 203.0.113.45.
DNS (Domain Name System) is the internet’s massive, distributed phonebook. It translates human-friendly domain names like example.com into computer-friendly IP addresses that machines actually use to communicate. When you type youtube.com into your browser, DNS quietly figures out which IP address hosts YouTube’s servers so your request gets routed correctly.

How DNS Works: The Simple Breakdown
Here’s what happens behind the scenes when you visit a website:
- You enter a domain name (e.g.,
mywebsite.com) - Your computer asks a DNS server: “What’s the IP for
mywebsite.com?” - The DNS server responds with the IP address (e.g.,
104.26.10.123) - Your browser connects to that IP and loads the website
This process happens in milliseconds, but it’s the backbone of how the entire internet functions. Without DNS, you’d need to memorize IP addresses for every website, server, or service you use—impossible for humans, impractical for everyone.
Why Run Your Own Local DNS Server?
You might wonder: “Why not just use my ISP’s DNS or Google’s public DNS (like 8.8.8.8)?” While public DNS works fine for browsing the web, running your own DNS server unlocks powerful capabilities that are especially valuable for:
1. Home Labs and Learning Environments
If you’re building a home lab with multiple servers (maybe running Kubernetes, Docker containers, or virtual machines), a local DNS server lets you create custom names like:
web.local→ your web serverdb.local→ your database serverapi.local→ your API backend
Instead of remembering 192.168.1.10, 192.168.1.11, and 192.168.1.12, you just use friendly names. This is how real production environments work, and practicing with a local DNS server teaches you skills you’ll use in professional DevOps roles.
2. Internal Corporate Networks
Companies run internal DNS servers to resolve names for private services that don’t exist on the public internet:
intranet.company.internalgitlab.internalmonitoring.internal
These addresses only work within the organization’s network, and a local DNS server makes them accessible to all employees without exposing them externally. This is critical for security and network management.
3. Development and Testing Environments
Developers benefit hugely from local DNS when testing applications:
- Simulate production domain names locally (
app.dev,staging.dev) - Test DNS-related functionality without paying for real domain registration
- Quickly switch between different backend servers by updating DNS records
- Debug network issues by controlling how names resolve
You can even create multiple “zones” for different projects, giving you complete control over your development network.
4. Privacy and Control
When you use public DNS servers (Google, ISP, Cloudflare), they see every domain you query. A local DNS server means:
- No third party logs your internal network queries
- You control caching behavior (faster lookups for frequently accessed services)
- You can block or redirect specific domains (e.g., redirect
ads.localto nothing) - Custom TTL (Time-to-Live) values for your specific needs
5. Faster DNS Lookups for Internal Services
Public DNS servers are optimized for the global internet, not your local network. A local DNS resolver:
- Caches responses for your internal domains
- Reduces lookup latency from milliseconds to microseconds
- Eliminates dependency on external network connectivity for internal services
- Speeds up container orchestration (Kubernetes heavily relies on DNS)
Real-World Analogy: DNS as Your Office Phone Directory
Think of your company’s office phone directory. Instead of memorizing every employee’s direct extension (like
x1001,x1002,x1003), you dial their name through the directory system: “John in Accounting” → automatically connects tox1005.DNS works the same way. Instead of memorizing
192.168.1.10, you typeweb-server.local, and DNS translates it for you. The difference is DNS does this automatically, instantly, and for billions of users worldwide.
BIND9: The Industry-Standard DNS Server
When you’re ready to set up your own DNS server on Ubuntu, BIND9 (Berkeley Internet Name Domain) is the most popular and widely-used choice on Linux. It’s:
- Open-source and free
- Used by the majority of internet DNS servers
- Highly flexible and configurable
- Actively maintained with regular security updates
BIND9 has been the gold standard for DNS servers since the 1980s, and understanding it gives you skills that apply to enterprise environments worldwide.
Why BIND9 is the Industry Standard
BIND9 isn’t just another DNS server—it’s the backbone of the internet. Here’s why it dominates:
| Feature | Why It Matters |
|---|---|
| Used by 70%+ of internet DNS servers | Enterprises, ISPs, and governments trust it |
| Open-source and free | No licensing costs, full community support |
| Actively maintained | Regular security updates from ISC (Internet Systems Consortium) |
| Highly configurable | Supports everything from simple zones to complex DNSSEC |
| Cross-platform | Works on Ubuntu, Rocky Linux, Arch, Debian, and more |
BIND9 has been around since 1988, evolving through decades of internet growth. It’s the DNS software behind major tech companies, university networks, and cloud providers. When you learn BIND9, you’re learning the tool that powers the real world.
Other DNS servers exist (like dnsmasq or unbound), but BIND9 offers the most complete feature set for production environments. For home labs, internal networks, and learning DevOps, it’s the perfect choice.
Real-World Example: Small Business Using BIND9
Imagine TechStart Solutions, a 25-person software company with these internal servers:
web.techstart.internal→ Their company website (running on192.168.10.50)git.techstart.internal→ GitLab server (192.168.10.51)db.techstart.internal→ Production database (192.168.10.52)monitoring.techstart.internal→ Grafana/Prometheus stack (192.168.10.53)
Without BIND9, employees would need to remember all these IP addresses or use messy shortcuts. With BIND9:
- Everyone types friendly names like
git.techstart.internal - IT can change server IPs without updating employee configs (just update BIND9 records)
- New employees automatically get access to internal services
- No dependency on external DNS (works even if the internet goes down)
This is how every mid-to-large company runs their internal network. TechStart’s IT team installed BIND9 on a Ubuntu 26.04 server, configured zone files for .techstart.internal, and saved hours of manual troubleshooting every week.
Ready to Build Your Own?
Now that you understand what DNS is and why it’s valuable, you’re ready to set up your own BIND9 DNS server on Ubuntu 26.04. Whether you’re building a home lab, managing an internal network, or just wanting to learn enterprise-grade networking, this guide will walk you through every step—from installation to configuration to troubleshooting.
Let’s get your DNS server running!
Prerequisites and Server Preparation
Before you install BIND9 and configure your DNS server, you need to make sure your Ubuntu 26.04 system is properly prepared. Think of this as laying the foundation before building a house—if you skip these steps, everything else becomes unstable. Let’s walk through what you need and how to set it up correctly.
Minimum Hardware Requirements
You don’t need a powerhouse server to run a DNS server—DNS is incredibly lightweight. Here’s what Ubuntu 26.04 requires:
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 1 core | 2 cores |
| RAM | 512 MB | 1 GB+ |
| Disk Space | 5 GB | 10 GB+ |
| Network | Ethernet/WiFi | Stable Ethernet |
For a home lab or internal network DNS server, even an old Raspberry Pi 4 or a $5/month cloud VPS works perfectly. BIND9 consumes minimal resources—typically under 100 MB RAM when idle.
Why You Need a Static IP Address
DNS servers must have a static (permanent) IP address. Here’s why: if your server’s IP changes (which happens with dynamic IP assignment), clients won’t know where to find your DNS service. Imagine your office phone number changing every week—you’d never get calls!
For this guide, we’ll use 192.168.1.10 as our example static IP. Adjust it to match your network’s addressing scheme (e.g., 10.0.0.10 for corporate networks, 172.16.0.10 for private cloud setups).
Step 1: Configure Static IP Using Netplan
Ubuntu 26.04 uses Netplan for network configuration. Here’s how to set a static IP:
ip link showThis command displays all network interfaces (like eth0, enp0s3, or wlan0). Look for your primary Ethernet interface (usually starts with en for Ethernet or eth), which you’ll need for the next step. Most servers use eth0 or enp0s3.
sudo nano /etc/netplan/00-installer-config.yamlThis opens the Netplan config file in the nano text editor. The filename (00-installer-config.yaml) is Ubuntu’s default; if you see a different name in /etc/netplan/, use that instead. Replace the entire contents with this static IP configuration:
network:
version: 2
ethernets:
eth0: # Replace with your interface name from the previous command
addresses:
- 192.168.1.10/24
nameservers:
addresses:
- 192.168.1.1 # Your router/gateway IP
- 8.8.8.8 # Google DNS (fallback)
routes:
- to: default
via: 192.168.1.1 # Your gateway IPKey parameters explained:
addresses: 192.168.1.10/24→ Sets your static IP (192.168.1.10) with subnet mask/24(equals255.255.255.0)nameservers→ Defines DNS servers your server uses to resolve external domains (before you configure BIND9 as its own DNS)via: 192.168.1.1→ Your network gateway/router IP (usually your router’s address)
sudo netplan applyThis command applies your YAML file and restarts the network service without rebooting. If you get an error like “No network connectivity,” double-check your interface name (eth0 vs enp0s3) and gateway IP (192.168.1.1). Verify your static IP worked with ip addr show eth0—you should see 192.168.1.10 listed.
After applying, test connectivity:
ping -c 4 192.168.1.1 # Ping your gateway
ping -c 4 8.8.8.8 # Ping external DNSStep 2: Update Your System
Before installing new software, ensure your system has the latest packages and security patches:
sudo apt update && sudo apt upgrade -yThis command runs two actions together (&&):
apt update→ Refreshes the package repository lists so Ubuntu knows what new versions existapt upgrade -y→ Installs all available upgrades; the-yflag auto-confirms without prompting you
Keeping your system updated is critical for security, especially for a server that will handle network traffic. Ubuntu 26.04 includes newer kernel versions and security fixes that improve stability.
After updating, verify your Ubuntu version:
sudo ubuntu-release-upgrader --checkStep 3: Basic Security Setup
A DNS server is a network-facing service, so you need basic security in place before proceeding:
Create a Non-Root User with Sudo Privileges
Never log in as root directly. Create a dedicated user:
sudo adduser dnsadminsudo usermod -aG sudo dnsadminThis creates a user dnsadmin with a password prompt, then adds them to the sudo group (equivalent to root privileges). The && ensures the second command only runs if the first succeeds. Always use a user account for daily tasks—root is only for emergency repairs.
Enable the Uncomplicated Firewall (UFW)
Ubuntu includes UFW, a simple firewall tool. Enable it to block unnecessary ports:
sudo ufw allow 22/tcpsudo ufw enableufw allow 22/tcp→ Allows SSH so you can still log in from networkufw enable→ Activates the firewall (blocks all incoming traffic by default)
Important: If you’re on a cloud VPS, check your provider’s firewall settings first. Some cloud platforms (like AWS or Google Cloud) manage networking externally, and enabling UFW might block your access. Always allow SSH before enabling UFW!
Install Fail2Ban for SSH Protection
Fail2Ban monitors login attempts and blocks IPs that try brute-force attacks:
sudo apt install fail2ban -yThis installs Fail2Ban, which automatically bans IPs after 3 failed SSH login attempts within 10 minutes. It runs as a background service and doesn’t require configuration for basic protection.
Activate it:
sudo systemctl enable fail2bansudo systemctl start fail2banNetwork Configuration Checklist
Before moving to the BIND9 installation, verify these are all set:
| Check | Command | Expected Result |
|---|---|---|
| Static IP active | ip addr show eth0 | Shows 192.168.1.10/24 |
| Gateway reachable | ping 192.168.1.1 | Replies received |
| External DNS works | ping 8.8.8.8 | Replies received |
| System updated | apt list --upgradable | Empty list (no upgrades) |
| Firewall enabled | sudo ufw status | Shows Status: active |
| SSH accessible | ssh dnsadmin@192.168.1.10 | Logs in successfully |
Troubleshooting Common Issues
Problem: After netplan apply, no internet connectivity
Fix: Check your interface name (eth0 vs enp0s3) and gateway IP. Use ip route to see your current gateway.
Problem: apt upgrade fails with “Could not get lock”
Fix: Another process is using apt. Wait 30 seconds or run sudo killall apt apt-get then retry.
Problem: UFW blocks SSH after enabling
Fix: Boot into recovery mode or use your cloud provider’s console to run sudo ufw disable, then re-add SSH rule.
Problem: Can’t ping external IPs but gateway works
Fix: Your nameservers in Netplan might be wrong. Try 8.8.8.8 (Google) or 1.1.1.1 (Cloudflare).
You’re Ready for BIND9 Installation
Your Ubuntu 26.04 server now has:
- ✅ A static IP address (
192.168.1.10) - ✅ Updated system packages
- ✅ Basic firewall protection (UFW)
- ✅ SSH access secured with Fail2Ban
- ✅ A non-root user account
These preparations ensure your DNS server will be stable, secure, and reliable. Next, you’ll install BIND9—the actual DNS software that will handle all your domain resolution requests.
Installing BIND9 DNS Server Software
Now that your Ubuntu 26.04 server is properly prepared, you’re ready to install the actual DNS software. Meet BIND9 (Berkeley Internet Name Domain)—the most popular, reliable, and widely-used DNS server on Linux systems. If DNS servers have a “gold standard,” BIND9 is it.
Step 1: Install BIND9 and Utility Packages
BIND9 comes split into three packages in Ubuntu’s package manager. Here’s how to install all of them:
sudo apt install bind9 bind9utils bind9-dnsutils -yThis command installs three essential packages:
bind9→ The core BIND9 daemon (the actual DNS server that listens for queries)bind9utils→ Diagnostic and troubleshooting tools likenamed-checkconfandnamed-checkzonebind9-dnsutils→ Client DNS tools includingdig(powerful DNS query tool),nslookup(legacy DNS tester), andhost(simple name resolver)
The -y flag auto-confirms the installation, so you don’t have to type “yes” when apt asks for confirmation. Ubuntu downloads about 3–5 MB of packages, and installation takes 30–60 seconds on most systems.
What gets installed:
/etc/bind/ → BIND9 configuration directory
/var/cache/bind/ → Zone file cache directory
/var/log/bind9/ → Log files (if logging enabled)
/usr/sbin/named → The BIND9 daemon binary
/usr/bin/dig → DNS query tool
/usr/bin/nslookup → Legacy DNS testerIf you’re on a cloud VPS and the installation fails with “Could not resolve host,” check your nameservers setting in Netplan (from the prerequisites section). Your server needs working DNS to fetch packages from Ubuntu’s repositories.
Step 2: Verify BIND9 Service is Running
After installation, Ubuntu automatically starts the BIND9 service. Let’s confirm it’s active:
systemctl status bind9This command shows:
- Active state: Should say
active (running)in green - Process ID: The PID of the
nameddaemon (usually around 1000–2000) - Recent logs: Last few lines from BIND9’s startup process
- Enabled status: Shows whether BIND9 starts on boot (should say
enabled)
Expected output:
● bind9.service - BIND Domain Name Server
Loaded: loaded (/lib/systemd/system/bind9.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2026-06-18 14:30:22 PKT; 2min ago
Main PID: 1234 (named)
Tasks: 1 (limit: 4915)
CGroup: /system.slice/bind9.service
└─ /usr/sbin/named -u bindPress q to exit the status viewer. If you see inactive or failed, troubleshoot with:
sudo systemctl start bind9 # Manually start the service
sudo journalctl -u bind9 -n 50 # Check error logsCommon failure reasons:
- Port 53 already in use: Another DNS service (like
dnsmasq) is running. Check withsudo ss -tuln | grep 53 - Configuration syntax error: BIND9 won’t start if
/etc/bind/named.confhas mistakes. Runsudo named-checkconfto validate
Understanding the BIND9 Package Structure
Here’s what each package does in more detail:
| Package | Primary Purpose | Key Tools Included |
|---|---|---|
bind9 | DNS server daemon | named (main process) |
bind9utils | Configuration validation | named-checkconf, named-checkzone, rho |
bind9-dnsutils | DNS query/testing | dig, nslookup, host, delv |
You’ll use tools from all three packages regularly:
named-checkconf→ Validate your BIND9 config before restartingnamed-checkzone→ Check zone file syntaxdig→ Test DNS queries (your go-to debugging tool)nslookup→ Quick legacy tests (less powerful thandig)
What Happens After Installation?
Ubuntu creates several default files and directories:
ls -la /etc/bind/You’ll see:
named.conf→ Main configuration file (includes other configs)named.conf.local→ Where you’ll add your zone definitionsnamed.conf.options→ Global BIND9 options (recursion, caching, etc.)db.root→ Root zone file (for internet DNS resolution)db.127.0.0.1→ Loopback zone (localhost resolution)db.0,db.255→ Reverse zones for special IPs
The default configuration allows recursive queries (resolving external domains like google.com) but restricts this to your local network (127.0.0.0/8 and ::1). This is secure by default—you’ll expand it later when configuring your firewall.
Security Note: BIND9 Runs as a Non-Root User
BIND9 doesn’t run as root—it uses the bind user for security:
ps -el | grep namedExpected output:
F S UID PID PPID C TIME CMD
4 S 1001 1234 1 0 0:00.12 /usr/sbin/named -u bindThe UID 1001 corresponds to the bind user. This means if BIND9 has a security vulnerability, the attacker can’t access the entire system—they’re limited to the bind user’s permissions. This is a critical security practice for any network-facing service.
Configuring BIND9 for Your Domain (Forward Zone)
Now comes the fun part: telling BIND9 what domains it should manage. Think of this as creating your own mini internet where you control which names point to which IP addresses. We’ll set up a forward zone for example.local—a custom domain that only works on your network. You can swap example.local for whatever name you prefer (like myhome.lab, company.internal, or dev.staging).
What is a Forward Zone?
A forward zone maps domain names to IP addresses. When someone queries web-server.example.local, BIND9 looks up your zone file and returns 192.168.1.10. This is the opposite of a reverse zone (which we’ll cover later), where you map IP addresses back to names.
For now, focus on forward zones—they’re what you’ll use 90% of the time for internal networks, home labs, and development environments.
Step 1: Add Zone Definition to named.conf.local
BIND9’s main configuration file (/etc/bind/named.conf) includes several other files. The one you’ll edit is /etc/bind/named.conf.local—this is where you define custom zones.
# Add zone definition for your custom domain
zone "example.local" {
type master;
file "/etc/bind/db.example.local";
};This zone declaration tells BIND9:
zone "example.local"→ The domain name this zone manages (everything ending in.example.local)type master→ This server is the primary (authoritative) source for this zone. You’d useslavefor a secondary/replica serverfile "/etc/bind/db.example.local"→ The path to the zone file containing all DNS records for this domain
Why master? In production, you’d have a master server (primary) and one or more slave servers (replicas) for redundancy. For home labs and learning, master is perfect—you’re the only DNS server, so you’re automatically the master.
Where to add this: Open /etc/bind/named.conf.local and append this block at the bottom:
sudo nano /etc/bind/named.conf.localYou’ll see some default content (like reverse zone definitions). Add your new zone after those:
// Existing reverse zones (leave these):
zone "127.0.0.1/in-addr.arpa" {
type master;
file "/etc/bind/db.127";
};
// Your new forward zone (add this):
zone "example.local" {
type master;
file "/etc/bind/db.example.local";
};Press Ctrl+O, then Enter to save, and Ctrl+X to exit nano.
Step 2: Create the Zone File with DNS Records
Now you need to create the actual zone file /etc/bind/db.example.local. This file contains all your DNS records—the “phonebook entries” that map names to IPs.
# Create the zone file for example.local
sudo nano /etc/bind/db.example.localPaste this fully commented template into the file. Replace the values with your real domain and IP addresses:
; Zone file for example.local
; This file maps domain names to IP addresses for your internal network
; SOA Record (Start of Authority) - defines zone metadata
$ORIGIN example.local. ; Default domain for this zone
$TTL 3600 ; Default TTL: 3600 seconds (1 hour)
@ IN SOA ns1.example.local. dnsadmin.example.local. (
2026061801 ; Serial number (YYYYMMDDNN format)
3600 ; Refresh: slave checks for updates every 1 hour
1800 ; Retry: retry interval if refresh fails (30 min)
604800 ; Expire: max time slave uses old data (7 days)
3600 ) ; Minimum TTL: min TTL for negative responses (1 hour)
; NS Record (Name Server) - lists authoritative servers
@ IN NS ns1.example.local. ; Primary name server
; A Records (Address) - map hostnames to IP addresses
ns1 IN A 192.168.1.10 ; Name server (your BIND9 server)
web-server IN A 192.168.1.11 ; Web server
db-server IN A 192.168.1.12 ; Database server
api-server IN A 192.168.1.13 ; API backend
mail-server IN A 192.168.1.14 ; Mail server
; CNAME Records (Canonical Name) - create aliases
www IN CNAME web-server.example.local. ; www.example.local → web-server
api IN CNAME api-server.example.local. ; api.example.local → api-server
db IN CNAME db-server.example.local. ; db.example.local → db-server
; MX Record (Mail Exchanger) - for email routing (optional)
@ IN MX 10 mail-server.example.local. ; Priority 10 mail server
; PTR Record placeholder (for reverse zone - leave empty here)
; PTR records go in the reverse zone file, not forward zoneBreaking down the key records:
| Record Type | Purpose | Example |
|---|---|---|
| SOA | Start of Authority—zone metadata (serial number, refresh times) | Must exist in every zone file |
| NS | Name Server—lists which servers are authoritative for this zone | Points to ns1.example.local |
| A | Address—maps hostname to IPv4 address | web-server → 192.168.1.11 |
| CNAME | Canonical Name—creates aliases (shorter names) | www → web-server.example.local |
| MX | Mail Exchanger—routes email to mail server | Priority 10 = highest priority |
Key parameters explained:
$ORIGIN example.local.→ Sets the default domain. Any record without a full domain (likeweb-server) automatically becomesweb-server.example.local$TTL 3600→ Time-to-Live: how long clients cache this record (3600 seconds = 1 hour). Lower TTL (300s) for frequently changing records; higher TTL (86400s) for stable records- Serial number (
2026061801) → Zone version number. Format:YYYYMMDDNN(year, month, day, sequence). Increment this by 1 whenever you edit the zone file—otherwise slave servers won’t detect changes ns1.example.local.→ Note the trailing dot! This means “absolute domain,” not relative to$ORIGIN. Without the dot, it becomesns1.example.local.example.local(wrong!)dnsadmin.example.local.→ Email address of zone administrator (use.instead of@)
Real-world example: Imagine you’re setting up a home lab with these servers:
- Your BIND9 DNS server:
192.168.1.10→ns1.home.lab - Raspberry Pi running Home Assistant:
192.168.1.20→homeassistant.home.lab - Docker host:
192.168.1.21→docker.home.lab - NAS storage:
192.168.1.22→nas.home.lab
Your zone file would look like:
$ORIGIN home.lab.
$TTL 3600
@ IN SOA ns1.home.lab. admin.home.lab. (
2026061801 3600 1800 604800 3600 )
@ IN NS ns1.home.lab.
ns1 IN A 192.168.1.10
homeassistant IN A 192.168.1.20
docker IN A 192.168.1.21
nas IN A 192.168.1.22Then add aliases for convenience:
ha IN CNAME homeassistant.home.lab. ; ha.home.lab → homeassistant
dock IN CNAME docker.home.lab. ; dock.home.lab → dockerNow you can type ha.home.lab instead of the full name—super handy for CLI tools and scripts!
Understanding Record Syntax
BIND9 zone files follow a strict format:
[NAME] [TTL] [CLASS] TYPE RDATAExample breakdown:
web-server IN A 192.168.1.11- NAME:
web-server→ Becomesweb-server.example.local.(thanks to$ORIGIN) - TTL: (empty) → Uses default
$TTL 3600 - CLASS:
IN→ Internet (always useINfor IPv4) - TYPE:
A→ Address record (IPv4) - RDATA:
192.168.1.11→ The IP address
For CNAME records:
www IN CNAME web-server.example.local.- RDATA:
web-server.example.local.→ Must be a fully qualified domain name (FQDN) with trailing dot
Common mistakes to avoid:
- ❌ Missing trailing dot on FQDN:
web-server.example.local→ Becomesweb-server.example.local.example.local - ❌ Wrong serial number format:
2026-06-18→ Use2026061801(no dashes) - ❌ Using
@in email:admin@example.local→ Useadmin.example.local.(replace@with.)
Validate Your Zone File Before Restarting
Never restart BIND9 without checking your config first. Run these validation commands:
# Check main configuration syntax
sudo named-checkconfIf there are no errors, you get no output (success). If there’s a problem, it shows the line number and error message.
# Check zone file syntax for example.local
sudo named-checkzone example.local /etc/bind/db.example.localExpected output:
zone example.local/IN: loaded serial 2026061801
OKIf you see ERROR or FAILED, check for:
- Missing semicolons on comments
- Missing trailing dots on FQDNs
- Wrong serial number format
- Syntax errors in SOA record (missing parentheses)
What Happens Next?
After validating, you’ll restart BIND9 to load your new zone. But first, let’s make sure the file permissions are correct:
# Ensure zone file has proper ownership
sudo chown bind:bind /etc/bind/db.example.local
sudo chmod 644 /etc/bind/db.example.localchown bind:bind→ Owned by thebinduser (same as the BIND9 daemon)chmod 644→ Readable by all, writable only by owner (secure default)
Now your forward zone is ready! BIND9 will respond to queries like:
web-server.example.local→192.168.1.11www.example.local→192.168.1.11(via CNAME)db.example.local→192.168.1.12(via CNAME)
In the next section, you’ll set up a reverse zone to map IP addresses back to names—essential for email servers and network debugging.
Setting Up Reverse DNS (Pointer Records)
You’ve just set up your forward zone so names resolve to IP addresses. But what if you need to go the other way—IP addresses to names? That’s where reverse DNS comes in, and it’s absolutely critical for email servers, network security, and troubleshooting.
What is Reverse DNS?
Reverse DNS (also called pointer resolution) maps IP addresses back to domain names using PTR (Pointer) records. Instead of asking “What’s the IP for web-server.example.local?”, you ask “What’s the name for 192.168.1.10?”
Forward DNS: web-server.example.local → 192.168.1.10
Reverse DNS: 192.168.1.10 → web-server.example.local
This two-way resolution is how the internet validates identities. Without it, systems can’t verify that an IP actually belongs to the domain it claims to represent.
Why Reverse DNS is Critical?
1. Email Server Compliance (SMTP)
This is the most common real-world reason organizations need reverse DNS. Here’s why:
When your mail server sends an email to Gmail, Outlook, or Yahoo, the receiving server:
- Looks up your mail server’s IP address (e.g.,
192.168.1.14) - Performs a reverse DNS query to get the hostname
- Checks if that hostname matches your domain’s MX record
- If reverse DNS is missing or mismatched, your email gets rejected or marked as spam
Real-world example: Imagine TechStart Solutions sends newsletters from their mail server at 192.0.2.50. Their domain is techstart.com, and their MX record points to mail.techstart.com.
Without reverse DNS:
- Gmail receives email from
192.0.2.50 - Gmail queries reverse DNS for
192.0.2.50 - Reverse DNS returns nothing (or
unknown) - Gmail marks the email as spam or rejects it entirely ❌
With reverse DNS:
- Gmail queries reverse DNS for
192.0.2.50 - Reverse DNS returns
mail.techstart.com - Gmail checks forward DNS:
mail.techstart.com→192.0.2.50✓ - Email passes validation and lands in inbox ✅
Major email providers (Google, Microsoft, Amazon SES) require valid reverse DNS for SMTP compliance. If you’re running a mail server, reverse DNS is non-negotiable.
2. Network Debugging and Security
When you run ping, ssh, or curl and see an IP address, reverse DNS lets you see the actual hostname:
# Reverse lookup: what's the name for this IP?
dig -x 192.168.1.10Output:
web-server.example.localThis helps you:
- Identify unknown devices on your network
- Verify SSH connections (you’re connecting to
web-server, not some random IP) - Debug firewall issues (logs show hostnames instead of IPs)
- Monitor network traffic with meaningful names in tools like Wireshark
3. SSH and Service Verification
Many services use reverse DNS for access control:
- SSH
HostKeyAlgoriesvalidation - Database connection authentication
- API rate limiting by hostname
- Load balancer health checks
Without reverse DNS, you’re working with anonymous IPs—hard to manage at scale.
How Reverse DNS Works: The Zone Naming Trick
Reverse DNS uses a special domain called in-addr.arpa (for IPv4). The IP address gets reversed and appended:
| IP Address | Reverse Zone |
|---|---|
192.168.1.10 | 1.168.192.in-addr.arpa |
10.0.0.5 | 0.0.10.in-addr.arpa |
172.16.0.1 | 0.16.172.in-addr.arpa |
Why reverse it? DNS zone hierarchy works from right to left. The root zone (.) contains arpa, which contains in-addr, which contains network segments. Reversing the IP lets DNS organize by network:
- All
192.168.1.xIPs live in1.168.192.in-addr.arpa - All
192.168.2.xIPs live in2.168.192.in-addr.arpa - And so on
For your 192.168.1.x network, the reverse zone is 1.168.192.in-addr.arpa.
Step 1: Add Reverse Zone to named.conf.local
Now let’s configure BIND9 to handle reverse DNS for your 192.168.1.x network.
# Define reverse zone for your IP range
zone "1.168.192.in-addr.arpa" {
type master;
file "/etc/bind/db.192.168.1";
};This zone declaration tells BIND9:
zone "1.168.192.in-addr.arpa"→ The reverse zone for the192.168.1.xnetworktype master→ This server is the primary authority for this reverse zonefile "/etc/bind/db.192.168.1"→ The zone file containing PTR records
Where to add this: Open /etc/bind/named.conf.local and append it after your forward zone:
# Forward zone (from previous section)
zone "example.local" {
type master;
file "/etc/bind/db.example.local";
};
# Reverse zone (add this):
zone "1.168.192.in-addr.arpa" {
type master;
file "/etc/bind/db.192.168.1";
};Save and exit (Ctrl+O, Enter, Ctrl+X).
Step 2: Create the Reverse Zone File with PTR Records
Now create the reverse zone file /etc/bind/db.192.168.1 with PTR records:
# Create the reverse zone file for 192.168.1.x
sudo nano /etc/bind/db.192.168.1Paste this fully commented template:
; Reverse zone file for 192.168.1.x
; This file maps IP addresses back to hostnames
$ORIGIN 1.168.192.in-addr.arpa.
$TTL 3600
; SOA Record
@ IN SOA ns1.example.local. dnsadmin.example.local. (
2026061801 ; Serial (YYYYMMDDNN)
3600 ; Refresh (1 hour)
1800 ; Retry (30 min)
604800 ; Expire (7 days)
3600 ) ; Minimum TTL (1 hour)
; NS Record
@ IN NS ns1.example.local.
; PTR Records (Pointer) - map IPs to hostnames
10 IN PTR ns1.example.local. ; 192.168.1.10 → ns1
11 IN PTR web-server.example.local. ; 192.168.1.11 → web-server
12 IN PTR db-server.example.local. ; 192.168.1.12 → db-server
13 IN PTR api-server.example.local. ; 192.168.1.13 → api-server
14 IN PTR mail-server.example.local. ; 192.168.1.14 → mail-server
; Add more PTR records as needed:
; 20 IN PTR homeassistant.example.local. ; For 192.168.1.20
; 21 IN PTR docker.example.local. ; For 192.168.1.21Breaking down PTR record syntax:
10 IN PTR web-server.example.local.10→ The last octet of the IP (192.168.1.10). BIND9 automatically prepends1.168.192.in-addr.arpa.thanks to$ORIGININ→ Internet class (always useIN)PTR→ Pointer record typeweb-server.example.local.→ The fully qualified hostname (FQDN) with trailing dot
Key rule: The PTR record value must be a fully qualified domain name (FQDN) with a trailing dot. Without the dot, it becomes web-server.example.local.1.168.192.in-addr.arpa (wrong!).
Real-world example: TechStart’s email compliance setup
TechStart’s mail server runs at 192.0.2.50 with hostname mail.techstart.com. Their reverse zone file includes:
$ORIGIN 2.0.192.in-addr.arpa.
@ IN SOA ns1.techstart.com. admin.techstart.com. (
2026061801 3600 1800 604800 3600 )
@ IN NS ns1.techstart.com.
50 IN PTR mail.techstart.com.When Gmail receives email from 192.0.2.50:
- Reverse DNS query:
50.2.0.192.in-addr.arpa→mail.techstart.com - Forward DNS check:
mail.techstart.com→192.0.2.50✓ - Email passes SPF/DKIM validation and lands in inbox ✅
Without this PTR record, TechStart’s emails would be rejected by major providers.
Validate and Activate Your Reverse Zone
Before restarting BIND9, validate both configurations:
# Check main configuration syntax
sudo named-checkconf# Check reverse zone file syntax
sudo named-checkzone 1.168.192.in-addr.arpa /etc/bind/db.192.168.1Expected output:
zone 1.168.192.in-addr.arpa/IN: loaded serial 2026061801
OKSet proper permissions:
# Ensure reverse zone file has correct ownership
sudo chown bind:bind /etc/bind/db.192.168.1
sudo chmod 644 /etc/bind/db.192.168.1Testing Reverse DNS
Once BIND9 restarts, test reverse lookups:
# Reverse lookup for 192.168.1.11
dig -x 192.168.1.11 @localhostExpected output:
web-server.example.local# Alternative using nslookup
nslookup 192.168.1.11 localhostExpected output:
Name: web-server.example.local
Address: 192.168.1.11Common troubleshooting:
| Problem | Solution |
|---|---|
Reverse lookup returns unknown | Check PTR record has trailing dot: web-server.example.local. |
| Serial number not updating | Increment serial in SOA record: 2026061801 → 2026061802 |
| Zone not loading | Run named-checkzone to find syntax errors |
| Wrong hostname returned | PTR value must match A record hostname exactly |
When You Don’t Need Reverse DNS
For pure home labs or development environments where you’re not sending email, reverse DNS is optional. But if you plan to:
- Run a mail server (even for testing)
- Deploy to production
- Work with enterprise networks
- Use cloud services (AWS, Google Cloud require reverse DNS for some services)
Then reverse DNS is mandatory. It’s also a best practice for any serious network setup.
Validating Configuration and Restarting BIND9
You’ve now configured both forward and reverse zones for your DNS server. But before you restart BIND9 and hope everything works, you must validate your configuration. Why? Because BIND9 won’t start if there’s a syntax error, and if it fails to start, your DNS service goes down completely. Think of validation as checking your airplane’s engines before takeoff—you don’t want to discover problems mid-flight.
Why Validation is Non-Negotiable
BIND9 is strict about configuration syntax. A single missing brace, typo, or wrong character can break the entire service. Common mistakes include:
- Missing closing brace
}in zone definitions - Missing trailing dot on FQDNs (
example.localinstead ofexample.local.) - Wrong serial number format (
2026-06-18instead of2026061801) - Unmatched parentheses in SOA records
Without validation, you’d have to restart BIND9, see it fail, check logs, guess the error, fix it, and restart again. Validation catches these issues before you restart, saving you time and preventing service downtime.
Step 1: Check Main Configuration Syntax
First, validate your BIND9 configuration files (/etc/bind/named.conf.*):
# Check BIND9 configuration file syntax
named-checkconfThis command scans all included configuration files:
/etc/bind/named.conf→ Main config (includes other files)/etc/bind/named.conf.local→ Your zone definitions/etc/bind/named.conf.options→ Global options
Expected output:
- Success: No output (command exits silently)
- Failure: Error message showing the file, line number, and problem
Example failure:
/etc/bind/named.conf.local:5: missing ';' before 'zone'This tells you line 5 in named.conf.local has a missing semicolon before the zone keyword.
Why no output means success: BIND9 follows Unix conventions—silent = good, output = error. If you see anything, fix it before proceeding.
Step 2: Verify Forward Zone File Syntax
Now validate your forward zone file (/etc/bind/db.example.local):
# Verify zone file syntax for example.local
named-checkzone example.local /etc/bind/db.example.localThis checks:
- SOA record structure
- NS record presence
- A/CNAME/MX record syntax
- Serial number format
- Trailing dots on FQDNs
Expected output:
zone example.local/IN: loaded serial 2026061801
OKWhat each part means:
zone example.local/IN→ Zone name + class (IN = Internet)loaded serial 2026061801→ Confirms serial number is validOK→ Zone file is syntactically correct
Example failure:
/etc/bind/db.example.local:15: missing ';' before 'IN'Line 15 has a syntax error (probably missing a semicolon after a comment).
Common errors and fixes:
| Error | Cause | Fix |
|---|---|---|
missing ';' | Comment without semicolon | Add ; before comment text |
unknown key 'XYZ' | Invalid record type | Check record type (A, NS, SOA, etc.) |
too many parentheses | Unmatched ( in SOA | Ensure SOA has one ( and one ) |
not a valid domain name | Missing trailing dot | Add . to FQDN: example.local. |
Step 3: Verify Reverse Zone File Syntax
Don’t forget your reverse zone! Validate it the same way:
# Verify reverse zone file syntax
named-checkzone 1.168.192.in-addr.arpa /etc/bind/db.192.168.1Expected output:
zone 1.168.192.in-addr.arpa/IN: loaded serial 2026061801
OKIf you get an error, check:
- PTR records have trailing dots:
web-server.example.local. $ORIGINmatches the reverse zone name:1.168.192.in-addr.arpa.- Last octet in PTR records is just the number:
10(not192.168.1.10)
Real-World Troubleshooting: Fixing a Missing Brace Error
Let’s walk through a real scenario. You just added your reverse zone to named.conf.local, but when you run named-checkconf, you get:
/etc/bind/named.conf.local:12: expecting '{'What happened: You forgot the opening brace { in your zone definition. Your config looks like:
zone "1.168.192.in-addr.arpa" ; Missing '{' here!
type master;
file "/etc/bind/db.192.168.1";
};How to fix:
- Open the file:
sudo nano /etc/bind/named.conf.local - Add the missing brace:
zone "1.168.192.in-addr.arpa" {
type master;
file "/etc/bind/db.192.168.1";
};- Save and exit (
Ctrl+O,Enter,Ctrl+X) - Re-run validation:
named-checkconf - Now you get no output = success ✓
Why this happens: Zone definitions require braces to group their contents. Without {, BIND9 can’t parse the type and file lines.
Pro tip: Use your editor’s syntax highlighting. Nano shows unmatched braces in red, helping you catch errors before validation.
Step 4: Restart and Enable BIND9 Service
Once both validations pass, restart BIND9 to load your new zones:
# Restart and enable BIND9 service
sudo systemctl restart bind9 && sudo systemctl enable bind9This command does two things:
systemctl restart bind9→ Stops and starts BIND9, applying all configuration changessystemctl enable bind9→ Ensures BIND9 starts automatically on boot (won’t go down after reboot)
The && operator runs the second command only if the first succeeds. If BIND9 fails to start, the second command won’t run, and you’ll see an error.
Expected behavior:
- Command exits silently (no output)
- BIND9 starts within 1–2 seconds
Verify it’s running:
systemctl status bind9You should see:
Active: active (running) since Thu 2026-06-18 14:45:33 PKT; 5s agoIf BIND9 fails to start:
# Check error logs
sudo journalctl -u bind9 -n 50This shows the last 50 log entries from BIND9, revealing exactly why it failed (syntax error, port conflict, permission issue, etc.).
Common startup failures:
| Error | Cause | Fix |
|---|---|---|
address already in use | Another DNS service running (dnsmasq) | Stop dnsmasq: sudo systemctl stop dnsmasq |
permission denied | Zone file owned by wrong user | Fix ownership: sudo chown bind:bind /etc/bind/db.example.local |
unknown host | DNS resolver can’t reach internet | Check /etc/resolv.conf nameserver |
zone not loaded | Validation error missed | Re-run named-checkzone for that zone |
Understanding systemd Service Management
Ubuntu uses systemd for service management. Here are essential BIND9 commands:
# Start BIND9 (if stopped)
sudo systemctl start bind9
# Stop BIND9
sudo systemctl stop bind9
# Check if BIND9 is enabled (starts on boot)
sudo systemctl is-enabled bind9
# View all BIND9 logs
sudo journalctl -u bind9
# Reload configuration without restarting (faster)
sudo systemctl reload bind9Restart vs Reload:
- Restart → Full stop/start (applies all changes, takes 2–3 seconds)
- Reload → Keeps process running, re-reads config (faster, 1 second), but doesn’t apply all changes
For initial setup, use restart. For minor config tweaks, use reload.
Double-Check: Is BIND9 Listening on Port 53?
After restarting, verify BIND9 is actually listening for DNS queries:
# Check if BIND9 is listening on port 53
sudo ss -tuln | grep 53Expected output:
udp UNCONN 0 0 0.0.0.0:53 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:53 0.0.0.0:*This shows BIND9 listening on:
- UDP port 53 → Standard DNS queries (most common)
- TCP port 53 → Zone transfers and large responses
If you don’t see this output, BIND9 isn’t running. Check logs with journalctl -u bind9.
Final Validation: Test Both Zones
Now that BIND9 is running, test both forward and reverse resolution:
# Test forward lookup
dig @localhost web-server.example.localExpected: Returns 192.168.1.11
# Test reverse lookup
dig @localhost -x 192.168.1.11Expected: Returns web-server.example.local
If both work, your configuration is perfect! If one fails, check the corresponding zone file and re-run validation.
You’re Now Running a Production DNS Server
Your BIND9 server is now:
- ✅ Configured with forward and reverse zones
- ✅ Validated for syntax errors
- ✅ Restarted and enabled for boot
- ✅ Listening on port 53 (UDP/TCP)
- ✅ Resolving both forward and reverse queries
In the next section, you’ll configure your firewall to allow DNS queries from client machines, then test from outside your server.
Configuring the Firewall for DNS Traffic
You’ve got BIND9 running and validating queries perfectly—but wait! Your firewall is probably blocking incoming DNS traffic. Remember back in the prerequisites when you enabled UFW? By default, it blocks all incoming connections except SSH. That means client machines can’t reach your DNS server yet. Let’s open the right ports so your network can use your new DNS service.
Why DNS Needs Ports 53 (TCP and UDP)
DNS uses port 53 for all communication, but it uses both UDP and TCP—not just one. Here’s why:
| Protocol | When It’s Used | Why |
|---|---|---|
| UDP port 53 | Standard DNS queries (most common) | Ultra-fast, no connection setup, under 512 bytes |
| TCP port 53 | Zone transfers, large responses, retries | Reliable, handles data >512 bytes, ensures delivery |
UDP (User Datagram Protocol): Think of UDP like sending a postcard—you drop it in the mailbox and hope it arrives. No connection handshake, no confirmation, just super fast. Most DNS queries (like dig example.com) use UDP because they’re tiny (under 512 bytes) and speed matters. Your browser asks “What’s the IP for google.com?” and gets the answer in milliseconds.
TCP (Transmission Control Protocol): TCP is like a phone call—you establish a connection, confirm receipt, and resend if needed. DNS uses TCP for:
- Zone transfers: When a slave server copies all records from the master (can be megabytes)
- Large responses: DNS responses over 512 bytes (DNSSEC signatures, many AAAA records)
- Retries: If UDP query fails, DNS falls back to TCP
Real-world analogy: Imagine ordering coffee. UDP is like shouting “Coffee!” at the counter and grabbing it when ready (fast, no confirmation). TCP is like ordering at the counter, getting a receipt, waiting for confirmation, and getting your coffee with a name tag (slower but guaranteed).
For your DNS server to work, you need both ports open. Missing one breaks specific functions.
Step 1: Allow DNS Queries Over UDP
Open UDP port 53 for standard DNS lookups:
# Allow DNS queries over UDP (most common)
sudo ufw allow 53/udpThis command:
ufw allow→ Adds an allow rule (incoming traffic permitted)53/udp→ Port 53 using UDP protocol
Why UDP first? 95% of DNS queries use UDP. This is the most critical rule for your DNS server to function. Without it, clients can’t resolve names at all.
What happens: UFW adds a rule to /etc/ufw/user.rules and updates the firewall immediately. No restart needed.
Step 2: Allow DNS Zone Transfers Over TCP
Now open TCP port 53 for zone transfers and large responses:
# Allow DNS zone transfers and large responses over TCP
sudo ufw allow 53/tcpThis command:
53/tcp→ Port 53 using TCP protocol
Why TCP matters:
- Zone transfers: If you set up a slave DNS server later (Section 9), it needs TCP to copy all your zone records
- DNSSEC responses: Signed DNS records often exceed 512 bytes, requiring TCP
- AAAA records: IPv6 queries sometimes return large responses
Without TCP open, zone transfers fail and some DNS queries time out. Your server will work for basic lookups but break for advanced use cases.
Step 3: Verify Your Firewall Rules
Check that both rules are active:
# Verify active firewall rules
sudo ufw statusThis command lists all firewall rules. Expected output:
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
53/udp ALLOW Anywhere
53/tcp ALLOW AnywhereWhat to confirm:
- ✅
Status: active→ Firewall is running - ✅
53/udp ALLOW→ UDP DNS queries permitted - ✅
53/tcp ALLOW→ TCP DNS permitted - ✅
22/tcp ALLOW→ SSH still works (from prerequisites)
If you don’t see 53 rules:
- Rule wasn’t added: Re-run
ufw allow 53/udpandufw allow 53/tcp - Firewall inactive: Run
sudo ufw enable(but allow SSH first!)
Detailed view:
# Show rule numbers and more details
sudo ufw status numberedThis adds numbers to each rule (useful for deleting later: ufw delete 3).
Real-World Best Practice: Restrict Zone Transfers to Specific IPs
Allowing TCP port 53 from “Anywhere” is fine for most setups, but zone transfers should be restricted. Why? If anyone can transfer your zone, they can steal your entire DNS database (all internal IPs, hostnames, services).
Secure approach: Only allow zone transfers from your slave server’s IP:
# Remove open TCP rule (if already added)
sudo ufw delete allow 53/tcp
# Allow TCP port 53 only from slave server IP
sudo ufw allow from 192.168.1.11 to any port 53 proto tcpThis rule:
from 192.168.1.11→ Only the slave server at this IP can connectto any→ Any local IP (your server)port 53 proto tcp→ TCP port 53 only
Verify the restricted rule:
sudo ufw statusOutput:
53/tcp ALLOW 192.168.1.11Important: You still need UDP open from anywhere (standard queries):
sudo ufw allow 53/udp # Still allows from AnywhereWhy this matters: In enterprise environments, attacking parties scan for open DNS servers and request zone transfers to map internal networks. Restricting TCP to known slave IPs prevents this data leakage.
Alternative: Use BIND9’s allow-transfer
You can also restrict zone transfers in BIND9 config itself (/etc/bind/named.conf.options):
options {
allow-transfer { 192.168.1.11; }; # Only slave server
};This is double protection: firewall blocks unauthorized IPs, and BIND9 rejects transfers even if they somehow get through. Use both layers for security.
Understanding UFW Rule Priority
UFW processes rules top-to-bottom. The first matching rule wins:
1. 22/tcp ALLOW from Anywhere
2. 53/udp ALLOW from Anywhere
3. 53/tcp ALLOW from 192.168.1.11
4. deny all (default)If a request from 192.168.1.50 hits port 53/tcp:
- Rule 3 doesn’t match (IP is wrong)
- Falls to Rule 4 → DENIED
This is why restricted rules work. If you had 53/tcp ALLOW from Anywhere instead, Rule 3 wouldn’t exist, and everyone gets access.
Troubleshooting Firewall Issues
Problem: Clients can’t reach DNS server
Check: sudo ufw status → Confirm 53/udp and 53/tcp show ALLOW
Problem: UDP works but TCP fails
Check: Did you run ufw allow 53/tcp? Some guides forget TCP.
Problem: Firewall blocks SSH after enabling
Fix: Always allow SSH before enabling UFW:
sudo ufw allow 22/tcp
sudo ufw enableProblem: Rules don’t persist after reboot
Fix: UFW rules save automatically, but ensure it’s enabled:
sudo ufw enable
sudo ufw status # Should show "Status: active"Problem: Want to test without firewall
Temporarily disable:
sudo ufw disable
# Test DNS
dig @192.168.1.10 web-server.example.local
# Re-enable
sudo ufw enableSecurity Checklist for DNS Firewall Rules
| Rule | Secure? | Note |
|---|---|---|
53/udp ALLOW from Anywhere | ✅ Yes | Standard queries need open access |
53/tcp ALLOW from Anywhere | ⚠️ Acceptable | Fine for home labs, not enterprises |
53/tcp ALLOW from 192.168.1.11 | ✅ Best | Restricts zone transfers to slave |
| No UDP rule | ❌ Broken | DNS queries fail completely |
| No TCP rule | ⚠️ Partial | Basic queries work, zone transfers fail |
Final Verification: Test DNS from a Client
After opening firewall ports, test from another machine on your network:
# On a client machine (not your DNS server)
dig @192.168.1.10 web-server.example.localExpected: Returns 192.168.1.11
# Test reverse lookup from client
dig @192.168.1.10 -x 192.168.1.11Expected: Returns web-server.example.local
If both work, your firewall is configured correctly!
You’ve Secured Your DNS Server
Your Ubuntu 26.04 DNS server now:
- ✅ Accepts UDP DNS queries from any client
- ✅ Accepts TCP zone transfers (restricted to slave IP if you followed best practice)
- ✅ Blocks all other incoming traffic (firewall still active)
- ✅ Maintains SSH access for remote management
In the next section, you’ll test your DNS server thoroughly using dig, nslookup, and client machine configuration to ensure everything works end-to-end.
Testing Your DNS Server
So you’ve configured BIND9, double-checked your zone files, and restarted the service. Now comes the moment of truth: does it actually work? Testing your DNS server isn’t just a formality—it’s the only way to confirm that your carefully crafted records are resolvable and that clients can find your services.
Let’s walk through the essential validation commands that every DNS administrator should know. We’ll cover both forward lookups (hostname → IP) and reverse lookups (IP → hostname), plus a real-world scenario that brings it all together.
Forward Lookup: Does Your Server Know the Hostname?
The most basic test: can your DNS server resolve a hostname to an IP address? Here’s how to check:
# Test forward lookup for web-server.example.local
dig @localhost web-server.example.localThis command sends a query directly to your BIND9 server (via @localhost) asking for the A record of web-server.example.local. The response will include an ANSWER SECTION—that’s where you’ll find the IP address your server has associated with that hostname. If the section is empty or returns NXDOMAIN, something’s off in your forward zone file.
Pro tip: dig is the DNS administrator’s swiss army knife. It’s flexible, produces clear output, and gives you complete visibility into what your server is actually returning.
Reverse Lookup: Can You Trace an IP Back to a Name?
Forward lookups are only half the story. Many applications and security tools rely on reverse DNS to verify that an IP address belongs to the expected hostname.
# Test reverse lookup for IP 192.168.1.10
dig @localhost -x 192.168.1.10The -x flag tells dig to perform a reverse query. It automatically constructs the special in-addr.arpa domain and looks for the PTR record associated with that IP. If everything’s configured correctly, the ANSWER SECTION will display the hostname that maps to 192.168.1.10. This is crucial for services like mail servers, which often reject connections from IPs without valid reverse DNS.
The Simpler Alternative: nslookup
Not everyone loves dig‘s verbose output. Sometimes you just want a quick, readable answer:
# Alternative test using nslookup
nslookup web-server.example.local localhostnslookup is a simpler, more approachable tool for DNS queries. By passing localhost as the second argument, you’re explicitly telling it to query your BIND9 server rather than the system’s default resolver. The output shows the resolved IP right next to “Address”—no parsing through verbose sections required. It’s particularly handy when you’re quickly verifying records during development.
Real-World Validation: Testing from an Actual Client
Local tests are great, but the real proof is whether other machines can use your DNS server. Here’s a scenario every developer will recognise:
Meet Alex. Alex just deployed a new internal microservice called
payment-processor.example.localon192.168.1.10. The service is running, but the frontend team can’t connect because they’re using the hostname instead of the IP. Alex needs to verify that the DNS server is actually serving this record to other machines on the network.
To test this, Alex temporarily configures a client machine to use the DNS server directly:
# Test from a client by setting DNS to your server IP
sudo tee /etc/resolv.conf > /dev/null << EOF
nameserver 192.168.1.10
EOFThis overwrites the client’s DNS resolver configuration to point exclusively at your BIND9 server (192.168.1.10 in this example). Now any DNS query from this client—whether from a browser, curl, or a ping command—will be resolved by your server.
Alex runs ping payment-processor.example.local and gets a response. The service is accessible. The DNS server is live.
A quick note: On modern Linux distributions using systemd-resolved, /etc/resolv.conf is often a symlink managed dynamically. For permanent client configuration, you’d typically edit /etc/systemd/resolved.conf or configure your DHCP server to hand out your DNS server address automatically. But for a quick validation test like Alex’s, the temporary approach works perfectly.
Optional: Setting Up a Secondary (Slave) DNS Server
Let’s be honest: a single DNS server is a single point of failure. And in production, that’s a risk you don’t want to take. If your master server goes down—whether from hardware failure, a network hiccup, or an ill-timed maintenance window—every service that depends on hostname resolution grinds to a halt.
That’s where a secondary (slave) DNS server comes in. It holds a read-only copy of your zones and stays in sync with the master through zone transfers. Clients can query the slave just like they would the master, so DNS keeps working even when the primary is offline. It’s redundancy that pays for itself the first time something goes wrong.
Step 1: Tell the Master to Share
Before the slave can pull any data, the master needs to know who’s allowed to request a zone transfer. By default, BIND allows transfers to any host—which is a security risk you absolutely don’t want in production. Lock it down to just your slave server:
# Allow slave server to request zone transfers
options {
allow-transfer { 192.168.1.11; };
};This snippet goes inside the options block of your master’s named.conf (typically /etc/bind/named.conf.options). It tells BIND: “Only the server at 192.168.1.11 is authorised to request a full copy of any zone.” The allow-transfer directive applies globally to all zones defined on this server. If you need per-zone control, you can also place it inside individual zone blocks instead.
Security note: For production environments, consider using TSIG (Transaction Signatures) to cryptographically sign zone transfers instead of relying solely on IP addresses. IPs can be spoofed; cryptographic keys are much harder to fake.
Step 2: Configure the Slave to Pull
Now hop over to your slave server. Here’s where you define which zones it should replicate and where to find the master:
# Configure slave zone with master server IP
zone "example.local" {
type slave;
masters { 192.168.1.10; };
file "/etc/bind/db.example.local";
};This goes in the slave’s named.conf.local (or directly in named.conf). Let’s break it down:
type slave;– declares this as a secondary zone. The slave will periodically check the master for updates.masters { 192.168.1.10; };– tells the slave which server to contact for zone transfers. You can list multiple masters for extra resilience.file "/etc/bind/db.example.local";– specifies where to store the local copy of the zone. BIND will write the transferred zone data here, which speeds up server restarts and reduces bandwidth usage.
Once you restart BIND on the slave (sudo systemctl restart bind9), it will reach out to the master, perform an initial zone transfer (AXFR), and save the zone file locally. From then on, it will check back at the interval defined by the Refresh value in the master’s SOA record.
Real-World Example: Keeping the Lights On During a Crisis
Meet Sarah. Sarah runs the IT infrastructure for a mid-sized e-commerce company. Their DNS master server sits on a single physical machine in their primary data centre. One Tuesday morning, that machine’s power supply fails catastrophically. The server is down. Hard.
Normally, this would be a disaster. No DNS means no one can resolve
api.orders.example.localorcheckout.payments.example.local. The website would still be reachable by IP, but every internal microservice call would fail. Orders would stop flowing. Customers would see errors.But Sarah set up a slave DNS server six months ago on a VM in their secondary data centre at
192.168.1.11. The master at192.168.1.10was already configured withallow-transfer { 192.168.1.11; };, and the slave hadmasters { 192.168.1.10; };in its zone definition. The slave had been quietly pulling zone updates every few hours, staying perfectly in sync.When the master died, Sarah’s monitoring dashboard alerted her immediately. She updated the DHCP scope to hand out the slave’s IP as the primary DNS server, and within minutes, all client machines were resolving hostnames again. The engineering team kept deploying, the orders kept flowing, and Sarah fixed the power supply at her leisure. No panic. No downtime. Just a well-designed redundant DNS setup doing its job.
That’s the power of a secondary DNS server. It’s not glamorous, but when the master fails, it’s the unsung hero that keeps your entire infrastructure running.
Common Troubleshooting Tips and Best Practices
Let’s be real for a moment: DNS troubleshooting can feel like detective work. You stare at config files, restart services, and still get that dreaded “server not found” error. But here’s the good news—most DNS issues follow predictable patterns, and with the right tools in your toolkit, you’ll resolve them faster than you think.
Whether you’re chasing down a misconfigured zone file, debugging a stubborn reverse lookup, or locking down your server against abuse, this section will walk you through the most effective troubleshooting techniques—and the best practices that keep problems from happening in the first place.
Debug Mode: When You Need to See Everything
Sometimes, the logs just don’t tell you enough. You need to see every query, every decision BIND makes, in real time. That’s when debug mode becomes your best friend:
# Enable debug mode for detailed logging
sudo bind9 -d 3This runs BIND9 in the foreground with debug level 3, streaming verbose query and error logs directly to your terminal (stderr). Think of it as lifting the hood while the engine’s running—you’ll see exactly how BIND processes each incoming request, which zone files it’s consulting, and where things might be going wrong.
When to use it: Debug mode is invaluable when you’re testing a new zone configuration or tracking down a query that’s being answered incorrectly. The -d flag accepts levels from 0 (minimal) to 99 (everything including the kitchen sink). Start with level 3—it gives you enough detail without drowning you in noise. Just remember to stop the debug process (Ctrl+C) and restart BIND as a daemon (sudo systemctl start bind9) once you’ve found your answer. Running in debug mode indefinitely isn’t practical for production.
The Journalctl Lifeline: Your First Stop for Logs
Before you dive into debug mode, check the system logs. Nine times out of ten, the answer is already there:
# View recent BIND9 logs
sudo journalctl -u bind9 -n 50This command pulls the last 50 log entries from BIND9’s systemd journal. You’ll see startup errors, zone transfer failures, permission issues, and query denials—all in chronological order. It’s your first line of defence when something feels off.
When to use it: Always start here. Did BIND fail to start? The journal will tell you exactly which line in your named.conf has a syntax error. Did a zone transfer fail? You’ll see the timeout or permission denial right in the logs. For ongoing monitoring, increase the -n value to see more context, or use -f to follow logs in real time (journalctl -u bind9 -f).
Locking Down Recursion: A Security Non-Negotiable
Here’s a mistake that’s surprisingly common: leaving recursion open to the entire internet. It’s the DNS equivalent of leaving your front door wide open. Attackers can use your server in DNS amplification attacks—and suddenly your carefully configured DNS server becomes part of a DDoS campaign against someone else.
# Restrict recursion to trusted networks only
options {
allow-recursion { 192.168.1.0/24; };
};This limits recursive queries to your internal network (192.168.1.0/24), preventing unauthorised external clients from using your server for recursion. For authoritative-only servers, consider setting recursion no; entirely—authoritative name servers shouldn’t allow recursive queries except to localhost.
Why this matters: Open recursive resolvers are a favourite tool for attackers. They spoof the source IP of their victims and send queries to your server, which then responds with large DNS replies—amplifying the attack traffic. By restricting recursion, you’re not just protecting your own resources; you’re being a good internet citizen.
Real-World Scenario: The Case of the Missing Period
Meet Jamie. Jamie is a junior sysadmin who just set up a new BIND9 server for a small company’s internal network. Forward lookups work perfectly—
dig web-server.example.localreturns the right IP. But reverse lookups? They’re returning nonsense likewindows.cis527.example.local.40.168.192.in-addr.arpa. The team can’t SSH into servers by hostname, and everyone’s frustrated.Jamie runs
sudo journalctl -u bind9 -n 50—no obvious errors. The zone files look correct at first glance. So Jamie starts BIND in debug mode:sudo bind9 -d 3and queries the reverse zone. The debug output shows BIND appending the domain name to every PTR record. That’s the clue.Jamie opens the reverse zone file and spots it: a missing period at the end of a hostname entry. Instead of
windows.cis527.example.local.(with the trailing dot), it sayswindows.cis527.example.local(without it). BIND automatically appends the zone’s domain name to any unqualified name, turning it into a garbled mess.One period added. Zone reloaded. Reverse lookups work perfectly. Jamie learned the hard way that in DNS, that tiny dot matters more than you’d ever expect.
Best Practices That Save You From Midnight Calls
1. TTL Tuning: Balance Performance and Flexibility
Time-to-Live values determine how long clients and resolvers cache your DNS records. The most common TTL values range from 1 minute to 60 minutes. Shorter TTLs (like 30–300 seconds) let you make changes quickly—ideal when you’re about to migrate a service. Longer TTLs (like 3600–86400 seconds) reduce query load and improve performance. The trick is knowing when to use which. Planning a maintenance window? Lower your TTLs 24 hours in advance. Running a stable production service? Keep TTLs higher to reduce DNS traffic.
2. Regular Updates: Security Patches Aren’t Optional
BIND has had its share of vulnerabilities. Recent CVEs have included assertion failures that can crash your server when serving stale cache data alongside authoritative zone content. The fix? Upgrade to patched releases. Set up a regular update schedule and test patches in a staging environment before applying them to production.
3. Use ACLs for Granular Control
Access Control Lists let you define trusted networks once and reuse them across multiple directives—allow-query, allow-recursion, allow-transfer, and more. This keeps your configuration clean and reduces the risk of typos. For example:
acl trusted { 192.168.1.0/24; 10.0.0.0/8; };
options {
allow-recursion { trusted; };
allow-query { trusted; };
};4. TSIG for Secure Zone Transfers
IP-based allow-transfer is a good start, but it’s not foolproof—IPs can be spoofed. For production environments, implement TSIG (Transaction Signatures) to cryptographically sign zone transfers between masters and slaves. This ensures that only servers with the correct shared key can request or receive zone data.
5. Validate Zone Files Before Reloading
Before you restart BIND, run named-checkzone example.local /etc/bind/db.example.local to validate your zone file syntax. It catches missing semicolons, incorrect SOA serials, and—you guessed it—missing periods. A few seconds of validation can save you minutes of troubleshooting.
Frequently Asked Questions (FAQ)
1. Can this DNS server resolve external domains like google.com, or only internal names?
Yes, it can resolve both. By default, BIND9 allows recursive queries for external domains from your local network. To enable it for your entire network, update the allow-recursion setting in /etc/bind/named.conf.options to include your internal IP range (like 192.168.1.0/24). For internal-only resolution, disable recursion entirely.
2. Why can’t client machines resolve names even though my DNS server is running?
This is usually a client configuration or firewall issue. First, ensure your firewall allows port 53 (UDP and TCP). Then, set your client machine’s DNS setting to point to your BIND9 server IP (192.168.1.10). Finally, test with a command like dig @192.168.1.10 web-server.example.local from the client.
3. How often should I update the serial number in my zone file, and what happens if I forget?
You must increment the serial number by 1 every time you edit a zone file (add records, change IPs, etc.). If you forget, slave DNS servers won’t detect changes, and clients will keep using old cached records. This causes inconsistent DNS across your network. Always update the serial before restarting BIND9.
4. Is it safe to allow recursive queries from my entire network, or should I restrict them?
Always restrict recursive queries to your trusted internal network only. Opening recursion to “any” makes your server an open resolver, which attackers use for DDoS amplification attacks and cache poisoning. In production, explicitly list your network range (like 192.168.1.0/24) in the allow-recursion setting.
5. Can I run both BIND9 and another DNS service (like dnsmasq) on the same server?
No, not without conflicts. Both services try to bind to port 53, causing an “address already in use” error. For a dedicated DNS server, disable dnsmasq and systemd-resolved completely. BIND9 is the only DNS service you need on a server running as your primary DNS.
Conclusion
You’ve now built a fully functional BIND9 DNS server on Ubuntu 24.04—from initial setup to forward and reverse zone configuration, firewall protection, and live testing. This isn’t just a tutorial; it’s a production-ready skill that enterprises, DevOps teams, and home lab enthusiasts use daily.
What You’ve Accomplished
✅ Installed BIND9 — The industry-standard DNS server powering 70%+ of internet DNS
✅ Created a forward zone — Map web-server.example.local → 192.168.1.11 and more
✅ Set up reverse DNS — Enable IP-to-name resolution for email compliance and debugging
✅ Validated configuration — Used named-checkconf and named-checkzone to prevent errors
✅ Configured firewall — Opened UDP/TCP port 53 while maintaining security
✅ Tested end-to-end — Verified forward and reverse lookups with dig and nslookup
Next Steps to Level Up
Now that your DNS server works, here’s how to make it enterprise-ready:
- Add a Secondary (Slave) DNS Server — Configure redundancy so your DNS stays online if the master fails
- Implement DNSSEC — Sign your zones cryptographically to prevent DNS spoofing attacks
- Enable Logging and Monitoring — Track queries, detect abuse, and troubleshoot issues proactively
- Restrict Recursion — Limit recursive queries to your internal network to prevent open resolver abuse
- Automate Zone Updates — Use scripts or Ansible to update DNS records when servers change IPs
Recommended Courses
If you are new to Linux and want a solid starting point, I highly recommend Ubuntu Linux Server Basics by Cody Ray Miller. This beginner-friendly guide walks you step by step through setting up and managing Ubuntu servers, making it perfect for students, sysadmins, and developers who want practical hands-on knowledge. With clear explanations and real-world examples, this resource can fast-track your Linux learning journey and save you countless hours of trial and error.
Disclaimer: This post contains affiliate links. If you purchase through these links, I may earn a small commission at no extra cost to you.
Key Resources
- Ubuntu Server DNS Documentation opens in new tab
- BIND Project Official Website opens in new tab
- RFC 1034: Domain Names – Concepts and Facilities opens in new tab




Leave a Reply
You must be logged in to post a comment.