A short introduction to Catalog Zones
  • 11 May 2022
  • 7 Minutes to read
  • Contributors
  • Dark
    Light
  • PDF

A short introduction to Catalog Zones

  • Dark
    Light
  • PDF

Article Summary

Catalog Zones is a BIND feature allowing easy provisioning of zones to secondary servers. A "catalog zone" is a special DNS zone that contains a list of other zones to be served, along with their configuration parameters.  The zones listed in a catalog zone are called "member zones".  When a catalog zone is loaded or transferred to a secondary server that supports this functionality, the secondary server creates the member zones automatically.  When the catalog zone is updated (for example, to add or delete member zones, or change their configuration parameters) those changes are immediately put into effect. Because the catalog zone is a normal DNS zone, these configuration changes can be propagated using the standard AXFR/IXFR zone transfer mechanism.

This guide shows the basic usage of catalog zones - how to set up a primary and a secondary provisioned using a catalog zone, how to add a new zone to the catalog zone, and how to possibly automate it. In this guide we'll be using three servers - a primary running on 10.53.0.1 and two secondaries running on 10.53.0.2 and 10.53.0.3. To make it easier to try out this example on your own system, we are using unprivileged ports 5300 and 9953, for DNS and RNDC respectively.

495c35f4-d81a-4ea2-8783-c8940da143e4.jpeg

First we create an empty catalog zone named "catalog.example". It has to have NS and SOA records (just like a regular zone) and an IN TXT record which tells the server what version of the catalog zone syntax this zone uses - all released BIND versions starting from 9.11.0 support version "1", and starting from version 9.18.3 BIND also supports version "2". Please refer to the Administrator Reference Manual for the information about the differences between them.

; catalog.example.db
catalog.example. IN SOA . . 1 86400 3600 86400 3600
catalog.example. IN NS invalid.
version IN TXT "1"
Attention

As documented in the ARM, a supported version record is required for a valid catalog zone, but the restriction wasn't being enforced until BIND 9.18.3, starting from which BIND will refuse to load a catalog zone without a supported version record.

For the primary server we have to enable "allow-new-zones" and RNDC access, and we also have to serve the catalog zone for secondaries:

; named-ns1.conf
key rndc_key { secret "1234abcd8765"; algorithm hmac-sha256; };

controls { inet 10.53.0.1 port 9953 allow { any; } keys { rndc_key; }; };

options {
          query-source address 10.53.0.1;
          notify-source 10.53.0.1;
          transfer-source 10.53.0.1;
          port 5300;
          allow-new-zones yes;
          pid-file "named.pid";
          listen-on { 10.53.0.1; };
          listen-on-v6 { none; };
          notify no;
          recursion no;
          allow-transfer { any; };
};

zone "catalog.example" {
          type primary;
          file "catalog.example.db";
          allow-transfer { any; };
          allow-update { any; };
          also-notify { 10.53.0.2; 10.53.0.3; };
          notify explicit;
};

For both secondary servers we need to receive the catalog.example zone from the primary and enable it as a catalog zone.  The configuration for ns3 is identical to ns2 except all 10.53.0.2 addresses are replaced with 10.53.0.3:

; named-ns2.conf
options {
          query-source address 10.53.0.2;
          notify-source 10.53.0.2;
          transfer-source 10.53.0.2;
          port 5300;
          pid-file "named.pid";
          listen-on { 10.53.0.2; };
          listen-on-v6 { none; };
          notify no;
          recursion no;
          catalog-zones {
               zone "catalog.example" default-masters { 10.53.0.1; };
          };
};

zone "catalog.example" {
          type secondary;
          file "catalog.example.db";
          masters { 10.53.0.1; };
};
; named-ns3.conf
options {
          query-source address 10.53.0.3;
          notify-source 10.53.0.3;
          transfer-source 10.53.0.3;
          port 5300;
          pid-file "named.pid";
          listen-on { 10.53.0.3; };
          listen-on-v6 { none; };
          notify no;
          recursion no;
          catalog-zones {
               zone "catalog.example" default-masters { 10.53.0.1; };
          };
};

zone "catalog.example" {
          type secondary;
          file "catalog.example.db";
          masters { 10.53.0.1; };
};

We also need to setup "rndc.conf" with our key:

; rndc.conf
key rndc_key { secret "1234abcd8765"; algorithm hmac-sha256; };
Please remember that this is just an example configuration

In the real world you should never allow updates to everyone. Zone transfers should be protected with TSIG and catalog zones should not be open for queries.

We then launch ns1 and ns2 (in separate directories) - we'll leave ns3 for later. ns2 should download the now empty "catalog.example" zone and we should be able to query it:

$ dig +short @10.53.0.2 -p 5300 soa catalog.example
. . 1 86400 3600 86400 3600

Although leaving the catalog zone open for queries is not recommended, in our case it'll be useful for debugging.

To add a zone we first need to create a stub primary file:

; example.com.db
example.com. 3600 IN SOA . . 1 3600 3600 3600 3600
example.com. IN NS ns1.isc.org.
example.com. IN NS ns2.isc.org.

and then add the zone to primary using rndc:

rndc -k rndc.conf -y rndc_key -s 10.53.0.1 -p 9953 addzone example.com '{type primary; file "example.com.db";};'

The zone is now served by ns1:

$ dig +short @10.53.0.1 -p 5300 soa example.com
. . 1 3600 3600 3600 3600

To provision the zone on secondary we have to add it to the catalog zone; for now we'll do it using nsupdate. The long label (c5e4...) is the hex digest of the SHA1 hash of the zone name ("example.com") in wire format.  The method of calculating it is shown below in the catz-add.py script:

$ cat << __EOF | nsupdate
server 10.53.0.1 5300
update add c5e4b4da1e5a620ddaa3635e55c3732a5b49c7f4.zones.catalog.example 3600 IN PTR example.com
send
__EOF
Member zone label

Using the mentioned SHA1 hash for a label is not strictly required, as you can use any other unique label instead.

Reusing a member zone label

Reusing a member zone label (when a member zone contains more than one PTR record) is strongly discouraged as it will make older BIND versions use only one of the records, while silently ignoring the others, and starting from version 9.18.3, BIND will refuse to load such catalog zones.

The new version of catalog.example zone is transferred to ns2 and a moment later ns2 should transfer and serve example.com:

$ dig +short @10.53.0.2 -p 5300 soa example.com
. . 1 3600 3600 3600 3600

Obviously in a real-life scenario this should be automated. Using dnspython and the isc.rndc module we can write a simple Python script that does everything we did automatically:

#!/usr/bin/python
# catz-add.py
import sys
import os
import isc
import dns.query
import dns.update
import dns.name
import hashlib

ZONEPATH='/tmp/'
MASTER='10.53.0.1'
DNSPORT=5300
RNDCPORT=9953
RNDCALGO='sha256'
RNDCKEY='1234abcd8765'
CATZONE='catalog.example'

def add_zone(name):
   # Create a stub primary file
   with file('%s%s.db' % (ZONEPATH, name), 'w') as f:
        f.write('@ 3600 IN SOA . . 1 3600 3600 3600 3600\n')    
        f.write('@ IN NS ns1.isc.org.\n')
        f.write('@ IN NS ns2.isc.org.\n')
        
       # Add zone to primary using RNDC
   r = isc.rndc((MASTER, RNDCPORT), RNDCALGO, RNDCKEY)  
   response = r.call('addzone %s {type primary; file "%s%s.db";};' % (name, ZONEPATH, name))
   if response['result'] != '0':
        raise Exception("Error adding zone to primary: %s" % response['err'])
       
       # Update catalog zone
   update = dns.update.Update(CATZONE)
   hash = hashlib.sha1(dns.name.from_text(name).to_wire()).hexdigest()
   update.add('%s.zones' % hash, 3600, 'ptr', '%s.' % name)  
   response = dns.query.tcp(update, MASTER, port=DNSPORT)  
   if response.rcode() != 0:
        raise Exception("Error updating catalog zone: %d" % response.rcode())

add_zone(sys.argv[1])

We can use this script to add a new zone:

python ./catz-add.py example2.com

Which will be immediately served by both primary and secondary:

$ dig +short @10.53.0.1 -p 5300 soa example2.com
. . 1 3600 3600 3600 3600
$ dig +short @10.53.0.2 -p 5300 soa example2.com
. . 1 3600 3600 3600 3600

We can obviously also delete zones; here's an example script for this purpose:

#!/usr/bin/python
# catz-del.py
import sys
import os
import isc
import dns.query
import dns.update
import dns.name
import hashlib

ZONEPATH='/tmp/'
MASTER='10.53.0.1'
DNSPORT=5300
RNDCPORT=9953
RNDCALGO='sha256'
RNDCKEY='1234abcd8765'
CATZONE='catalog.example'

def del_zone(name):
     # Update catalog zone
     update = dns.update.Update(CATZONE)
     hash = hashlib.sha1(dns.name.from_text(name).to_wire()).hexdigest()
     update.delete('%s.zones' % hash)
     response = dns.query.tcp(update, MASTER, port=DNSPORT)
     if response.rcode() != 0:     
          raise Exception("Error updating catalog zone: %d" % response.rcode())
     
     # Delete zone from primary using RNDC
     r = isc.rndc((MASTER, DNSPORT), RNDCALGO, RNDCKEY)
     response = r.call('delzone %s' % name)
     if response['result'] != '0':     
          raise Exception("Error deleting zone from primary: %s" % response['err'])
     
     # Delete zone file
     os.unlink('%s%s.db' % (ZONEPATH, name))  

del_zone(sys.argv[1]) 

What was shown above isn't anything amazing and could be easily achieved by simply adding the zones to the secondary using rndc addzone. The advantage of Catalog Zones is that no matter how many secondaries you have, the zones catalog is kept in one central place - in a catalog zone, on the primary server. If we now launch ns3 it will download this configured catalog zone and immediately add the previously configured example.com and example2.com zones and start serving them:

$ dig +short @10.53.0.3 -p 5300 soa example.com
. . 1 3600 3600 3600 3600
$ dig +short @10.53.0.3 -p 5300 soa example2.com
. . 1 3600 3600 3600 3600