BIND9: Setup a DNS Server on Ubuntu 26.04

Share on Social Media

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.

BIND9: Setup DNS Server on Ubuntu
BIND9: Setup DNS Server on Ubuntu

How DNS Works: The Simple Breakdown

Here’s what happens behind the scenes when you visit a website:

  1. You enter a domain name (e.g., mywebsite.com)
  2. Your computer asks a DNS server: “What’s the IP for mywebsite.com?”
  3. The DNS server responds with the IP address (e.g., 104.26.10.123)
  4. 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 server
  • db.local → your database server
  • api.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.internal
  • gitlab.internal
  • monitoring.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.local to 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 to x1005.

DNS works the same way. Instead of memorizing 192.168.1.10, you type web-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:

FeatureWhy It Matters
Used by 70%+ of internet DNS serversEnterprises, ISPs, and governments trust it
Open-source and freeNo licensing costs, full community support
Actively maintainedRegular security updates from ISC (Internet Systems Consortium)
Highly configurableSupports everything from simple zones to complex DNSSEC
Cross-platformWorks 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 on 192.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.

YouTube player

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:

ResourceMinimumRecommended
CPU1 core2 cores
RAM512 MB1 GB+
Disk Space5 GB10 GB+
NetworkEthernet/WiFiStable 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 show

This 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.yaml

This 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 IP

Key parameters explained:

  • addresses: 192.168.1.10/24 → Sets your static IP (192.168.1.10) with subnet mask /24 (equals 255.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 apply

This 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 DNS

Step 2: Update Your System

Before installing new software, ensure your system has the latest packages and security patches:

sudo apt update && sudo apt upgrade -y

This command runs two actions together (&&):

  • apt update → Refreshes the package repository lists so Ubuntu knows what new versions exist
  • apt upgrade -y → Installs all available upgrades; the -y flag 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 --check

Step 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 dnsadmin

This 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.

YouTube player
Enable the Uncomplicated Firewall (UFW)

Ubuntu includes UFW, a simple firewall tool. Enable it to block unnecessary ports:

sudo ufw allow 22/tcpsudo ufw enable
  • ufw allow 22/tcp → Allows SSH so you can still log in from network
  • ufw 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 -y

This 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 fail2ban

Network Configuration Checklist

Before moving to the BIND9 installation, verify these are all set:

CheckCommandExpected Result
Static IP activeip addr show eth0Shows 192.168.1.10/24
Gateway reachableping 192.168.1.1Replies received
External DNS worksping 8.8.8.8Replies received
System updatedapt list --upgradableEmpty list (no upgrades)
Firewall enabledsudo ufw statusShows Status: active
SSH accessiblessh dnsadmin@192.168.1.10Logs 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 -y

This command installs three essential packages:

  • bind9 → The core BIND9 daemon (the actual DNS server that listens for queries)
  • bind9utils → Diagnostic and troubleshooting tools like named-checkconf and named-checkzone
  • bind9-dnsutils → Client DNS tools including dig (powerful DNS query tool), nslookup (legacy DNS tester), and host (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 tester

If 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 bind9

This command shows:

  • Active state: Should say active (running) in green
  • Process ID: The PID of the named daemon (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 bind

Press 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 logs

Common failure reasons:

  • Port 53 already in use: Another DNS service (like dnsmasq) is running. Check with sudo ss -tuln | grep 53
  • Configuration syntax error: BIND9 won’t start if /etc/bind/named.conf has mistakes. Run sudo named-checkconf to validate

Understanding the BIND9 Package Structure

Here’s what each package does in more detail:

PackagePrimary PurposeKey Tools Included
bind9DNS server daemonnamed (main process)
bind9utilsConfiguration validationnamed-checkconf, named-checkzone, rho
bind9-dnsutilsDNS query/testingdig, nslookup, host, delv

You’ll use tools from all three packages regularly:

  • named-checkconf → Validate your BIND9 config before restarting
  • named-checkzone → Check zone file syntax
  • dig → Test DNS queries (your go-to debugging tool)
  • nslookup → Quick legacy tests (less powerful than dig)

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 definitions
  • named.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 named

Expected output:

F S   UID       PID  PPID  C    TIME   CMD
4 S 1001 1234 1 0 0:00.12 /usr/sbin/named -u bind

The 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 use slave for a secondary/replica server
  • file "/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.local

You’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.local

Paste 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 zone

Breaking down the key records:

Record TypePurposeExample
SOAStart of Authority—zone metadata (serial number, refresh times)Must exist in every zone file
NSName Server—lists which servers are authoritative for this zonePoints to ns1.example.local
AAddress—maps hostname to IPv4 addressweb-server192.168.1.11
CNAMECanonical Name—creates aliases (shorter names)wwwweb-server.example.local
MXMail Exchanger—routes email to mail serverPriority 10 = highest priority

Key parameters explained:

  • $ORIGIN example.local. → Sets the default domain. Any record without a full domain (like web-server) automatically becomes web-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 becomes ns1.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.10ns1.home.lab
  • Raspberry Pi running Home Assistant: 192.168.1.20homeassistant.home.lab
  • Docker host: 192.168.1.21docker.home.lab
  • NAS storage: 192.168.1.22nas.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.22

Then add aliases for convenience:

ha      IN      CNAME   homeassistant.home.lab.   ; ha.home.lab → homeassistant
dock    IN      CNAME   docker.home.lab.          ; dock.home.lab → docker

Now 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  RDATA

Example breakdown:

web-server  IN  A  192.168.1.11
  • NAME: web-server → Becomes web-server.example.local. (thanks to $ORIGIN)
  • TTL: (empty) → Uses default $TTL 3600
  • CLASS: IN → Internet (always use IN for 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 → Becomes web-server.example.local.example.local
  • ❌ Wrong serial number format: 2026-06-18 → Use 2026061801 (no dashes)
  • ❌ Using @ in email: admin@example.local → Use admin.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-checkconf

If 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.local

Expected output:

zone example.local/IN: loaded serial 2026061801
OK

If 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.local
  • chown bind:bind → Owned by the bind user (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.local192.168.1.11
  • www.example.local192.168.1.11 (via CNAME)
  • db.example.local192.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.local192.168.1.10
Reverse DNS: 192.168.1.10web-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:

  1. Looks up your mail server’s IP address (e.g., 192.168.1.14)
  2. Performs a reverse DNS query to get the hostname
  3. Checks if that hostname matches your domain’s MX record
  4. 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.com192.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.10

Output:

web-server.example.local

This 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 HostKeyAlgories validation
  • 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 AddressReverse Zone
192.168.1.101.168.192.in-addr.arpa
10.0.0.50.0.10.in-addr.arpa
172.16.0.10.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.x IPs live in 1.168.192.in-addr.arpa
  • All 192.168.2.x IPs live in 2.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 the 192.168.1.x network
  • type master → This server is the primary authority for this reverse zone
  • file "/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.1

Paste 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.21

Breaking down PTR record syntax:

10  IN  PTR  web-server.example.local.
  • 10 → The last octet of the IP (192.168.1.10). BIND9 automatically prepends 1.168.192.in-addr.arpa. thanks to $ORIGIN
  • IN → Internet class (always use IN)
  • PTR → Pointer record type
  • web-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:

  1. Reverse DNS query: 50.2.0.192.in-addr.arpamail.techstart.com
  2. Forward DNS check: mail.techstart.com192.0.2.50
  3. 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.1

Expected output:

zone 1.168.192.in-addr.arpa/IN: loaded serial 2026061801
OK

Set 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.1

Testing Reverse DNS

Once BIND9 restarts, test reverse lookups:

# Reverse lookup for 192.168.1.11
dig -x 192.168.1.11 @localhost

Expected output:

web-server.example.local
# Alternative using nslookup
nslookup 192.168.1.11 localhost

Expected output:

Name:    web-server.example.local
Address: 192.168.1.11

Common troubleshooting:

ProblemSolution
Reverse lookup returns unknownCheck PTR record has trailing dot: web-server.example.local.
Serial number not updatingIncrement serial in SOA record: 20260618012026061802
Zone not loadingRun named-checkzone to find syntax errors
Wrong hostname returnedPTR 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.local instead of example.local.)
  • Wrong serial number format (2026-06-18 instead of 2026061801)
  • 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-checkconf

This 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.local

This 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
OK

What each part means:

  • zone example.local/IN → Zone name + class (IN = Internet)
  • loaded serial 2026061801 → Confirms serial number is valid
  • OK → 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:

ErrorCauseFix
missing ';'Comment without semicolonAdd ; before comment text
unknown key 'XYZ'Invalid record typeCheck record type (A, NS, SOA, etc.)
too many parenthesesUnmatched ( in SOAEnsure SOA has one ( and one )
not a valid domain nameMissing trailing dotAdd . 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.1

Expected output:

zone 1.168.192.in-addr.arpa/IN: loaded serial 2026061801
OK

If you get an error, check:

  • PTR records have trailing dots: web-server.example.local.
  • $ORIGIN matches the reverse zone name: 1.168.192.in-addr.arpa.
  • Last octet in PTR records is just the number: 10 (not 192.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:

  1. Open the file: sudo nano /etc/bind/named.conf.local
  2. Add the missing brace:
zone "1.168.192.in-addr.arpa" {
    type master;
    file "/etc/bind/db.192.168.1";
};
  1. Save and exit (Ctrl+O, Enter, Ctrl+X)
  2. Re-run validation: named-checkconf
  3. 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 bind9

This command does two things:

  • systemctl restart bind9 → Stops and starts BIND9, applying all configuration changes
  • systemctl 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 bind9

You should see:

Active: active (running) since Thu 2026-06-18 14:45:33 PKT; 5s ago

If BIND9 fails to start:

# Check error logs
sudo journalctl -u bind9 -n 50

This shows the last 50 log entries from BIND9, revealing exactly why it failed (syntax error, port conflict, permission issue, etc.).

Common startup failures:

ErrorCauseFix
address already in useAnother DNS service running (dnsmasq)Stop dnsmasq: sudo systemctl stop dnsmasq
permission deniedZone file owned by wrong userFix ownership: sudo chown bind:bind /etc/bind/db.example.local
unknown hostDNS resolver can’t reach internetCheck /etc/resolv.conf nameserver
zone not loadedValidation error missedRe-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 bind9

Restart 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 53

Expected 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.local

Expected: Returns 192.168.1.11

# Test reverse lookup
dig @localhost -x 192.168.1.11

Expected: 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:

ProtocolWhen It’s UsedWhy
UDP port 53Standard DNS queries (most common)Ultra-fast, no connection setup, under 512 bytes
TCP port 53Zone transfers, large responses, retriesReliable, 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/udp

This 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/tcp

This 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 status

This command lists all firewall rules. Expected output:

Status: active

To Action From
-- ------ ----
22/tcp ALLOW Anywhere
53/udp ALLOW Anywhere
53/tcp ALLOW Anywhere

What 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/udp and ufw allow 53/tcp
  • Firewall inactive: Run sudo ufw enable (but allow SSH first!)

Detailed view:

# Show rule numbers and more details
sudo ufw status numbered

This 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 tcp

This rule:

  • from 192.168.1.11 → Only the slave server at this IP can connect
  • to any → Any local IP (your server)
  • port 53 proto tcp → TCP port 53 only

Verify the restricted rule:

sudo ufw status

Output:

53/tcp                     ALLOW       192.168.1.11

Important: You still need UDP open from anywhere (standard queries):

sudo ufw allow 53/udp  # Still allows from Anywhere

Why 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 enable

Problem: 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 enable

Security Checklist for DNS Firewall Rules

RuleSecure?Note
53/udp ALLOW from Anywhere✅ YesStandard queries need open access
53/tcp ALLOW from Anywhere⚠️ AcceptableFine for home labs, not enterprises
53/tcp ALLOW from 192.168.1.11✅ BestRestricts zone transfers to slave
No UDP rule❌ BrokenDNS queries fail completely
No TCP rule⚠️ PartialBasic 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.local

Expected: Returns 192.168.1.11

# Test reverse lookup from client
dig @192.168.1.10 -x 192.168.1.11

Expected: 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.local

This 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.10

The -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 localhost

nslookup 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.local on 192.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
EOF

This 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.local or checkout.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 at 192.168.1.10 was already configured with allow-transfer { 192.168.1.11; };, and the slave had masters { 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 3

This 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 50

This 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.local returns the right IP. But reverse lookups? They’re returning nonsense like windows.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 3 and 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 says windows.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:

  1. Add a Secondary (Slave) DNS Server — Configure redundancy so your DNS stays online if the master fails
  2. Implement DNSSEC — Sign your zones cryptographically to prevent DNS spoofing attacks
  3. Enable Logging and Monitoring — Track queries, detect abuse, and troubleshoot issues proactively
  4. Restrict Recursion — Limit recursive queries to your internal network to prevent open resolver abuse
  5. Automate Zone Updates — Use scripts or Ansible to update DNS records when servers change IPs

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



About the Author

Ahmer M
Ahmer M

Ahmer M

Linux & DevOps Blogger | Freelancer
I am a Technology Enthusiast with more than 15 years of Experience in Linux, DevOps, Cloud and Container Orchestration.


Leave a Reply