How To Configure A Bind9 Container To Support Dynamic Updates

May 7, 2025 · 23 mins read
How To Configure A Bind9 Container To Support Dynamic Updates

In the video below, we show you how to configure a Bind9 Docker image to support Dynamic DNS


Dynamic Updates for DNS servers like Bind are very useful, particularly when you use DHCP because it allows you to access a device via its hostname and it makes no difference if the IP address changes

And it has uses beyond that as I found when deploying my own ACME server

Now I did post a video about how to configure Dynamic DNS for Bind for a stand alone server some time ago, but it’s a bit more tricky when you’re running Bind as a container because of file permissions

Useful links:
https://Bind.readthedocs.io/en/stable/chapter6.html#dynamic-update
https://Bind.readthedocs.io/en/stable/reference.html#namedconf-statement-update-policy

Assumptions:
Now because this video is specifically about configuring Dynamic Updates in Bind I’m going to assume you already have Bind running in a container

If not then I do have another video which shows you to create your own Docker image to run Bind in a container

What I won’t be covering, however, is how to configure an end device like a DHCP server because it depends on what that end device is

But I do have another video on how to configure Kea to support Dynamic DNS for instance

Zone File Changes:
For a long long time, DNS has involved just static zone files

The IP addressing and hostname rarely changed, so you configured a file which contains the DNS records and you gave the Bind user read access to them

The idea is, if the DNS service is ever compromised, the attacker can’t modify your DNS entries through that Bind user account

For operating systems like Debian, zone files are typically found in the /etc/bind folder

There, only root or someone with elevated privileges can make changes to a file

docker exec bind9 ls -l /etc | grep bind

But for Dynamic DNS to work, the Bind user needs to make changes to the zone file as well as to create and edit a journal file in the same folder

For that, it makes sense to use the /var/lib/bind folder where the Bind user already has write access

docker exec bind9 ls -l /var/lib | grep bind

Because if we allow write access in /etc/bind, the Bind user could potentially be able to modify the domains hosted on the server as well

I’m already logged in as the Docker user that manages containers

So first we’ll create a folder on the host so zone files can survive a container restart for instance

mkdir -p bind9/var/lib/bind

For me, this is in the user’s home directory and config and zone files for Bind are normally in bind9/etc/bind

So if you use different folders you’ll need to modify the commands being used

Now try as I might, I couldn’t come up with an easy and practical way to keep dynamic and static records separate

I don’t want to use subdomains because I don’t want a DNS server called ns1.homelab.lan and a web server called www.dynamic.homelab.lan for instance

But for security reasons, I don’t want Bind to be able to replace static host records

I spent a really long time looking into this, even getting feedback from the likes of ChatGPT and Gemini

But the problem was they kept coming up with solutions that didn’t work and it was only after mentioning that would they then explain why it didn’t work

It really makes you wonder what these things know, because if you know a solution won’t work, why suggest it in the first place?

In any case, based on the feedback, I’m going to have to stick with a single zone file for now, and at a later date, work on a way to get Ansible to update and maintain static entries in a different way

This is the same method I’ve been using with a standalone Bind server, but later we’ll need to address the permissions problems

First, we’ll move the original files

mv bind9/etc/bind/db.homelab.lan bind9/var/lib/bind
mv bind9/etc/bind/db.192.168 bind9/var/lib/bind

Then we’ll update the configuration file to point to the new locations

nano bind9/etc/bind/named.conf.local

So now it looks like this

zone "homelab.lan"  {
	type master;
	file "/var/lib/bind/db.homelab.lan";
};

zone "168.192.in-addr.arpa" {
	type master;
	file "/var/lib/bind/db.192.168";
};

Now save and exit

Next, the volume mappings need to change and for me that means updating the compose file, but we’ll take a backup first

cp docker-compose.yml docker-compose.yml.bak
nano docker-compose.yml

So now the volume mappings looks like this

    volumes:
      - ./bind9/etc/bind/named.conf.local:/etc/bind/named.conf.local:ro
      - ./bind9/etc/bind/named.conf.options:/etc/bind/named.conf.options:ro
      - ./bind9/etc/resolv.conf:/etc/resolv.conf:ro
      - ./bind9/var/lib/bind:/var/lib/bind

Now save and exit

The reason I’m mapping individual files that belong in /etc/bind is there are other files in the folder that I don’t want to manage

If the developers of Bind ever added a new file to /etc/bind or existing ones changed due to an update, I don’t need to concern myself with those because they’ll still be there

However, if I mapped the entire /etc/bind folder instead, a file like named.conf for instance would no longer be seen by the container if it doesn’t exist on the host and then Bind wouldn’t work

Now, because the Bind user will later need write access to the /var/lib/bind folder, it’s easier to map the whole folder as there’s nothing there to begin with

This is a major change, so we’ll stop and remove the container

docker compose down bind9

Then we’ll start it back up afresh

docker compose up -d

We’ll then check the container is still working

docker ps -l

Assuming Bind is up and running, we’ll check to make sure that DNS resolution is working

host ca.homelab.lan
host 192.168.102.12

New Container Image:
A big problem I’ve run into with containers like this is file permissions

We want files to persist a container restart, but the user inside the container needs write access

In this case, we’ve got a problem with the /var/lib/bind folder and the files within it

What we’ve done is to map the folder from the host to the container, and now the ownership of /var/lib/bind within the container has been overwritten

docker exec bind9 ls -l /var/lib | grep bind

What we’d see is the UID and GID of our host user which doesn’t exist in this container

More importantly though, Bind currently doesn’t have write access to this folder

There are various ways to deal with this, and up till now I’ve been aligning the UID and GID of the host and container users to resolve this

I’d prefer to have tighter security and I wasn’t having any success with using a shared group but I didn’t like this ID mapping strategy

So I had a conversation with ChatGPT to find a solution

First, we’ll create a new folder

mkdir images/bind9
cd images/bind9

Since I’ve been creating my own Bind image, this is the revised Dockerfile for this

nano dockerfile

FROM debian:latest

# Arguments for user and group names, as well as user and group IDs
ARG USER_NAME=binduser
ARG GROUP_NAME=bindgroup
ARG USER_ID=5001
ARG GROUP_ID=5001

# Install bind9 and acl
RUN apt update && \
    apt install -y bind9 acl && \
    apt clean && rm -rf /var/lib/apt/lists/*

# Create shared group and user for bind
RUN groupadd --gid ${GROUP_ID} ${GROUP_NAME} && \
    useradd --uid ${USER_ID} --gid ${GROUP_ID} \
            --home /nonexistent --shell /usr/sbin/nologin ${USER_NAME}

# Modify ownership and permissions for the new user account and group
RUN chown -R root:${GROUP_NAME} /var/cache/bind && \
    chmod -R 2770 /var/cache/bind && \
    chown -R root:${GROUP_NAME} /etc/bind && \
    find /etc/bind -type f -exec chmod 640 {} \; && \
    find /etc/bind -type d -exec chmod 2750 {} \;

# Set default ACLs so new files are always group writable
RUN setfacl -d -m g:bindgroup:rw /var/cache/bind

# Switch to non-root user
USER ${USER_NAME}

# Start named with umask enforced
CMD ["sh", "-c", "umask 002 && exec /usr/sbin/named -f"]

Now save and exit

We’ll leave the existing Bind user account and group alone and instead create our own

To make it easier to change these, we’ll define arguments for them

I’d prefer to avoid a clash of the UID and GID now and in the future, so we’ll define high numbers to assign to use

Then we’ll install the bind9 and acl packages

What’s interesting is that ChatGPT added an extra line to remove files in /var/lib/apt/lists/ in order to shrink the image size

As they say, you learn something new every day

Next we create a new group and user account to run Bind, but it doesn’t need a home folder and it doesn’t need to login via SSH

We then need to change the ownership of folders to match these

There was a lot of to-ing and fro-ing around permissions and what was actually necessary to change

For example, /var/lib/bind was in the proposed dockerfile I was given, but since it’s mapped through by Docker it wouldn’t achieve anything so I had it removed

The main problem I ran into in my earlier testing was file permissions weren’t working the way I’d intended for a shared group

What was suggested by ChatGPT was the use of chmod -R 2770, so that new files will inherit the group permissions of the folder because the setgid bit is set to 2

The setafcl command though is an extra step to set a default ACL for the folder to ensure any new file created in the folder has the correct group permissions

We want to avoid root access as much as possible so we’ll switch to the new user account

Even an interactive session will then have limited access

When the container does run, we want it to run in the foreground, to keep the container running

TIP: A container will simply run and then stop if you start a service or run a command in the background

One thing to note is the use of umask 002 which is to ensure that when Bind does run, new files are written with the correction permissions

Again, this is part of the reason why I had problems trying to get a shared group working myself

Without these extra changes, of setgid and umask, files were being given permissions of the user’s primary group which in turn had only read access

I now need to check what the next version should be

docker image ls | grep bind9

We’ll then create a new version of the image

docker build --network=host --no-cache -t bind9_image:0.3 .

TIP: It would be tempting to just build a newer latest version but if this doesn’t work you may find yourself without a working DNS server and then you may not be able to fix the problem because DNS is down and so you can’t rebuild the image

I’ve used the –network=host parameter as I want Docker to use internal DNS and not the default of Google’s DNS servers

In addition, I’ve also used –no-cache as sometimes caching causes issues

Once the build is complete, we’ll update the compose file to use this newer version

cd
nano docker-compose.yml

...
  bind9:
#    image: bind9_image:latest
    image: bind9_image:0.3

...

Now save and exit

I’ve commented out the latest version to make it a bit easier to swap between images during the testing phase

Host Permissions:
For this to actually work, we need to make some changes to file ownership and permissions on the host computer so I’ll switch to a user with sudo rights

This is because the user account I use with Docker was made a member of the docker group so that I don’t have to keep using sudo

Although granted, non-root accounts make more sense when you’re using Podman

In any case, we’ll create a new group on the host to match the one we created in the container, set the permissions and add the Docker user to the group

sudo groupadd -g 5001 bindgroup
sudo chown -R dockermgr:bindgroup /home/dockermgr/bind9/var/lib/bind
sudo chmod -R 2774 /home/dockermgr/bind9/var/lib/bind
sudo usermod -aG bindgroup dockermgr

The reason others will have read access incidentally is so backups can be done by another user

The only problem is we need to maintain permissions for files, so we’ll use ACLs for this

sudo apt install acl
sudo setfacl -d -m g::rw /home/dockermgr/bind9/var/lib/bind
sudo setfacl -d -m o::r /home/dockermgr/bind9/var/lib/bind

For the changes to take effect on the host, we’ll have to exit out as the Docker user and then log back in

For the container itself, we need to stop and restart the Bind container

docker compose down bind9
docker compose up -d

We’ll then check the container is still working

docker ps -l

Again, we’ll also check DNS resolution is working

host pbs.homelab.lan
host 192.168.102.30

And to make sure the permissions are working, we’ll create a new file within the container

docker exec bind9 touch /var/lib/bind/test.txt

Assuming we don’t get an error, the file should also show up in the host

ls -l bind9/var/lib/bind

Assuming that worked, we’ll remove the file using the Docker user

rm bind9/var/lib/bind/test.txt

So both users now have write access

Enable Dynamic Updates:
Dynamic Updates or Dynamic DNS involves allowing computers to make changes to DNS records

But we don’t want just any device being able to make changes, so we’ll create a key or passphrase to restrict this ability

TIP: The Bind developers removed IP address restrictions because the source IP address could potentially be spoofed

Now depending on your environment you could create one key and give it to all your servers

Or you could give servers their own individual keys

The choice comes down to, if a server gets compromised, how many servers will you need to update with a new key and how long will it take to do this?

For DHCP servers for instance, you may only have two servers and since they’ll have the same access, it may not matter so much if they used the same key

Of course, if you have an automated way to push keys out to individual servers, I guess it wouldn’t take much time to replace all of the keys on all the servers

As an example though, I’ll create one key for DHCP servers to use

For Debian, the command we need is installed as part of the Bind installation

In which case, to create a key we’ll run this command through the container

docker exec bind9 tsig-keygen -a hmac-sha256 dhcp.key >> bind9/etc/bind/dns.keys.conf

The output would normally be something like this but I’ve created a file to store keys in called dns.keys.conf

key "dhcp.key" {
	algorithm hmac-sha256;
	secret "qggo3scR2I3qvllD4ts5qOsgve52hZZnSQcyKsZrVtA=";
};

TIP: Although the documentation mentions using an FQDN for the key name, it doesn’t seem to be necessary Having said that, an FQDN would make management easier if you’re running multiple domains, each with a separate key

For security reasons and to avoid system errors, we’ll restrict access to this file from an account with sudo rights

sudo chown dockermgr:bindgroup /home/dockermgr/bind9/etc/bind/dns.keys.conf
sudo chmod 640 /home/dockermgr/bind9/etc/bind/dns.keys.conf

Now this does throw up a conundrum because earlier we allowed others to have read access of the zone files so they could be backed up

We can’t do that with this file though because Bind will complain, and also because it contains more sensitive information

And while we could make the backup user a member of bindgroup, that would give the backup user write access to the zone files

For me, one option is to encrypt this file and make that encrypted file readable so it can be backed up

But my goal is to let Ansible maintain all this anyway, so that I only need to keep the Ansible computer backed up

The Bind user will need to be able to read this file, so back as the Docker user, we’ll add an extra mapping in our Docker Compose file

nano docker-compose.yml

...
      - ./bind9/etc/bind/dns.keys.conf:/etc/bind/dns.keys.conf:ro
...

Now save and exit

Next, we’ll backup the existing configuration file

cp bind9/etc/bind/named.conf.local bind9/etc/bind/named.conf.local.bak

TIP: I ran into quite a few issues myself due to typos for instance, so this makes it much easier to revert back to a working file then try again

Now we’ll update the forward and reverse lookup zones so that it looks like this

nano bind9/etc/bind/named.conf.local

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

zone "homelab.lan"
        type master;
        file "/var/lib/bind/db.homelab.lan";
        update-policy { grant dhcp.key wildcard *.homelab.lan A DHCID; };
};

zone "168.192.in-addr.arpa" {
        type master;
        file "/var/lib/bind/db.192.168";
        update-policy { grant dhcp.key wildcard *.168.192.in-addr.arpa PTR DHCID; };
};

Now save and exit

At the top of the config file I’ve added a line to include the file containing the key

Going forward, any more keys would be added to that same dns.keys.conf file

I’ve also added a line within each zone to allow Dynamic DNS updates for devices with that key

This is standard practice because you can hand over the config file for support for instance without revealing any secrets

For a DHCP server, I only expected this would involve A records in the forward lookup zone and PTR records in the reverse lookup zone

However, more modern DHCP servers also maintain DHCID records to track client devices

This is to protect an existing leasing from being overwritten

We’ll now stop and start the container again for these changes take effect

docker compose down bind9
docker compose up -d

Then double check the container is running

docker ps -l

Once again, we’ll double check that DNS is still working

host testpve.homelab.lan
host 192.168.102.11

NSUPDATE:
Before you move on to configuring a DHCP server for instance, it’s best to test that Dynamic Updates work

Otherwise, if your DHCP server for instance can’t update Bind, you would have to work out if it’s a problem with the Bind server or the DHCP server

To do this we’ll use the nsupdate command

First I’ll switch to a user with sudo rights

Then we’ll make sure the dnsutils package is installed

sudo apt update
sudo apt install dnsutils -y

Now I’ll switch back to our Docker user

There are different ways to use nsupdate, but in these examples we’ll create an instruction file

nano nsupdate.txt

server testdns1.homelab.lan
update delete ddnstest.homelab.lan. A
update add ddnstest.homelab.lan. 600 A 192.168.250.250
show
send

Now save and exit

First we select the DNS server that has a writable version of the database

We’ll delete an existing A record, if it exists

Then create an A record to test against

We’ll then display the changes to be applied and then apply them

Now we can test if DDNS works

nsupdate -k bind9/etc/bind/dns.keys.conf -v nsupdate.txt

NOTE: At this point in time there’s only one key in the key file so I can reference this with the -k parameter. Otherwise I’d need a separate file. Unfortunately, there’s no way that I know of to reference a particular key in a file

The -v parameter is to use TCP instead of the default option which is UDP. If a request is too big it can be a problem for UDP, so it’s easier to just stick with TCP

Assuming there are no errors returned, a DNS lookup should return details about this new record

dig -t A ddnstest.homelab.lan.

Expect to see a status of NOERROR as well as the A record in the ANSWER SECTION

This record will probably not show up immediately in the zone file though

cat bind9/var/lib/bind/db.homelab.lan

This is because Bind uses a journal to track and later update changes made to a zone file, similar to a database server

ls -l bind9/var/lib/bind/

Assuming there are no problems, we’ll tidy things up by removing the record

nano nsupdate.txt

server testdns1.homelab.lan
update delete ddnstest.homelab.lan. A
show
send

Now save and exit

Then we’ll apply the change

nsupdate -k bind9/etc/bind/dns.keys.conf -v nsupdate.txt

Now we shouldn’t be able to find this record

dig -t A ddnstest.homelab.lan.

Finally we’ll remove this temporary file

rm nsupdate.txt

NOTE: Going forward, dynamic changes will be being made to DNS and you can run into conflicts because there are also static entries to manage

There are two approaches to handling this

One is to pause Dynamic Updates

docker exec bind9 rndc freeze

Then you can manually update the zone file(s) with the record(s) you want to add, remove or modify but you must remember to increment the serial number as well

nano bind9/var/lib/bind/db.homelab.lan

Then you can re-enable updates

docker exec bind9 rndc thaw

But since the last video I released for Dynamic Updates, I’ve read that the better approach is to use nsupdate for your own static IP records

For one thing, you aren’t pausing Dynamic Updates and you won’t forget to increment the serial number

In which case, if you plan to do that, I think it would make sense to create a separate key for an Administrator to use

Static Records:
My main concern with this is that static records can be changed through Dynamic Updates, accidentally or maliciously

That’s why I’ve been looking for a way to separate static and dynamic records

Basically, Bind loads everything into memory and at that point changes can be made if you have the right key

For example, let’s resolve the IP address for a record I manually added to the zone file

host test2.homelab.lan

Next, we’ll use nsupdate to change this, so first we’ll create a file to use

nano nsupdate.txt

server testdns1.homelab.lan
update delete test2.homelab.lan. A
update add test2.homelab.lan. 600 A 192.168.250.220
show
send

Now save and exit

Then we’ll apply the change

nsupdate -k bind9/etc/bind/dns.keys.conf -v nsupdate.txt

Now as you’ll see this hostname is resolving to a different IP address

host test2.homelab.lan

In which case I’ll revert this back

rm nsupdate.txt
nano nsupdate.txt

server testdns1.homelab.lan
update delete test2.homelab.lan. A
update add test2.homelab.lan. 600 A 192.168.102.101
show
send

Now save and exit

Then we’ll apply the change

nsupdate -k bind9/etc/bind/dns.keys.conf -v nsupdate.txt

And we’ll double check we resolve to the correct IP address

host test2.homelab.lan

To protect these type of records you can deny dynamic changes to them

First though, we’ll backup the existing configuration file

cp bind9/etc/bind/named.conf.local bind9/etc/bind/named.conf.local.bak

For example, to prevent the record for pvedemo1.homelab.lan from being changed we would edit the named.conf.local file and modify the update-policy rules

nano bind9/etc/bind/named.conf.local

zone "homelab.lan"  {
	type master;
	file "/var/lib/bind/db.homelab.lan";
	update-policy {
		deny * name pvedemo1.homelab.lan;
		grant dhcp.key wildcard *.homelab.lan A DHCID; 
	};
};

Now save and exit

NOTE: The ordering of policies is important. Like a firewall, Bind will stop at the first match found, so this deny rule has to precede the grant rule

For the change to take effect we’ll need to restart the container

docker compose restart bind9

Then double check the container is running

docker ps -l

Now we’ll try to change that record

rm nsupdate.txt
nano nsupdate.txt

server testdns1.homelab.lan
update delete pvedemo1.homelab.lan. A
update add pvedemo1.homelab.lan. 600 A 192.168.250.220
show
send

Now save and exit

Then we’ll try and apply the change

nsupdate -k bind9/etc/bind/dns.keys.conf -v nsupdate.txt

What we should see is that the update is refused

In which case, we’ll remove this temporary file

rm nsupdate.txt

I must admit, this is going to mean more admin, which is another reason to use Ansible

Security is important and it’s all about layers and this would protect against unwanted changes that could have major consequences

What it won’t do is prevent a compromised Bind server rewriting the original zone file though

This makes regular patching and restricting access all the more important

Versioning:
Since we look to have a working image for Bind, I need to roll out a new latest version

cd images/bind9
docker build --network=host --no-cache -t bind9_image:latest .

And then update the compose file

cd
nano docker-compose.yml

...
  bind9:
    image: bind9_image:latest

...

Now save and exit

For the changes to take effect, we need to stop and restart the Bind container

docker compose down bind9
docker compose up -d

We’ll then check the container is still working

docker ps -l

It’s actually the same image, but we should still check DNS resolution is working all the same

host test2.homelab.lan
host 192.168.102.32
host www.microsoft.com

Assuming there are no issues, Bind is now ready to use for Dynamic Updates

For me, my DHCP servers will need updating to support this

How you configure that depends on what DHCP server you’re using and I can’t cover all of those in this video

But if you’re interested in how to configure Kea for instance, check out my other video as the process is the same whether Kea is a stand alone server or in a container

In this example, I’ve set up a key for a DHCP server, but I’d also want one one for an Admin user, for servers using ACME, for Ansible and so on

Once those keys are created, it would be a matter of updating the policies and restarting the container

Summary:
Running services like Bind in containers does present extra challenges, in this case file permissions

But containers need much less compute power and resources which for me saves on capital and running costs

And now I have a Bind container that supports Dynamic Updates, it can be used to support services like Dynamic DNS and ACME DNS challenges

Sharing is caring!