Dynamic DNS with bind and nsupdate

March 24, 2017
Last modified October 8, 2019

I have a home server with a dynamic IP-address. Previously I used an Asus router which comes with automatic dynamic DNS. This worked fine, but I kind of wanted to do this myself. So, I did it using bind and nsupdate.

I have a domain I use for my LAN, domain.tld, and ext.domain.tld points to my public IP-address. I host my DNS with Underworld, and I don’t have rights to use nsupdate directly with them. So, I’ll set up a DNS-server on one of my servers, and let that server resolve lookups for the subdomain ext.domain.tld, and continue to let Underworld serve domain.tld.

In this setup Underworld will be “super-master”, cookie (my main server) will be master, and mrslave will be the client (or, if you will, slave). Both cookie and mrslave runs Ubuntu 16.04, but the configuration should be pretty similar for older and newer versions of both Ubuntu and Debian.

Creating a key-pair

To create a key-pair, we’ll be using dnssec-keygen.

dennis@cookie:/tmp$ dnssec-keygen -a HMAC-SHA512 -b 512 -n USER mrslave.domain.tld.

This will give you two files. Kmrslave.domain.tld.+165+11930.private and Kmrslave.domain.tld.+165+11930.key. the .key-file will contain your public key, and look like something like this:

mrslave.domain.tld. IN KEY 0 3 165 iYUzeD93iVtpXFik/6vH8TVnOUfo27k5a2gS4SYBXTQaSJUE/A7KhzXn nrFP2LeJ6nm9mfAA3cjzBGXV6yv9gA==

And the .private-file will look like something like this:

Private-key-format: v1.3
Algorithm: 165 (HMAC_SHA512)
Bits: AAA=
Created: 20170324142537
Publish: 20170324142537
Activate: 20170324142537

As you might have noticed, both keys are the same, except for a space in the .key-file. You’ll also notice that the Private-key-format is v1.3. With v1.2, we needed the .private-file, but with v.1.3 we no longer need it. We only need the key from the .key-file, so you can just delete the .private-file, if you want to.

Installing and configuring bind9 on master

First things first. Let’s install bind9 on the master server, and set it up so that our server can answer requests for our subdomain.

dennis@cookie:~$ sudo apt install bind9 bind9utils bind9-doc

Now that bind is installed, we will have to configure it.


Let’s start by creating a configuration-file to put keys in. Create /etc/bind/keys.conf, and include it in named.conf by adding the following line to the bottom on named.conf:

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

Now we’ll put the key we created (from the .key-file, with the extra space) earlier into keys.conf.

key mrslave.domain.tld. {
    algorithm HMAC-SHA512;
    secret "iYUzeD93iVtpXFik/6vH8TVnOUfo27k5a2gS4SYBXTQaSJUE/A7KhzXn nrFP2LeJ6nm9mfAA3cjzBGXV6yv9gA=="

And then wee need to make sure the file is safe from prying eyes, and that bind can read the file.

dennis@cookie:~$ sudo chmod o-x /etc/bind/keys.conf
dennis@cookie:~$ sudo chgrp bind /etc/bind/keys.conf


Add these two lines below directory "/var/cache/bind"; in /etc/bind9/named.conf.options:

    recursion no;
    allow-transfer { none; };

The file should then look something like this:

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

    recursion no;
    allow-transfer { none; };

    dnssec-validation auto;

    auth-nxdomain no;    # conform to RFC1035
    listen-on-v6 { any; };


Now we have to add our zone to named.conf.local, and create the zone-file. Put the following into your named.conf.local:

zone "ext.domain.tld" {
    type master;
    file "/etc/bind/pz/ext.domain.tld";
    allow-update {
        key mrslave.domain.tld.;

Using allow-update here will allow the key mrslave.domain.tld full access to the zone ext.domain.tld. If you want more fine-grained policies, you can use update-policy.

update-policy {
    grant <key> <type> <zone> <record-types>;

So, if you only want to allow the key mrslave.domain.tld to update the A-record of ext.domain.tld, the file would look something like this:

zone "ext.domain.tld" {
    type master;
    file "/etc/bind/pz/ext.domain.tld";
    update-policy {
        grant mrslave.domain.tld. name ext.domain.tld. A;


If there are no errors, we can create our zone-file. We have to create the directory /etc/bind/pz, give bind permission to write to it, and place the zone ext.domain.tld there.

dennis@cookie:~$ sudo mkdir /etc/bind/pz
dennis@cookie:~$ sudo chgrp bind /etc/bind/pz
dennis@cookie:~$ sudo chmod g+w /etc/bind/pz

This is what my zone-file, ext.domain.tld looks like:

$TTL    604800
@       IN      SOA     cookie.eriksen.im. webmaster.eriksen.im. (
                     2017032402         ; Serial
                         604000         ; 
                          86400         ; Retry
                        2419200         ; Expire
                         604000 )       ; Negative Cache TTL
@       IN      NS      cookie.eriksen.im.

@  600  IN      A       ; TTL=600s


With this setup, when you start using nsupdate, apparmor will start complaining with the following error:

Mar 25 17:21:46 cookie kernel: [4089644.355272] audit: type=1400 audit(1490458906.635:17): apparmor="DENIED" operation="mknod" profile="/usr/sbin/named" name="/etc/bind/pz/ext.domain.tld.jnl" pid=27119 comm="named" requested_mask="c" denied_mask="c" fsuid=131 ouid=131

In order to fix this, we have to give named (bind) permission to write to the directory /etc/bind/pz/. We do this by inserting the line /etc/bind/pz/ rw, right below /etc/bind/** r, in the file /etc/apparmor.d/usr.sbin.named. If you’ve never edited this file, lines 19-24 should probably look something like this:

  /etc/bind/** r,
  /etc/bind/pz/* rw,
  /var/lib/bind/** rw,
  /var/lib/bind/ rw,
  /var/cache/bind/** lrw,
  /var/cache/bind/ rw,

And then we reload apparmor.

dennis@cookie:~$ sudo systemctl reload apparmor.service


We can now check the configuration using check-nameconf.

dennis@cookie:~$ check-nameconf

This will check named.conf.options, named.local, named.conf.default-zones, and named.conf, as the three first are included in named.conf.

We’ll use named-checkzone to test the zone-file.

dennis@cookie:~$ sudo named-checkzone ext.domain.tld /etc/bind/pz/ext.domain.tld
zone ext.domain.tld/IN: loaded serial 2017032402

Alright. Now we’ve configured bind, and set up the zone. Now it’s time to restart bind, and see if it’s working. Do a systemctl restart bind9, and see if it’s working by testing it with dig. It should look something like this:

dennis@cookie:~$ sudo systemctl restart bind9
dennis@cookie:~$ dig ext.domain.tld +short

Alright! We’ve got a working master DNS-server!

Setting up the client

First, we need to install nsupdate. nsupdate is part of the package dnsutils, so we’ll install that.

dennis@mrslave:~$ sudo apt install dnsutils

“Configuring” nsupdate

When using nsupdate, we’ll need a key-file. As I mentioned earlier, the .private-file was needed when we were using Private-key-format v1.2. Now the key-file must be presented in bind-format. So, we can just copy /etc/bind/keys.conf from earlier!

key mrslave.domain.tld. {
   algorithm HMAC-SHA512;
   secret "iYUzeD93iVtpXFik/6vH8TVnOUfo27k5a2gS4SYBXTQaSJUE/A7KhzXn nrFP2LeJ6nm9mfAA3cjzBGXV6yv9gA=="

Now that we’ve got the key-file, we can create a text-file containing the update-commands we want to send. Let’s call ut nsupdate.txt, and it should look like this:

server cookie.eriksen.im
zone ext.domain.tld
update delete ext.domain.tld. A
update add ext.domain.tld. 600 A


Now we can try to update DNS!

dennis@mrslave:~$ nsupdate -k mrslave.conf -v nsupdate.txt
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:      0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;ext.domain.tld.                        IN      SOA

ext.domain.tld.         0       ANY     A
ext.domain.tld.         600     IN      A

The update was successfull. Now we can test to see if it actually works.

dennis@mrslave:~$ dig ext.domain.tld +short

Earlier, when creating /etc/bind/pz/ext.domain.tld on the master, we set the IP-address to, so getting in response now means that it worked.

It worked!

Automating DNS-updates

As I mentioned earlier, this is my home-server, and I want it to update the DNS-record every time its IP-address updates. So, I have written a script that checks wether my public IP-address has changed, and then issues an update if it has.

I’ve uploaded the script to my server, so feel free to use it. Installing it is quite straight-forward. Put nsupdate.sh in /usr/local/sbin/ and configure it (you only need to change the top four variables), and install the cron-file.