Using PowerDNS as internal resolver

Setting up DNS for an internal network can be a bit daunting; To be able to resolve records within your internal zone you will need to configure your computers to use your internal DNS server as resolver but this means it will also need a way to resolve names out on the internet. This means you need to setup an authoritative server as wel as a recursor.

In PowerDNS 4.1 recursion was removed from the authoritative server which means you’ll have to have an authoritative server as well as a recursor. Both can’t be listening on port 53 so how do you go about setting this up? and how would you go about managing your internal zones without having to go into the database and use insert queries?

Managing your records becomes really easy with PowerDNS-Admin, a web based management tool. I’ll describe the installation in a separate post.
To setup an authoritative server with recursing capabilities, also referred to as Split Horizon DNS, we can use DNSDist, a load balancer for DNS. DNSDist also gives us a lot of flexibility to forward queries for certain zones to specific name servers, for instance in a situation where you have multiple office locations interconnected via VPN, each with their own DNS servers.

Setting up an Authoritative PowerDNS server

The image below outlines what we will be setting up; Internal clients, in this case ‘Laptop’, queries DNSDist, DNSDist processes the query, determines that laptop is coming from a network that is allowed to make recursive queries ( in this case) and sends the query to the recursor. The recursor in turn will determine if the query matches an internal zone, if so, the query is sent to the authoritative server which retrieves the answer from the database and returns it.

Sandboxed clients, in this case ‘Sandboxed dev server’, queries DNSDist, DNSDist determines that the sandboxed dev server is not coming from and sends the query to the authoritative server. the dev server will therefor only be able to resolve internal zones.

We’ll start with a few definitions that we’ll use for this tutorial:
Our internal domain: int.mydomain.tld
The internal IP range:
The ip range for sandboxed servers:
The ip address of the server we’re setting up:
The hostname of the server:
Our email address: your@emailaddress.tld
The external resolvers:, (google’s dns servers)

Starting with a Debian 9 minimal install with only SSH Server and Standard System Utilities selected we will setup some prerequisites and tools. We’ll start with vim, curl, pwgen, net-tools, dnsutils and sudo and we’ll setup mariadb-server as backend for PowerDNS

$> apt install vim curl pwgen net-tools dnsutils sudo -y
$> apt install mariadb-server -y

Run mysql_secure_installation to set it up for production use.

$> mysql_secure_installation

In order to log into MariaDB to secure it, we'll need the current
password for the root user.  If you've just installed MariaDB, and
you haven't set the root password yet, the password will be blank,
so you should just press enter here.

Enter current password for root (enter for none):
OK, successfully used password, moving on...

Setting the root password ensures that nobody can log into the MariaDB
root user without the proper authorisation.

Set root password? [Y/n] y
New password: ************
Re-enter new password: ************
Password updated successfully!
Reloading privilege tables..
 ... Success!

By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them.  This is intended only for testing, and to make the installation
go a bit smoother.  You should remove them before moving into a
production environment.

Remove anonymous users? [Y/n] y
 ... Success!

Normally, root should only be allowed to connect from 'localhost'.  This
ensures that someone cannot guess at the root password from the network.

Disallow root login remotely? [Y/n] y
 ... Success!

By default, MariaDB comes with a database named 'test' that anyone can
access.  This is also intended only for testing, and should be removed
before moving into a production environment.

Remove test database and access to it? [Y/n] y
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

Reload privilege tables now? [Y/n] y
 ... Success!

Cleaning up...

All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!

Next we prepare a database for the authoritative server, login to your mysql server, create the database and setup permissions. Make sure to use a secure password, you can use the pwgen utility for that

#> pwgen 16 1
Owahm4eithekahWo (this is an example password, don't use this!)
#> mysql
mysql> GRANT ALL PRIVILEGES ON powerdns.* to powerdns@localhost identified by 'Owahm4eithekahWo';
mysql> \q

Next we’ll add the PowerDNS and DNSDist repositories.

$> curl | apt-key add -
$> echo "deb [arch=amd64] stretch-auth-42 main" > /etc/apt/sources.list.d/pdns.list
$> echo "deb [arch=amd64] stretch-rec-42 main" >> /etc/apt/sources.list.d/pdns.list
$> echo "deb [arch=amd64] stretch-dnsdist-14 main" > /etc/apt/sources.list.d/dnsdist.list
$> apt update

Then we can install the PowerDNS authoritative server

$> apt install pdns-server pdns-backend-mysql

Add the database connection details for PowerDNS

$> vim /etc/powerdns/pdns.d/pdns.local.gmysql.conf

and load the database schema

$> mysql powerdns < /usr/share/doc/pdns-backend-mysql/schema.mysql.sql
$> systemctl restart pdns.service

Check to see if PowerDNS started

$> netstat -tapn | grep pdns
tcp        0      0    *               LISTEN      4481/pdns_server
tcp6       0      0 :::53                   :::*                    LISTEN      4481/pdns_server

$> dig @

; <<>> DiG 9.10.3-P4-Debian <<>> @
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 34259
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

; EDNS: version: 0, flags:; udp: 1232
;.				IN	NS

;; Query time: 0 msec
;; WHEN: Sun May 05 16:42:42 CEST 2019
;; MSG SIZE  rcvd: 28

Now that we have a working PowerDNS Authoritative server we’ll change the configuration to prepare it for dnsdist and PowerDNS-Admin. First we backup the original config file

$> mv /etc/powerdns/pdns.conf /etc/powerdns/pdns.conf.orig

Then we create a unique key for API access (make sure you create your own)

$> openssl rand -base64 64

Then create /etc/powerdns/pdns.conf with the following contents


Some notes on the above
api=yes to enable api access
api-key is a unique key needed to access the api
default-soa-mail is the network admin’s email address, this is used in the SOA header
default-soa-name is the fqdn of your nameserver
include-dir tells to include files in /etc/powerdns/pdns.d which is where the database config file for powerdns is.
launch which backends to load and in what order to query them. our backends are loaded through the include-dir option so you can leave this empty.
local-port port to listen on, this defaults to 53 but we’ll set this to 5300, this is what DNSDist will connect to. DNSDist will handle all queries on port 53.
security-poll-suffix is meant to query to check for security updates but this doesn’t seem to work for the version I installed, setting this to an empty value disables it.
setgid and setuid are the user and group that the process runs under.
webserver to enable the built in webserver, needed for monitoring and api calls
webserver-address the address that the webserver will listen on. this should be set to to prevent connections from other locations.
webserver-allow-from which addresses are allowed to connect to the webserver. also set to only the localhost address.

Once the configuration file is saved, restart pdns

$> systemctl restart pdns.service

and do the checks to see if the server is running

$> netstat -tapn | grep pdns
tcp        0      0*               LISTEN      4961/pdns_server
tcp        0      0*               LISTEN      4961/pdns_server
tcp6       0      0 :::5300                 :::*                    LISTEN      4961/pdns_server

$> dig @ -p 5300

; <<>> DiG 9.10.3-P4-Debian <<>> @ -p 5300
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 61424
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

; EDNS: version: 0, flags:; udp: 1232
;.				IN	NS

;; Query time: 0 msec
;; WHEN: Sun May 05 20:48:24 CEST 2019
;; MSG SIZE  rcvd: 28

Now that our authoritative server is running we can continue with the PowerDNS Recursor

Setting up PowerDNS Recursor

Installing the recursor is pretty straight forward

$> apt install pdns-recursor -y

We’ll backup the original config file and create a new one with only the necessary options

$> mv /etc/powerdns/recursor.conf /etc/powerdns/recursor.conf.orig

Recreate /etc/powerdns/recursor.conf with the following content


Some notes on the above:
config-dir is self explanatory
forward-zones zones for which we forward queries, comma separated domain=ip pairs. here we list our internal zones, including the reverse lookup zone for your ip range. note the port numbers behind the address, this is the port that our authoritative server is listening on.
forward-zones-recurse zones for which we forward queries with recursion bit, comma separated domain=ip pairs. here we add our external resolvers in the form of .=;
hint-file If set, load root hints from this file
local-address IP addresses to listen on, separated by spaces or commas. Also accepts ports, but we’ll define the port separately.
local-port port to listen on. By default this is port 53 but we’ll change this to 5301 as dnsdist will be listening on port 53 later on.
quiet Suppress logging of questions and answers. You can set this to no to have queries logged which may be handy for troubleshooting, otherwise leave this set to yes.
security-poll-suffix disabled for the same reason as with the authoritative server
setgid and setuid the user and group the recursor wil run as

After saving the config file, restart the recursor

$> systemctl restart pdns-recursor.service

And check if it’s running

$> netstat -tapn | grep pdns_recursor
tcp        0      0*               LISTEN      5557/pdns_recursor
$> dig @ -p 5301

; <<>> DiG 9.10.3-P4-Debian <<>> @ -p 5301
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35122
;; flags: qr rd ra; QUERY: 1, ANSWER: 13, AUTHORITY: 0, ADDITIONAL: 1

; EDNS: version: 0, flags:; udp: 512
;.				IN	NS

.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS
.			68781	IN	NS

;; Query time: 0 msec
;; WHEN: Mon May 06 00:45:27 CEST 2019
;; MSG SIZE  rcvd: 252

At this stage you should also be able to use the recursor to resolve external names

$> dig @ -p 5301 +short

Now that our recursor is working it’s time to configure DNSDist

Configuring DNSDist

Install DNSDist as follows

$> apt install dnsdist -y

By default DNSDist doesn’t have a config file, there is an example file in /usr/share/doc/dnsdist/examples/dnsdist.conf.gz with a lot of explanation if you’re curious about the possibilities of DNSDist but for now we’ll only add what’s needed for this setup.

Create the file /etc/dnsdist/dnsdist.conf with the following contents


newServer({address='', name='auth', pool='auth'})
newServer({address='', pool='recursor'})

recursive_ips = newNMG()
addAction(NetmaskGroupRule(recursive_ips), PoolAction('recursor'))
addAction(AllRule(), PoolAction('auth'))
webserver("", "1","1", {["X-Frame-Options"]= "", ["X-Custom"]="custom"})

Notes on the above
setLocal is the address we’ll be listening on, this is the ip address of this server
setACL is a list of addresses and/or networks that are allowed to query this server
With newServer we define our authoritative server and recursor
recursive_ips = newNMG() defines a netmask group where we’ll add the netmasks for everyone that is allowed to do recursion, typically your local network. You can use this for instance to exclude development machines that are only allowed to resolve internal addresses from our authoritative server
With recursive_ips:addMask(‘’) we add our internal network to the allowed recursion group, you can also allow everyone to recurse by setting recursive_ips:addMask(‘’) instead. Keep in mind that if you start connecting to other networks that will need to use this server as recursor you will need to add them by adding an extra recursive_ips:addMask line.
recursive_ips:addMask(‘::/0’) to add ipv6 networks if you have them
addAction(NetmaskGroupRule(recursive_ips), PoolAction(‘recursor’)) basically tells that all address matching the ones defined in the recursive_ips group are directed to pool ‘recursor’.
all addresses that don’t match recursor_ips will be sent to pool ‘auth’ by this line: addAction(AllRule(), PoolAction(‘auth’))
Lastly, we can start a webserver for monitoring purposes with this line: webserver(“”, “1”,”1″, {[“X-Frame-Options”]= “”, [“X-Custom”]=”custom”})
This is optional though.

After saving the config file we can check the syntax

$> /usr/bin/dnsdist --check-config
Configuration '/etc/dnsdist/dnsdist.conf' OK!

then we can restart the server

$> systemctl restart dnsdist.service

And test if it works. At this stage we don’t have an internal zone yet so we’ll test recursion first.

$> dig @

; <<>> DiG 9.10.3-P4-Debian <<>> @
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16668
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

; EDNS: version: 0, flags:; udp: 512
;			IN	A


;; Query time: 8 msec
;; WHEN: Mon May 06 01:19:52 CEST 2019
;; MSG SIZE  rcvd: 59

Now that we have a fully working authoritative server with recursion, we can add PowerDNS-Admin to manage it.

Setting up PowerDNS-Admin

For PowerDNS-Admin we’ll need a few prerequisites

$> install -y default-libmysqlclient-dev python-mysqldb libsasl2-dev libffi-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev pkg-config apt-transport-https git python3-pip

We need to add a more recent nodejs repository as the one provided by debian is way too old. We can do this by adding the nodesource repository for debian as follows

$> curl -sL | bash -
$> apt install -y nodejs

Then we need to add the Yarn repository and in stall Yarn

$> curl -sS | apt-key add -
$> echo "deb stable main" > /etc/apt/sources.list.d/yarn.list
$> apt update
$> apt install yarn

Install virtualenv through pip

$> pip3 install virtualenv

Then we add a user for PowerDNS-Admin

$> useradd -m -d /home/pdnsadmin -s /bin/bash -c "PowerDNS Admin" pdnsadmin

Next we’ll prepare the database. You can use the pwgen utility to generate a secure password.

$> pwgen 16 1
$> mysql
mysql> CREATE DATABASE pdnsadmin CHARACTER SET utf8 COLLATE utf8_unicode_ci;
mysql> GRANT ALL ON pdnsadmin.* TO 'pdnsadmin'@'localhost' IDENTIFIED BY 'phikahTi5dahgie8';

Then we switch to the newly created pdnsadmin user. In this setup i’ve opted to install PowerDNS-Admin in the homedirectory of the pdnsadmin user, you can also create a directory in /opt if you prefer.

$> su - pdnsadmin
pdnsadmin $> git clone /home/pdnsadmin/powerdns-admin
pdnsadmin $> cd /home/pdnsadmin/powerdns-admin
pdnsadmin $> virtualenv -p python3 flask
pdnsadmin $> . ./flask/bin/activate
(flask) pdnsadmin $> pip3 install -r requirements.txt

Note that installing the requirements on a Raspberry Pi will seem like it’s getting stuck but it’s actually just taking a very long time to compile.

After installing the requirements, copy the file to (at this point we’re still in /home/pdnsadmin/powerdns-admin as the pdnsadmin user)

(flask) pdnsadmin $> cp

Edit and change the following:

SECRET_KEY = 'Put some random string or sentence in here'

SQLA_DB_USER = 'pdnsadmin'
SQLA_DB_PASSWORD = 'the password you generated for the pdnsadmin database'
SQLA_DB_NAME = 'pdnsadmin'

After saving the file, initialise the database with the following commands

(flask) pdnsadmin $> export FLASK_APP=app/
(flask) pdnsadmin $> flask db upgrade
 * Tip: There are .env files present. Do "pip install python-dotenv" to use them.
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 787bdba9e147, Init DB
INFO  [alembic.runtime.migration] Running upgrade 787bdba9e147 -> 59729e468045, Add view column to setting table
INFO  [alembic.runtime.migration] Running upgrade 59729e468045 -> 1274ed462010, Change setting.value data type
INFO  [alembic.runtime.migration] Running upgrade 1274ed462010 -> 4a666113c7bb, Adding Operator Role
INFO  [alembic.runtime.migration] Running upgrade 4a666113c7bb -> 31a4ed468b18, Remove all setting in the DB
INFO  [alembic.runtime.migration] Running upgrade 31a4ed468b18 -> 654298797277, Upgrade DB Schema

(flask) pdnsadmin $> flask db migrate -m "Init DB"
 * Tip: There are .env files present. Do "pip install python-dotenv" to use them.
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.env] No changes in schema detected.

Then generate asset files with Yarn

(flask) pdnsadmin $> yarn install --pure-lockfile
yarn install v1.15.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
Done in 23.26s.

(flask) pdnsadmin $> flask assets build
 * Tip: There are .env files present. Do "pip install python-dotenv" to use them.
Building bundle: generated/main.css
[2019-05-06 15:07:53,029] [INFO] | Building bundle: generated/main.css
Building bundle: generated/main.js
[2019-05-06 15:08:24,800] [INFO] | Building bundle: generated/main.js
Building bundle: generated/validation.js
[2019-05-06 15:08:25,512] [INFO] | Building bundle: generated/validation.js
Building bundle: generated/login.css
[2019-05-06 15:08:25,515] [INFO] | Building bundle: generated/login.css
Building bundle: generated/login.js
[2019-05-06 15:08:52,627] [INFO] | Building bundle: generated/login.js

Now we can test if PowerDNS-Admin will run properly

(flask) pdnsadmin $> ./
* Tip: There are .env files present. Do "pip install python-dotenv" to use them.
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: on
[2019-05-06 15:09:58,365] [INFO] |  * Running on (Press CTRL+C to quit)
[2019-05-06 15:09:58,367] [INFO] |  * Restarting with stat
 * Tip: There are .env files present. Do "pip install python-dotenv" to use them.
[2019-05-06 15:09:58,916] [WARNING] |  * Debugger is active!
[2019-05-06 15:09:58,922] [INFO] |  * Debugger PIN: 181-043-523

Exit the server with CTRL+C. and logout of the pdnsadmin user. As user root we’ll now add a proper systemd service script to start and stop PowerDNS-Admin. Create a file called /etc/systemd/system/powerdns-admin.service with the following contents


ExecStart=/home/pdnsadmin/powerdns-admin/flask/bin/gunicorn --workers 2 --bind unix:/home/pdnsadmin/powerdns-admin/powerdns-admin.sock app:app


After saving this file, update systemd, enable the service and start it.

$> systemctl daemon-reload
$> systemctl enable powerdns-admin.service
$> systemctl start powerdns-admin.service

Next we’ll install and configure NGINX to serve up PowerDNS-Admin

$> apt install nginx -y

then create a vhost file /etc/nginx/sites-available/pdnsadmin.conf with the following contents

server {
  server_name     ;

  index                     index.html index.htm index.php;
  root                      /home/pdnsadin/powerdns-admin;
  access_log                /var/log/nginx/pdnsadmin_access.log combined;
  error_log                 /var/log/nginx/pdnsadmin_error.log;

  client_max_body_size              10m;
  client_body_buffer_size           128k;
  proxy_redirect                    off;
  proxy_connect_timeout             90;
  proxy_send_timeout                90;
  proxy_read_timeout                90;
  proxy_buffers                     32 4k;
  proxy_buffer_size                 8k;
  proxy_set_header                  Host $host;
  proxy_set_header                  X-Real-IP $remote_addr;
  proxy_set_header                  X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_headers_hash_bucket_size    64;

  location ~ ^/static/  {
    include  /etc/nginx/mime.types;
    root /home/pdnsadmin/powerdns-admin/app;

    location ~*  \.(jpg|jpeg|png|gif)$ {
      expires 365d;

    location ~* ^.+.(css|js)$ {
      expires 7d;

  location / {
    proxy_pass            http://unix:/home/pdnsadmin/powerdns-admin/powerdns-admin.sock;
    proxy_read_timeout    120;
    proxy_connect_timeout 120;
    proxy_redirect        off;

Save the file and create a symlink in /etc/nginx/sites-enabled

$> ln -s /etc/nginx/sites-available/pdnsadmin /etc/nginx/sites-enabled/pdnsadmin

Check if you made any mistake

$> nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

restart NGINX

$> systemctl restart nginx

Now since we haven’t added our internal domain to the authoritative server we will have to add a temporary entry in hosts file to get to the web UI.
On Linux and OSX the hosts file can be found in /etc/hosts
On Windows it’s usually in C:\Windows\system32\drivers\etc\hosts and looks something like this	localhost

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

Add the following line and save it.

Then point your browser at

If all went well then you should see the login page. Go ahead and click the ‘Create an account’ link. The first account created in PowerDNS-Admin is automatically an admin. Once you’ve created your account you can login.
You’ll see a big red error telling you to complete the PowerDNS API details. Complete the form with the following details:
PDNS API KEY: You’ll find this in /etc/powerdns/pdns.conf

After updating the API details we can start adding our internal zone. Click ‘New Domain’ and add int.mydomain.tld. Leave Type and SOA-EDIT-API as they are and click Submit to save it.

We follow the same procedure to add the reverse zone Once that’s added, go to settings and enable the option auto_ptr, that way when you add an A record in the int.mydomain.tld zone it will automatically add a reverse record in the reverse zone.

I hope you enjoyed this tutorial. Let me know if I missed anything.

Leave a Reply