dynamic DNS with bind9, sftdyn and NGINX on a VPS

The goal here is to get an Internet router with dynamic external IP addresses register itself as a host under a specific domain name.

I'm already controlling the lastpixel.tv domain and I want my router to be resolved from some address like muppet.lastpixel.tv.

The domain itself sits on a server with ns services managed by the server operator. Such operators generally offer online forms and maybe even APIs for updating DNS records, but there's just too much variance from provider to provider, so I'm describing here a more uniform approach.

The idea is to setup a slave DNS to the master domain name server and then setup some automation to ensure the updates.

Prerequisites

  1. A Linux server with shell access.
  2. Bind9
  3. Python3 and sftdyn
  4. NGINX
  5. letsencrypt

The Environment

Setup a relatively recent Linux and have a Python 3 environment.

python3 -m venv /opt/sftdyn
. /opt/sftdyn/bin/activate
pip install wheel aiohttp
pip install sftdyn

wheel might complain about package yarl which is an aiohttp requirement. There shouldn't be any problem with sftdyn though:

Building wheels for collected packages: yarl
  Running setup.py bdist_wheel for yarl ... error

Install bind9 and nginx:

apt install bind9 dnsutils nginx

DNS Configuration

There are too many variables in play in order to configure the dynamic host directly under the root domain as muppet.lastpixel.tv. I'm just delegating a subdomain, dynamic.lastpixel.tv, to the server's DNS by adding 2 records:

  1. A and/or AAAA records towards the server. This is probably achieved via a web based control panel, it should generally look like:

    ns.lastpixel.tv.        IN      A       185.250.105.114
    ns.lastpixel.tv.        IN      AAAA    2a06:cd40:400:1:0:0:0:3d1
  2. An NS record delegating dynamic's resolution towards ns.lastpixel.tv:

    dynamic.lastpixel.tv    IN      NS      ns.lastpixel.tv

It might take a while until the DNS settings propagate, depending on how the server's operator have it set up.

This subdomain is mandatory as only the server's operators' DNS system can manage lastpixel.tv. The slave server can only manage queries related to a subdomain in this case.

Bind9 Configuration

Create a zone file for dynamic.lastpixel.tv at /etc/bind/db.dynamic.lastpixel.tv:

$TTL    86400
@       IN      SOA     ns.lastpixel.tv. root.dynamic.lastpixel.tv. (
                              1         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                          86400 )       ; Negative Cache TTL
;
@       IN      NS      ns.lastpixel.tv.

and reference it in /etc/bind/named.conf.local:

zone "lastpixel.tv" {
        type master;
        file "/etc/bind/db.lastpixel.tv";
};

zone "dynamic.lastpixel.tv" IN {
    type master;
    file "/etc/bind/db.dynamic.lastpixel.tv";
    journal "/var/cache/bind/db.dynamic.lastpixel.tv.jnl";
    update-policy local;
};

Restart Bind9:

systemctl restart bind9

Sftdyn Configuration

Grab the sftdyn config from /opt/sftdyn/lib/python3.7/site-packages/etc/sftdyn/sample.conf:

mkdir /etc/sftdyn
cp /opt/sftdyn/lib/python3.7/site-packages/etc/sftdyn/sample.conf /etc/sftdyn/conf

and edit the file:

# snip
http = "127.0.0.1:8080"
# snip
clients = {
    "register/muppet?secret=mysecret": "muppet.dynamic.lastpixel.tv",
}

This assumes sftdyn binds to localhost:8080 only. Feel free to work with a different port but it's essential that the address is local only for NGINX.

Configure sftdyn as a systemd service:

cp /opt/sftdyn/lib/python3.7/site-packages/usr/lib/systemd/system/sftdyn.service /etc/systemd/system/

and tune it up for the virtual environment and bind9 credentials:

[Unit]
Description=sft dynamic dns service
After=network.target

[Service]
User=bind
Group=bind
ExecStart=/opt/sftdyn/bin/python3 -u -m sftdyn -v
Restart=on-failure

[Install]
WantedBy=multi-user.target

And make sure systemctl sees everything and runs the service on boot:

systemctl daemon-reload
systemctl enable --now sftdyn.service

NGINX Configuration

Edit /etc/nginx/sites-enabled/default to contain the following:

server {
    # snip

    # redirects to local sftdyn instance here:
    location /dynamic/ {
        proxy_pass http://127.0.0.1:8080/;
        include /etc/nginx/proxy_params;
    }

    # snip
}

and restart NGINX:

systemctl restart nginx

This will make calls towards lastpixel.tv/dynamic hit sftdyn directly. The configured sftdyn URL register/muppet?secret=mysecret is viewed externally as lastpixel.tv/dynamic/register/muppet?secret=mysecret.

with letsencrypt

This one is straightforward with the letsencrypt and certbot instructions. It's recommended during the certbot run to completely disable http and select https only. certbot will edit the NGINX config and have the http schema redirect to https.

Final Result

I just configure the home router to update its dynamic dns via calls to https://lastpixel.tv/dynamic/register/muppet?secret=mysecret now.