The purpose of this page is to document the process of adding additional layers of security to your DNS servers when performing zone transfers or AXFR requests.  Most internal environments don't apply any security to zone transfers.  In this article we'll use a fictitious firm by the name of The Acme Corporation.  Let's assume Acme Corporation operates three (3) separate DNS zones to manage resolution in each of these areas, and has a new mandate to create increasing levels of security in its Development (DEV), User Acceptance Testing (UAT), and Production (PROD) environments.  The following table summarizes the DNS security policies as they relate to zone transfers for our fictitious company: 

EnvironmentZoneSecurity LevelZone Transfer Policy
DEV dev.acme.com Low none
UAT uat.acme.com Medium IP-based ACL
PROD prod.acme.com High IP-based ACL w/ TSIG

Since the DEV environment has a low or non-existent security policy level, we'll start there.  On a BIND DNS server, the allow-transfer directive is used to control the zone transfer security policy. The allow-transfer directive specifies which host(s) may perfrom a zone transfer from the server.  The allow-transfer directive can be set in either the options, zone, or view blocks on the server.  When set at the zone block, it will override the options allow-transfer setting.  If not set at all, the default policy is to freely permt zone transfers.  For the purposes of our exercise, we'll only be concerned with the zonelevel setting for this directive, since we need granular control.  The syntax for this directive as as follows:

[ allow-transfer { address_match_list; }; ] 

The address_match_list allows many variations:

address_match_list = element ; [ element; ... ]

This address_match_list made up of one or more elements each of which has the following syntax:

element = [!] (ip [/prefix] | key key_id | "acl_name" | { address_match_list } )

The address_match_list has four (4) pre-defined key words that we can use:

  • "none" - no hosts match
  • "any" - all hosts match
  • "localhost" - any/all physical and/or logical interfaces defined and directly connected on the server will match.  The local loopback (127.0.0.1) will also match this list.
  • "localnets" - all directly connected physical and/or logical IP address(es) and/or subnetwork(s) the server is connected to will match.  This rule also includes the local loopback 127.0.0.1

So, for the purposes of our DEV environment, our configuration will be quite simple to implement using the allow-transfer directive, and it would look something like the following:

options { 
    // global options go here 
};

zone "dev.acme.com" in {
    type "master";
    file "db.dev.acme.com";
    allow-transfer { any; };
};

The zone block definition above for dev.acme.com will conform to our Low security level, and freely permit any host to perform zone transfers from our master name server. 

For our UAT environment, we have to increase the security policy to Medium, and apply an IP-based ACL to the allow-transfer directive on that zone.  To do that, we first need to create the ACL on our master name server.  An ACL is established by creating an acl clause usually at the top of our named.conf file. The acl clause syntax is as follows:

acl acl-name {
    address_match_list; 
};

The same rules for the address_match_list apply here.

Let's suppose that we want to allow three (3) name servers to pull down the uat.acme.com domain from our master at the following 32-bit addresses, 192.168.1.100, 192.168.2.100, and 192.168.3.100 respectively.  Our configuration on the master to support an IP-based ACL for performing zone transfers might look like the following:

acl uat-acl {
    192.168.1.100;
    192.168.2.100;
    192.168.3.100;
};

options { 
    //global options go here 
};

zone "uat.acme.com" in {
    type master;
    file "db.uat.acme.com";
    allow-transfer { "uat-acl"; };
};

This configuration will only allow those IP addresses in the ACL "uat-acl" to perform a zone transfer from our nameserver, satisfying our medium-level security policy.  In our production environment, we need stronger security to be applied to our zone transfer policy because we know that IP addresses can be spoofed with packet forging techniques and such.

In the production environment we'll not only use the IP-based ACL solution that we used in the UAT environment, but we'll configure and implement portions of DNSSEC using the Transaction SIGnature or TSIG using shared secret keys and one-way hashing to provide a cryptographically secure means of identifying each endpoint of a connection as being allowed to make or respond to a DNS request/update.  In our case, the TSIG keys will be used to ensure the identities of the ACL endpoints that we define in our IP-based ACL for the PROD environment.  Configuring TSIG to secure zone transfers is a bit more complex than straight IP-based ACLs, so in this section we'll cover the following:

  1. Use of the dnssec-keygen utility to create keys
  2. What to do with the DNSSEC keys that are generated
  3. Additions to the named.conf file
  4. Use of the dig utility when zone transfers are secured with TSIG 

Before we delve into the TSIG configuration, let's assume that we have three (3) nameservers that will require zone transfer capability in our PROD environment at the following IP addresses: 10.1.1.100, 10.2.2.100, and 10.3.3.100.  The first step would be to create a new ACL for the PROD environment.  It would look something like the following:

acl prod-acl {
    10.1.1.100;
    10.2.2.100;
    10.3.3.100;
};

Next, we need to generate some keys.  There are two basic strategies for generating keys.  One option is to generate one key pair for the master to share with the three slaves. Another option is to generate three separate key pairs, one pair for each master-slave server.  For the purposes of this document, we'll take the less complex route and create one key pair for all three remote servers to share with  our master name server.  To build the keys we need to use the dnssec-keygen tool.  The usage for this tool is as follows:

Usage:
    dnssec-keygen -a alg -b bits [-n type] [options] name Version: 9.5.0b2
Required options:
    -a algorithm: RSA | RSAMD5 | DH | DSA | RSASHA1 | HMAC-MD5 | HMAC-SHA1 | HMAC-SHA224 | HMAC-SHA256 |  HMAC-SHA384 | HMAC-SHA512
    -b key size, in bits:
        RSAMD5:         [512..4096]
        RSASHA1:                [512..4096]
        DH:             [128..4096]
        DSA:            [512..1024] and divisible by 64
        HMAC-MD5:       [1..512]
        HMAC-SHA1:      [1..160]
        HMAC-SHA224:    [1..224]
        HMAC-SHA256:    [1..256]
        HMAC-SHA384:    [1..384]
        HMAC-SHA512:    [1..512]
    -n nametype: ZONE | HOST | ENTITY | USER | OTHER
        (DNSKEY generation defaults to ZONE
    name: owner of the key
Other options:
    -c <class> (default: IN)
    -d <digest bits> (0 => max, default)
    -e use large exponent (RSAMD5/RSASHA1 only)
    -f keyflag: KSK
    -g <generator> use specified generator (DH only)
    -t <type>: AUTHCONF | NOAUTHCONF | NOAUTH | NOCONF (default: AUTHCONF)
    -p <protocol>: default: 3 [dnssec]
    -s <strength> strength value this key signs DNS records with (default: 0)
    -r <randomdev>: a file containing random data
    -v <verbose level>
    -k : generate a TYPE=KEY key
Output:
     K<name>+<alg>+<id>.key, K<name>+<alg>+<id>.private

Since we're building one key pair, we'd type something like the following to generate our keys:

dnssec-keygen -a HMAC-MD5 -b 128 -n HOST prod.tsigkey

This command would generate 128-bit host keys using the HMAC-MD5 algorithm named 'prod.acme.com'.  Two key files are generated with this command, a public key and a private key named kprod.tsigkey.+157+57861.key and kprod.tsigkey.+157+57861.private respectively.

NOTE: the "157" in the key name corresponds to the algorithm used in generating the key.  Additionally, the "57861" represents a finger print that is used in DNSSEC because multiple keys can be generated for a single zone.

The public key, kprod.tsigkey.+157+57861.key contains:

prod.tsigkey. IN KEY 512 3 157 wzs+I/9e6CthD0BT3Uh00w==

While the private key, Kprod.tsigkey.+157+57861.privatecontains:

Private-key-format: v1.2
Algorithm: 157 (HMAC_MD5)
Key: wzs+I/9e6CthD0BT3Uh00w==
Bits: AAA=

Now that we have generated the DNSSEC keys, we need to put them to use and incorporate them into our configuration.  The first step is to define the key on our master name server.   The key is defined on the master using the key clause.  The format of the keyclause is as follows:

key "key-id" {
    algorithm algorithm_type;
    secret "hashed_secret_of_chars";
};

The key-id is the name of the key you wish to use in referring to this "secret" hash.  In our case, the key-id is called "prod.tisigkey".  The algorithm-type is the type of algorithm used in creating the secret hash.  Currently under BIND 9.3, only hmac-md5 is valid.

Microsoft Windows software does not support TSIG via hmac-md5, rather Microsoft has implemented a different mechanism for authenticating servers using GSS-TSIG.  For this reason, it is not possible to configure a Windows Server running the Microsoft DNS service to perform zone transfers from a server running BIND DNS configured as a master authoritative server with TSIG protection on the allow-transfer directive.

The secret is nothing more than the cryptographic hash output from the dnssec-keygen tool.  This must be protected.  We have two ways of defining the key for our PROD domain.  We can create the key clause directly in the named.conf file of the server, OR we can create a separate file to hold all of our server keys, and use the include directive to "import" the data from this external file at load time.  The preferred method of doing this would be to use an external keys file so that the keys could be periodically updated, as well as, give us an opportuntiy to use file system privileges to secure the file.  With that said, our key clause for the PROD environment would be as follows:

key "prod.tisigkey" {
    algorithm hmac-md5;
    secret "wzs+I/9e6CthD0BT3Uh00w==";
};

We MUST also install or define the key clause on our remote nameservers as well for this to all work.  We do the same thing to our remotes as we did for our local master.  Simply embed the key clause directly in the named.conf OR even better, use the include directive to pull in a key chain file that has all pertinent keys defined. 

Now that we've told our master nameserver about the key, we need to instruct it when to use the key.  We need to have our master nameserver use the key when it communicates with its other authorized devices and servers.  Remember earlier, we had stated that there were three devices that needed the ability to perform zone transfers that included 10.1.1.100, 10.2.2.100, and 10.3.3.100 respectively.  We introduce another clause to our master's named.conf called server.  The server clause allows certain directives to be established on our master when interracting with remote servers defined by an IP Address (IPv4 or IPv6).  The format of the server clause is as follows:

server ip-addr {
    [ keys "key-name"; [ "key-name"; ...; ]
};

Note: there are many directives that are not shown here that can be configured in the server clause.  They are not discussed since they are outside of the scope of this document. Our named.conf on the master server we are configuring will need three (3) such server clauses defined, one for each remote server we want to grant access to for zone transfers.  Here is what our configuration would look like for the three servers:

server 10.1.1.100 {
    keys { "prod.tsigkey"; };
};

server 10.2.2.100 {
    keys { "prod.tsigkey"; };
};

server 10.3.3.100 {
    keys { "prod.tsigkey"; };
};

At this point we have built keys, we've told our local master server & all remote servers about this key, and we've also told our local master when to use this key.  The last step is to apply the TSIG key usage to the allow-transfer directive in the zoneclause for the prod.acme.com domain.  This is quite simply done as follows:

zone "prod.acme.com" in {
    type master;
    file "db.prod.acme.com";
    allow-transfer { key "prod.tsigkey"; };
};

Hey! What about also associating our IP-based ACL?  We need very special handling for that.  We cannot simply add the "prod-acl" along with the keydirective in the address_match_list, because the address_match_list will match permit the first match read left to right.  So, we need to add a new ACL that contains ALL hosts EXCEPT what is contained in the "prod-acl".  We can do that as follows: 

acl not-prod-acl {
    !prod-acl;
    any;
};

This gives us an ACL that contains all servers BUT the prod -acl servers.  But wait! When you combine that ACL with the key directive, any server that is in that ACL will now be able to perform zone transfers, and we DON'T want that!  To get around that we apply the ! or negation operator to the ACL.  Now, we can simply adjust the allow-transferdirective to also include the ACL as well.

allow-transfer { !not-prod-acl; key "prod.tsigkey"; };

Let's put it all together now.  The named.conf file of our master should look something like the following:

acl uat-acl { 
    192.168.1.100; 192.168.2.100; 192.168.3.100; 
};

acl prod-acl { 
    10.1.1.100; 10.2.2.100; 10.3.3.100; 
};

acl not-prod-acl {
    !prod-acl; any;
};

key "prod.tsigkey" {
 algorithm hmac-md5;
  secret "wzs+I/9e6CthD0BT3Uh00w==";
}; server 10.1.1.100 {
  keys { "prod.tsigkey"; };
}; server 10.2.2.100 {
  keys { "prod.tsigkey"; };
}; server 10.3.3.100 {
  keys { "prod.tsigkey"; };
}; options {
  allow-transfer { none; };
  directory "/var/named";
}; zone "." {
 type  hint;
 file "db.cache";
}; zone "localhost" {
 type master;
 file "db.localhost";
}; zone "0.0.127.in-addr.arpa" {
 type master;
 file "db.127.0.0.1";
}; zone "acme.com" in {
 type master;
 file "db.acme.com";
 allow-transfer { none; };
}; zone "dev.acme.com" in {
 type master;
 file "db.dev.acme.com";
 allow-transfer { any; };
}; zone "uat.acme.com" in {
 type master;
 file "db.uat.acme.com";
 allow-transfer { "uat-acl"; };
}; zone "prod.acme.com" in {
 type master;
 file "db.prod.acme.com";
 allow-transfer { !not-prod-acl; key "prod.tsigkey"; };
};

To test our configuration, we'll use the dig utility that is provided with the BIND DNS software.  With dig we can perform the equivalent of doing a zone transfer via the following command:

dig @server zone axfr
dig @127.0.0.1 dev.acme.com. axfr

Since there is no security applied to the allow-transferdirective of the dev.acme.com domain, we should expect the zone transfer to complete without any problems.  This assumes that our name server is properly configured.  The output after running that command is shown below:

$ dig @127.0.0.1 dev.acme.com. axfr

; <<>> DiG 9.5.0b2 <<>> @127.0.0.1 dev.acme.com. axfr
; (1 server found)
;; global options:  printcmd
dev.acme.com.           86400   IN      SOA     ns.acme.com. dnsadmin.acme.com.
1 10800 3600 604800 86400
dev.acme.com.           86400   IN      NS      ns.acme.com.
devhost1.dev.acme.com.  86400   IN      A       1.1.1.1
devhost2.dev.acme.com.  86400   IN      A       1.1.1.2
devhost3.dev.acme.com.  86400   IN      A       1.1.1.3
dev.acme.com.           86400   IN      SOA     ns.acme.com. dnsadmin.acme.com.
1 10800 3600 604800 86400
;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Mar 30 13:43:18 2008
;; XFR size: 6 records (messages 1, bytes 203)

When we run the same command against the uat.acme.com domain, we may or may not get the same results.  Remember, we applied an IP-Based ACL on the allow-transfer directive for that zone.  The zone transfer will fail for any hosts attempting to perform the zone transfer other than what is included in the "uat-acl" (192.168.1.100, 192.168.2.100, 192.168.3.100).  Otherwise, if our client is in the ACL we'd get the full results of the zone transfer back from the server.  What about the TSIG protected zone?  Let's assume that the host we are performing the dig command from is in the "prod-acl" and we issue the command to perform a zone transfer as shown above.  We should see something like the following:

$ dig @192.168.2.13 prod.acme.com. axfr

; <<>> DiG 9.5.0b2 <<>> @192.168.2.13 prod.acme.com. axfr
; (1 server found)
;; global options:  printcmd
; Transfer failed.

If we had enabled BIND DNS logging directives, we might see something like this in our named.log file:

client 10.1.1.100#2683: zone transfer 'prod.acme.com/AXFR/IN' denied

This shows that even though we are in the ACL for the prod-acl, we are unable to perform a zone transfer in the clear.  Luckily, the dig utility offers a couple of ways to perform the zone transfer, using either the -k or -y option to pass the secret from a file or via the command line. When using the -k option, you must have a valid key file created that the utility can read from. 

Using the -y option means you must pass the secret directly to the dig on the command line.  This has security implications, because the secret can be comprimised by users that may be on the system that the command is being run from.  The secret will be visible via the ps command.   Therefore, it is not recommended that the -y option be used.

Here is an example of passing the secret on the command line:

$ dig -y prod.tsigkey:wzs+I/9e6CthD0BT3Uh00w== @192.168.2.13 prod.acme.com. axfr

; <<>> DiG 9.5.0b2 <<>> -y prod.tsigkey @192.168.2.13 prod.acme.com. axfr
; (1 server found)
;; global options:  printcmd
prod.acme.com.          86400   IN      SOA     ns.acme.com. dnsadmin.acme.com.
1 10800 3600 604800 86400
prod.acme.com.          86400   IN      NS      ns.acme.com.
prodhost1.prod.acme.com. 86400  IN      A       3.1.1.1
prodhost2.prod.acme.com. 86400  IN      A       3.1.1.2
prodhost3.prod.acme.com. 86400  IN      A       3.1.1.3
prod.acme.com.          86400   IN      SOA     ns.acme.com. dnsadmin.acme.com.
1 10800 3600 604800 86400
prod.tsigkey.           0       ANY     TSIG    hmac-md5.sig-alg.reg.int. 120690
9637 300 16 8031GI1TxWeFD8D2jW2SQQ== 856 NOERROR 0
;; Query time: 0 msec
;; SERVER: 192.168.2.13#53(192.168.2.13)
;; WHEN: Sun Mar 30 14:40:37 2008
;; XFR size: 6 records (messages 1, bytes 289)

Likewise, we could perform the same query using the -k option and passing the key file names.  What's odd is that both the public and private key must reside in the same directory when calling them from dig.  Here is the usage and output from the dig -k command:

$ dig -k Kprod.tsigkey.+157+57861.key @192.168.2.13 prod.acme.com. axfr

; <<>> DiG 9.5.0b2 <<>> -k Kprod.tsigkey.+157+57861.key @192.168.2.13 prod.acme.com
. axfr
; (1 server found)
;; global options:  printcmd
prod.acme.com.          86400   IN      SOA     ns.acme.com. dnsadmin.acme.com.
1 10800 3600 604800 86400
prod.acme.com.          86400   IN      NS      ns.acme.com.
prodhost1.prod.acme.com. 86400  IN      A       3.1.1.1
prodhost2.prod.acme.com. 86400  IN      A       3.1.1.2
prodhost3.prod.acme.com. 86400  IN      A       3.1.1.3
prod.acme.com.          86400   IN      SOA     ns.acme.com. dnsadmin.acme.com.
1 10800 3600 604800 86400
prod.tsigkey.           0       ANY     TSIG    hmac-md5.sig-alg.reg.int. 120691
0850 300 16 tVq04sl1f9YKgtYB2De8AA== 1559 NOERROR 0
;; Query time: 0 msec
;; SERVER: 192.168.2.13#53(192.168.2.13)
;; WHEN: Sun Mar 30 15:00:50 2008
;; XFR size: 6 records (messages 1, bytes 289)

Comments   

0 #1 Guillermo Osorio 2016-07-26 10:41
Very useful! Thanks a lot! :lol:
Quote

Add comment


Security code
Refresh