Setting up a self-hosted authoritative DNS server with PowerDNS

I have been using Cloudflare for over a year now for multiple domain names. I wanted to migrate away from that, and chose to self-host DNS using PowerDNS.

PowerDNS provides multiple types DNS servers, but what you’ll want to host our own domain is an authoritative name server. This will publish the DNS records for our domain so that recursive name servers (like Google’s, Cloudflare’s or your ISP’s) can forward the data to clients.

Prerequisites

Most registrars will require you to have two different name servers, which is a good practice (I chose OVH as my registrar but anything should work with this tutorial).

Ideally you want to have your two DNS servers at two different locations to provide enough redundancy. I chose to have one server on a VPS and another at home on the main Renn.es server. Make sure your ISP allows opening port 53 and requests come through correctly though, as most don’t.

Compose example

Here is a docker-compose example:

services:
  pdns:
    container_name: pdns
    image: powerdns/pdns-auth-master:48-lmdb-index-1
    ports:
      - "53:53"
      - "53:53/udp"
    volumes:
      - "/data/pdns/varlib:/var/lib/powerdns"
      - "/data/pdns/etc:/etc/powerdns"

You should of course change /data/pdns to wherever you want to host your PowerDNS related files.

Run docker-compose up -d once before proceeding to create the volume directories. It will probably just fail to start because of a missing database file.

Creating the database

Download the database schema from GitHub.

Then run:

sudo sqlite3 /data/pdns/varlib/pdns.sqlite3 < schema.sqlite3.sql

I had a permission issue when trying to run the container again, so I ran a sudo chown -R 953:953 /data/pdns. Make sure there is no UID or GID 953 on your system because that user would have access to /data/pdns (or make sure that /data is not readable by that user).

Then run docker-compose up -d again. The container’s log should end with the following line:

Done launching threads, ready to distribute questions

Editing zones

There are many frontends for PowerDNS, but I chose to not install any and use the included pdnsutil command-line utility for simplicity.

Create an empty zone for your domain name (I’ll use charennes.org from now on as an example):

docker exec -it pdns pdnsutil create-zone charennes.org

Edit the zone (I hope you’re familiar with vi):

docker exec -it pdns pdnsutil edit-zone charennes.org

I first changed the default SOA record to match the configuration I wanted:

charennes.org   3600    IN      SOA     ns.charennes.org admin.charennes.org 0 10800 3600 604800 3600

This tells DNS that the name of the main DNS server for your zone is ns.yourdomain.org (ns.charennes.org in my case). You should also add an email address with the @ replaced by a dot. If there are dots in your email address (before the @), use a backslash to escape them.

So let’s create an A record for ns.charennes.org:

ns.charennes.org        3600    IN      A       82.64.143.64

Of course you should replace the IP to match your public IP.

Let’s also add our VPS’s public IP, which will be used as a backup:

ns2.charennes.org       3600    IN      A       51.210.180.14

We can then add two NS records to point to the DNS servers we just defined:

charennes.org   3600    IN      NS      ns.charennes.org
charennes.org   3600    IN      NS      ns2.charennes.org

I also added the following to set the IPv4 address of charennes.org:

charennes.org   3600    IN      A       82.64.143.64

Here’s the final file:

; Warning - every name in this file is ABSOLUTE!
$ORIGIN .
charennes.org   3600    IN      SOA     ns.charennes.org admin.charennes.org 0 10800 3600 604800 3600
charennes.org   3600    IN      A       82.64.143.64
charennes.org   3600    IN      NS      ns.charennes.org
charennes.org   3600    IN      NS      ns2.charennes.org
ns.charennes.org        3600    IN      A       82.64.143.64
ns2.charennes.org       3600    IN      A       51.210.180.14

Now I was able to test the server with the following command:

nslookup charennes.org <server-ip>
...
Address: 82.64.143.64
...

Remember to open port 53 (for both TCP and UDP) and check that queries also work from outside your firewall!

I also ran the following command to enable DNSSEC:

docker exec -it pdns pdnsutil secure-zone charennes.org
docker exec -it pdns pdnsutil rectify-zone charennes.org

Note that you will have to find a way to sync the server’s database between your two servers. I chose to use a one-way rsync script which I run every time I update stuff. Remember to restart the docker container whenever you overwrite the database because it won’t reread it by itself. Also know that overwriting databases is not the best practice in most cases, but I think it’s OK here since the database isn’t being written to without modifying domains.

Letting DNS know about our new server

Now you’ll need to tell your registrar about the server you just created. This is a bit tricky as you need to give the registrar the domain names of your servers, but they don’t have one yet as the DNS isn’t propagated. What I did was add A records for ns.charennes.org and ns2.charennes.org matching the ones I had defined on my own name server, and then set the DNS servers to those which worked alright. It took some time though, around an hour to get to my own PC, but it could be up to two days until every computer on the internet has the new correct records (due to cache time to live).

Errata

You also need to add a DS record in your registrar’s control panel for DNSSEC to work! Here is how to find the DS record(s):

docker exec -it pdns pdnsutil show-zone charennes.org

Two of the outputted lines will be something like:

ID = 3 (CSK), flags = 257, tag = 7327, algo = 13, bits = 256      Active         Published  ( ECDSAP256SHA256 )
CSK DNSKEY = charennes.org. IN DNSKEY 257 3 13 <base64 encoded key> ; ( ECDSAP256SHA256 )

Copy the key to your registrar, and you’re done! I had to enter key tag 7327, flag 257, algorithm 13 and the base64 encoded key.

For security, I would also recommend setting the version-string option to anonymous to avoid bots scanning your server for vulnerable versions.