BIND DNS Server Setup and Configuration on Linux

Running your own DNS server is not always the right answer. I do not set up BIND just because I can. Managed DNS is easier for many public zones, and for small environments it is usually the better trade-off. But there are cases where I still reach for BIND: internal split-horizon DNS, authoritative zones that need tight control, lab environments, custom recursion rules, or plain old compatibility with tooling that already assumes named.

BIND has been around forever, which is both comforting and dangerous. It is mature and capable, but it also means there are decades of examples online, and a surprising number of them are outdated, incomplete, or insecure. Open resolvers, sloppy zone files, forgotten serial bumps, and broken reverse zones are still common.

This guide is how I actually set up BIND on Linux when I need a production-capable starting point. I will cover authoritative zones, reverse DNS, ACLs, logging, safe recursion, testing, reloading, and a few sharp edges that keep coming back.

If you want a refresher on record types before editing zone files, the DNS record types guide is worth reading first. If you plan to publish CA restrictions or sign the zone later, keep the CAA records overview and DNSSEC setup guide nearby.

Decide what role the server will have

Before installing anything, decide whether the server will be:

  • authoritative for one or more zones
  • recursive for internal clients
  • caching resolver with forwarders
  • a combination of authoritative plus recursion for trusted internal networks

I try not to mix public authoritative service and recursion on the same internet-exposed server unless I have a very specific reason. It is cleaner and safer to separate them.

When I run my own authoritative DNS

I use BIND as an authoritative server when I need:

  • zone files in version control
  • split-horizon DNS for internal names
  • TSIG-protected zone transfers to secondaries
  • predictable behavior without a vendor GUI in the middle
  • local labs or air-gapped environments

When I use it as a recursive resolver

I use BIND recursively inside private networks when I want:

  • local caching for internal clients
  • forwarding to upstream resolvers
  • detailed query logging for troubleshooting
  • internal ACL-based control

I do not expose recursion to the public internet. An open resolver is a security and abuse problem, not a convenience.

Install BIND9 and related tools

On Debian or Ubuntu:

sudo apt update
sudo apt install -y bind9 bind9utils bind9-dnsutils

On RHEL, AlmaLinux, or Rocky Linux:

sudo dnf install -y bind bind-utils

Enable and start the service:

sudo systemctl enable --now named
sudo systemctl status named --no-pager

On Debian-family systems the service name is often still named, even though the package is bind9. I check with systemctl list-unit-files | grep named if I am on an unfamiliar build.

Understand the configuration layout first

On Debian and Ubuntu, the main files are usually:

  • /etc/bind/named.conf
  • /etc/bind/named.conf.options
  • /etc/bind/named.conf.local
  • /etc/bind/named.conf.default-zones

On RHEL-family systems I more often see everything anchored under /etc/named.conf with zone files under /var/named/.

The general structure is the same no matter the distro:

  • options {} for global behavior
  • acl {} for client groups
  • logging {} for log channels and categories
  • zone {} blocks for forward and reverse zones

I like keeping custom authoritative zones in a dedicated directory:

sudo mkdir -p /etc/bind/zones
sudo chown root:bind /etc/bind/zones
sudo chmod 775 /etc/bind/zones

On RHEL-family systems, adapt that to the standard SELinux-aware path if needed:

sudo mkdir -p /var/named/custom
sudo chown root:named /var/named/custom
sudo chmod 750 /var/named/custom

Build a sane baseline in named.conf

A practical options block for an internal resolver plus authoritative service might look like this:

acl trusted_clients {
    127.0.0.1;
    10.0.0.0/8;
    172.16.0.0/12;
    192.168.0.0/16;
};

options {
    directory "/var/cache/bind";

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

    allow-query { any; };
    allow-recursion { trusted_clients; };
    allow-query-cache { trusted_clients; };

    recursion yes;
    dnssec-validation auto;

    auth-nxdomain no;
    version "not currently available";

    forwarders {
        1.1.1.1;
        9.9.9.9;
    };
    forward only;
};

A few notes from experience:

  • allow-query { any; }; is normal for public authoritative zones.
  • allow-recursion and allow-query-cache should be restricted.
  • forward only is fine for an internal caching resolver that should always use upstream providers.
  • hiding the version string is not magic security, but it is harmless and reduces noise.

Deprecated or risky defaults to avoid

I still see old tutorials recommend effectively open recursion. Do not do that. If you want recursion, limit it to your internal networks with ACLs.

I also see people leave logging undefined and assume syslog will tell them enough. It usually will not when you are debugging intermittent DNS behavior.

Define forward zones cleanly

For an authoritative zone on Debian or Ubuntu, I typically declare it in /etc/bind/named.conf.local:

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { key zonexfer-key; 192.0.2.53; };
    notify yes;
};

For a secondary:

zone "example.com" {
    type secondary;
    file "/var/cache/bind/db.example.com";
    masters { 192.0.2.10 key zonexfer-key; };
};

BIND now prefers primary and secondary terminology instead of the older master and slave. Old syntax still appears in many guides. I avoid copying it into new configs.

Write zone files that do not come back to bite you

Here is a working forward zone example:

$TTL 300
@   IN  SOA ns1.example.com. hostmaster.example.com. (
        2026072201 ; serial
        3600       ; refresh
        900        ; retry
        1209600    ; expire
        300        ; negative cache TTL
)
    IN  NS      ns1.example.com.
    IN  NS      ns2.example.com.

ns1 IN  A       192.0.2.10
ns2 IN  A       192.0.2.11
@   IN  A       192.0.2.20
@   IN  AAAA    2001:db8::20
www IN  CNAME   @
mail IN  A       192.0.2.30
@   IN  MX 10   mail.example.com.

This format is simple, readable, and easy to review in version control.

SOA record anatomy matters

The SOA record is not just boilerplate. I still troubleshoot zones where it was copied from somewhere else and never understood.

  • primary nameserver: usually ns1.example.com.
  • responsible mailbox: hostmaster.example.com. means hostmaster@example.com
  • serial: version number of the zone
  • refresh: how often secondaries check for updates
  • retry: how long secondaries wait after a failed refresh attempt
  • expire: how long secondaries keep serving stale data if the primary disappears
  • negative cache TTL: how long negative answers can be cached

I almost always use a serial format of YYYYMMDDNN, such as 2026072201.

That convention is boring, which is exactly why it works. You can tell at a glance when the zone changed and whether you forgot to increment it.

Serial number convention I recommend

If I make a second change the same day, I increment the last two digits:

  • 2026072201
  • 2026072202
  • 2026072203

Do not change a zone and forget the serial. That mistake wastes a lot of time because the primary will load the new file, but secondaries will not transfer it.

Add reverse DNS zones

Reverse DNS matters more than people think. Mail systems, logging, and troubleshooting all benefit from correct PTR records.

A reverse zone declaration for 192.0.2.0/24:

zone "2.0.192.in-addr.arpa" {
    type primary;
    file "/etc/bind/zones/db.192.0.2";
};

Example reverse zone file:

$TTL 300
@   IN  SOA ns1.example.com. hostmaster.example.com. (
        2026072201
        3600
        900
        1209600
        300
)
    IN  NS      ns1.example.com.
    IN  NS      ns2.example.com.

10  IN  PTR     ns1.example.com.
11  IN  PTR     ns2.example.com.
20  IN  PTR     example.com.
30  IN  PTR     mail.example.com.

The right side of a PTR record must be a fully qualified domain name with a trailing dot.

A missing trailing dot is one of those tiny mistakes that creates ugly results. BIND will happily append the zone origin, and suddenly mail.example.com becomes mail.example.com.2.0.192.in-addr.arpa.

Use ACLs to keep recursion safe

If BIND is doing recursion, I define the client ranges explicitly. Even in small offices I avoid broad any rules.

acl office_clients {
    127.0.0.1;
    192.168.50.0/24;
    10.20.30.0/24;
};

options {
    recursion yes;
    allow-recursion { office_clients; };
    allow-query-cache { office_clients; };
    allow-query { any; };
};

Why both allow-recursion and allow-query-cache? Because I want the policy to be unambiguous. Different old examples rely on defaults that are not obvious during audits.

If the server is public authoritative only, I turn recursion off entirely:

options {
    recursion no;
    allow-query { any; };
};

That is a clean public-facing posture.

Configure useful logging from day one

BIND logging is where troubleshooting gets much easier. I usually log default messages, security events, and optionally queries when diagnosing a problem.

Create a log directory first:

sudo mkdir -p /var/log/named
sudo chown bind:bind /var/log/named
sudo chmod 750 /var/log/named

Then add a logging section:

logging {
    channel default_log {
        file "/var/log/named/default.log" versions 7 size 10m;
        severity info;
        print-time yes;
        print-severity yes;
        print-category yes;
    };

    channel query_log {
        file "/var/log/named/queries.log" versions 5 size 20m;
        severity info;
        print-time yes;
    };

    channel security_log {
        file "/var/log/named/security.log" versions 5 size 10m;
        severity dynamic;
        print-time yes;
    };

    category default { default_log; };
    category queries { query_log; };
    category security { security_log; };
};

Query logging trade-offs

Query logging is useful. It is also noisy.

I normally enable it when I am diagnosing:

  • clients sending queries to the wrong server
  • recursion failures
  • stale answers coming from unexpected paths
  • application hosts hitting nonexistent names repeatedly

Then I decide whether to keep it long-term. On busy resolvers, query logs grow fast.

Test configuration before touching the running service

BIND gives you good validation tools. Use them every time.

Check overall configuration syntax:

sudo named-checkconf

Validate a forward zone:

sudo named-checkzone example.com /etc/bind/zones/db.example.com

Validate a reverse zone:

sudo named-checkzone 2.0.192.in-addr.arpa /etc/bind/zones/db.192.0.2

I do not skip these checks. They catch the easy mistakes before I start staring at empty answers and wondering why a zone did not load.

Common mistakes they catch quickly:

  • forgotten trailing dots
  • broken SOA syntax
  • duplicate records
  • malformed serials
  • PTRs outside the reverse zone

Reload without restarting the service

A full restart works, but I prefer targeted reloads because they are cleaner and less disruptive.

Reload all zones:

sudo rndc reload

Reload configuration after changes to named.conf files:

sudo rndc reconfig

Reload a single zone:

sudo rndc reload example.com

I use reconfig when I changed zone declarations and reload when I changed zone contents. In practice, either may appear to work, but keeping the distinction helps avoid confusion during larger changes.

If rndc fails, check whether the control channel and key were configured correctly by the package defaults. Most distro installs set this up automatically.

Test answers the right way

Once BIND loads successfully, I test it from both the local server and another system.

Query the server directly:

dig @127.0.0.1 example.com A

Query a specific record:

dig @127.0.0.1 www.example.com CNAME

Check mail exchange:

dig @127.0.0.1 example.com MX

Check reverse DNS:

dig @127.0.0.1 -x 192.0.2.20

When I am debugging delegation or weird responses, I add +norecurse or +trace depending on the situation. The DNS propagation troubleshooting guide goes deeper into using dig properly when answers differ across resolvers.

Use BIND as a caching resolver with forwarders

A very common internal setup is BIND as a local cache that forwards queries upstream. This gives you a consistent internal resolver without running full recursive lookups yourself.

Example:

acl internal {
    127.0.0.1;
    192.168.1.0/24;
};

options {
    listen-on port 53 { 127.0.0.1; 192.168.1.10; };
    recursion yes;
    allow-recursion { internal; };
    allow-query-cache { internal; };
    forwarders {
        1.1.1.1;
        8.8.8.8;
    };
    forward first;
};

I use forward only when the resolver should never attempt its own iterative lookups, and forward first when I want it to fall back to normal recursion if forwarders fail.

There is no universal right answer. In tightly controlled environments, forward only is usually simpler.

Secure zone transfers with TSIG

If you run secondaries, do not leave zone transfers wide open by source IP alone. TSIG gives you a shared-secret control that is simple and effective.

Generate a key:

sudo tsig-keygen -a hmac-sha256 zonexfer-key | sudo tee /etc/bind/keys-zonexfer.conf > /dev/null
sudo chmod 640 /etc/bind/keys-zonexfer.conf
sudo chown root:bind /etc/bind/keys-zonexfer.conf

Include it in the config:

include "/etc/bind/keys-zonexfer.conf";

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { key zonexfer-key; 192.0.2.53; };
    also-notify { 192.0.2.53; };
};

On the secondary:

include "/etc/bind/keys-zonexfer.conf";

zone "example.com" {
    type secondary;
    file "/var/cache/bind/db.example.com";
    masters { 192.0.2.10 key zonexfer-key; };
};

This is one of those small hardening steps that costs almost nothing and saves you from lazy exposure.

Firewall rules: DNS needs UDP and TCP 53

I still see administrators open UDP 53 and forget TCP 53.

You need both.

  • UDP handles most normal queries.
  • TCP is used for larger responses, zone transfers, and fallback cases.

With UFW:

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

With firewalld:

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

If you are using raw packet filtering, the iptables and the simpler UFW daily-use guide are both good references for locking this down correctly.

Common mistakes I keep seeing

Forgetting to increment the serial

The zone file looks updated on the primary, but secondaries keep serving old answers. That is almost always a serial problem.

Broken SOA syntax

A missing parenthesis or misordered timing value will stop the zone from loading. named-checkzone finds this quickly.

Missing trailing dots

This one is classic. ns1.example.com without the final dot inside a zone file becomes ns1.example.com.example.com.

Open recursion

If internet clients can use your resolver, fix that immediately. It is both a security issue and an abuse magnet.

Wrong reverse zone boundary

People sometimes define a /24 reverse zone and then try to insert PTRs that belong to a different network block. Reverse DNS has to match the delegated zone structure.

Operational habits that help

I keep zone files in version control, even for small internal deployments. DNS changes are easy to underestimate until someone asks who changed mail routing last Thursday.

I also try to separate authoritative and recursive roles unless the environment is small and internal. BIND can do both, but operational clarity matters. Fewer mixed responsibilities means fewer surprises during outages.

If you publish public authoritative zones, plan ahead for DNSSEC and CAA. They are not mandatory for every environment, but they are easier to add cleanly when the base zone design is already disciplined.

Final thoughts

BIND is still a solid tool when you need direct control over DNS. The trick is not the install. The trick is avoiding the old bad habits that cling to long-lived software: open recursion, no logging, sloppy zone files, and config changes pushed without validation.

My practical checklist is simple. Decide the server role first. Keep recursion restricted. Write readable forward and reverse zones. Use a serial convention you will not forget. Validate with named-checkconf and named-checkzone. Reload with rndc instead of restarting blindly. Open both UDP and TCP 53. And secure transfers with TSIG if there is a secondary involved.

Do that, and BIND becomes much less mysterious and much more predictable.

Scroll to Top