Dynamic DNS with the bind DNS server



The popular DNS server bind allows to have a configuration that enables clients to change DNS entries remotely. Since some of the public dynamic DNS services have moved to a pay-only subscription model and since I'm running my own mail and web server at a hosting site I was searching for a way to roll my own dynamic DNS service. This already is back some years now but since the Howto I used at the time seems to be gone (at least from my google bubble) I'm documenting here how it was done.

I'm running this on a Debian buster server at the time of this writing, so if you're on a different system some details may change. I'm calling the domain for the dynamic services dyn.example.com in the following.

The top-level config file of bind needs to include an additional config file for the dynamic domain. In my configuration this file is named.conf.handedited. In this file you need an entry for each dynamic DNS client as follows:

zone "dyn.example.com" {
        type master;
        allow-transfer {none;};
        file "/etc/bind/slave/pri.dyn.example.com";
        update-policy {
            grant h1.dyn.example.com. name h1.dyn.example.com. A TXT;
            grant h2.dyn.example.com. name h2.dyn.example.com. A TXT;
            [...]
        };
};

In this example the hosts h1 and h2 and possibly more may edit their own DNS entry. I'm allowing them to change their A and TXT records. You may want to add AAAA for IPv6.

Then the config-file /etc/bind/slave/pri.dyn.example.com contains:

dyn.example.com         IN SOA  ns1.example.com. admin.example.com. (
                                2020080100 ; serial
                                120      ; refresh (2 minutes)
                                120      ; retry (2 minutes)
                                120      ; expire (2 minutes)
                                120      ; minimum (2 minutes)
                                )
                        NS      ns1.example.com.
h1                      A       127.0.0.1
                        KEY     512 3 10 (
                                AwEAAdEvnGmGO4ku+xms4w1c5RWh5BvugiZ4ty9tkIes
                                <more gibberish lines>
                                ); alg = RSASHA512 ; key id = <number>

The values in angle brackets are comments and should be replaced by the correct values in your installation. The entries A and KEY are inserted by hand for each new host allowed to set its own IP address. The KEY is the public key created below. In my experience an A-record has to be present for it to work, I'm setting the localhost address here because the client will later rewrite this IP anyway. It's customary to have the admin email address (where the @ is replaced with a dot) in the SOA record where I've put admin.example.com..

To create a new host:

  • Create a new public/private key pair (preferrably the client does that and sends only the public key to the DNS admin for security reasons):

    dnssec-keygen -T key -a RSASHA512 -b 2048 -n HOST newhost.dyn.example.com
    
  • This creates a private and a public key. Note that on the client you need both, the public and the private key although in the command line for the dynamic DNS client you will only specify the private key!

  • Last time I created a new key the command did not support keys longer than 2048 bit although the hash algorithm is SHA2 with a high bit-length.

  • You need to freeze (and make bind write the current in-memory DB to a file) bind for your dynamic domain:

    rndc freeze dyn.example.com
    
  • Now you may edit the config file, you want to add a stanza for the new host and increment the serial number:

    $EDITOR /etc/bind/slave/pri.dyn.example.com
    
  • Then don't forget to thaw the domain:

    rndc unfreeze dyn.example.com
    
  • Do not forget to give the new host the necessary permissions in named.conf.handedited

  • You probably need to reload bind:

    systemctl reload bind9.service
    

On the client side the utility we use to tell bind about a new IP address of our client is called nsupdate. You can probably find this program for many client operating systems.

I'm using a simple script that detects a change of a dynamic IP address and performs a bind update in case the address changed. Since you're running your own DNS server, chances are that you also have a webserver at your disposal. The following simple script allows any client to detect its own IP-address (clients are often behind a NAT firewall and we don't want to use another public service when we just got rid of public dynamic DNS services, right?):

#!/bin/sh
echo Content-Type: text/plain
echo ""
echo $REMOTE_ADDR

This script is put into a cgi-bin directory of a web server and will echo the client IP address (in text form) back to the client. I name this script ip.cgi and it is available via the URL http://example.com/cgi-bin/ip.cgi in our example, see below in the client script where you need to change that URL.

My bind update script (you need to change some variables) looks as follows (note that this asumes the script above runs on the top-level domain example.com, otherwise change the URL of the cgi-bin program):

#!/bin/sh
ZONE=dyn.example.com
DOMAIN=h1.dyn.example.com
DNSKEY=/etc/nsupdate/Kh1.dyn.example.com.+010+04711.private
NS="ns1.example.com"

registered=$(host $DOMAIN $NS 2> /dev/null | grep 'has address' | tail -n1 | cut -d' '  -f4)
current=$(wget -q -O- http://example.com/cgi-bin/ip.cgi)
[ -n "$current" \
-a "(" "$current" != "$registered" ")" \
] && {
 nsupdate -d -v -k $DNSKEY << EOF
server $NS
zone $ZONE
update delete $DOMAIN A
update add $DOMAIN 60 A $current
send
EOF
 logger -t dyndns -p daemon.notice "Updated dyndns: $registered -> $current"
} > /dev/null 2>&1

exit 0

It should be noted again that the private key (in /etc/nsupdate/Kh1.dyn.example.com.+010+04711.private in the example above) is not enough, nsupdate also needs the public key in the same directory as the private key.