Mark Jenkins

Information Technologist


Reducing redundancy in bind zone files

I assume fairly advanced knowledge of bind and DNS here.

I’m feeling redundant

If you’ve ever looked at the zone files in a typical BIND DNS setup, you’ll find quite a bit of redundancy between them. Every single zone will have a separate file, each with SOA, NS, A, and possibly MX and CNAME records. Often these files are almost identical to each other, typically the result of the file being copied from another. I’ve encountered this most often on systems that provide web and email service for several domain names.

Each time a new domain name is added, the admin typically copies a previous zone file. If they’ve been using absolute (not relative) record names in the zone files, they have to go through the new file and change this everywhere. If they’ve been using the $ORIGIN directive they have to change that too.

Change is hard

Sure enough, a day comes where the admin decides to make a change that is common to all of these zones, such as an ip address (A record) change, new secondary name server (NS), etc.. If there are many zone files, the change will require some serious scripting. This won’t just be a matter of replacing an ip address in all places with another, serious DNS admins will lower the TTL, wait the old TTL, change the record, and bring back the OLD TTL when making some kinds of changes.

Now, double all of this trouble and opportunity for screwup when you start using split DNS. (this means your nameserver gives different answers to queries depending on where they original from, common use case is to provide different service to a LAN from the world)

To avoid what I call “zone file redundancy hell”, you should take advantage of the $INCLUDE directive in your zone files to move redundant information into one place.

Common Configuration

I’m going to take you through an example setup that provides authoritative, master nameservice for cool.tld. and super.tld., and avoids redundancy. Note that my configuration files are derived from the bind9 package in Debian and Ubuntu, which puts configuration in /etc/bind where they belong. You can adapt the ideas here to a more typical BIND configuration.

// this is named.conf, it implements split DNS
include "/etc/bind/named.conf.options";

view "local_network"
	match-clients {localhost; };
	recursion yes;

	// prime the server with knowledge of the root servers
	zone "." {
		type hint;
		file "/etc/bind/db.root";

	// Consider adding the 1918 zones here, if they are not used in your
	// organization
	include "/etc/bind/zones.rfc1918";

	// be authoritative for the localhost forward and reverse zones, and for
	// broadcast zones as per RFC 1912

	zone "localhost" {
		type master;
		file "/etc/bind/db.local";

	zone "" {
		type master;
		file "/etc/bind/db.127";

	zone "" {
		type master;
		file "/etc/bind/db.0";

	zone "" {
		type master;
		file "/etc/bind/db.255";

	include "/etc/bind/internal_zone_list.zones";

view "external_network"
	match-clients {!localhost; any; };
	recursion no;

	// prime the server with knowledge of the root servers
	zone "." {
		type hint;
		file "/etc/bind/db.root";

	include "/etc/bind/zone_list.zones";

/etc/bind/zone_list.zones and
are our zone list files. Both of them contain zone entries for cool.tld. and super.tld. . One specifies the zone files for the external side of the split DNS, /etc/bind/cool.tld.db and /etc/bind/super.tld.db, and other for the internal side, /etc/bind/internal_cool.tld.db and /etc/bind/internal_super.tld.db . To avoid redundancy, we don’t want to have to manualy edit both of these files where a new zone is added, so we maintain a common file zone_list_file


and we use a Makefile and a python script (make_zone_file) to autogenerate zone_list.zones and internal_zone_list.zones from zone_list_file.

# Makefile

all: zone_list.zones internal_zone_list.zones

zone_list.zones: $(ZONE_LIST) Makefile
	./make_zone_list --prefix "/etc/bind/" \
	--suffix $(ZONE_FILE_SUFFIX) $^ > $@

internal_zone_list.zones: $(ZONE_LIST) Makefile
	./make_zone_list --prefix "/etc/bind/internal_" \
        --suffix $(ZONE_FILE_SUFFIX) $^ > $@


#!/usr/bin/env python

from optparse import OptionParser
from sys import stdout

option_parser = OptionParser()
option_parser.add_option("-p", "--prefix", default="")
option_parser.add_option("-s", "--suffix", default="")
(options, args) = option_parser.parse_args()

def iterjoin(join_str, iterable):
    first = True
    for value in iterable:
        if not first:
            yield join_str
            first = False
        yield value

if len(args) > 0:
    input_file = file(args[0])
        ("""zone "%(zone_name)s" {
\tfile "%(file_prefix)s%(zone_name)s%(file_suffix)s";
\ttype master;
""" % {'zone_name': line.strip(), 
       'file_prefix': options.prefix,
       'file_suffix': options.suffix, }
         for line in input_file 
         if len(line.strip()) > 0 ) ) )

You end up with autogenerated zone entries like:

zone "cool.tld" {
	file "/etc/bind/cool.tld.db";
	type master;

We’ve thought through it, and we already know that all of our zones are going to have a common set of SOA, NS, CNAME, and MX records, and a common TTL for all records, so we create common_TTL_SOA_NS_CNAME_MX_for_cool_zones.db

; default TTL
$TTL 3h

; common SOA
@       IN      SOA     cool.tld. (
	2008081401 ; serial, todays date + todays serial
	3H         ; slave refresh frequency
	15M        ; slave retry rate when refresh fails
	4W         ; expire time until slaves give up on refresh
	2D )       ; minimum-TTL if one isn't specified

; common NS
@	NS	cool.tld.

; common CNAME
www	CNAME	@

; common MX
@	MX	10 cool.tld.

Different view

Now we start getting into the differences between zones. We want to have different A records for the internal view of our split DNS compared to the external view. So, we define common_TTL_SOA_NS_CNAME_MX_A_for_cool_zones.db :

$INCLUDE "/etc/bind/common_TTL_SOA_NS_CNAME_MX_for_cool_zones.db";
@	A

and common_TTL_SOA_NS_CNAME_MX_A_for_internal_cool_zones.db

$INCLUDE "/etc/bind/common_TTL_SOA_NS_CNAME_MX_for_cool_zones.db";
@	A

With those files in place, we don’t even need real files zone files for cool.tld. and super.tld, we could simply create symlinks from common_TTL_SOA_NS_CNAME_MX_A_for_cool_zones.db to cool.tld.db and super.tld.db and from common_TTL_SOA_NS_CNAME_MX_A_for_internal_cool_zones.db to internal_cool.tld.db and internal_super.tld.db . Now we can query the system for (cool.tld., SOA), (super.tld., SOA), (cool.tld., NS), (super.tld., NS), (, CNAME), (www.super.tld., CNAME), (cool.tld., MX), and (super.tld, MX).

More, zones!

If we want to add another domain name (new.tld.), it is a few simple steps

  1. Add it to zone_list_file
  2. Run make to regenerate zone_list.zones and internal_zone_list.zones
  3. Add a symlink from common_TTL_SOA_NS_CNAME_MX_A_for_cool_zones.db to new.tld.db and common_TTL_SOA_NS_CNAME_MX_A_for_internal_cool_zones.db to internal_new.tld.db
  4. Reload nameserver

More subdomains!

Time for another change, we want to add more subdomains to cool.tld, but not have them apply to super.tld. The cool.tld.db and internal_cool.tld.db zone files are thus now different, they can no longer be sym links, so we make real files

pics	CNAME	@
chat	CNAME	@


$INCLUDE "/etc/bind/common_TTL_SOA_NS_CNAME_MX_A_for_cool_zones.db";
$INCLUDE "/etc/bind/cool_extra_sub_domains.db";


$INCLUDE "/etc/bind/common_TTL_SOA_NS_CNAME_MX_A_for_internal_cool_zones.db";
$INCLUDE "/etc/bind/cool_extra_sub_domains.db";

As a result, we now have (, CNAME), and (, CNAME), and we have them in both the internal and external view of the cool.tld. zone. How much work is there if we want one more subdomain? Just add it to cool_extra_sub_domains.db and again, both zones with have it.

All files are available in my GitHub Gist