Setting up PowerDNS with working MariaDB Replication

I’ve been wanting to change to PowerDNS for a little but havnt got around to it. After taking some PTO from work and buying a bunch of new Raspberry Pi’s i figured now is as good a time as any to build this out.

If you google this topic, there are loads of guides out there, all of them seem to be a rehash of the same source instructions which…doesnt work!

If you do follow those instructions you will get working DNS servers but you will NOT get working replication. At first glance it may look like it is but as soon as you try and push an update replication will fail. After several hours of hair pulling and googling i have managed to get this working!

This will be split into 4 parts, setup for both servers, setup for ns1 (master), setup for ns2 (slave) and finally the PowerDNS Admin page setup.

Both PowerDNS servers

Firstly, lets install MariaDB

apt-get install mariadb-server

Now lets run through some configuration for MariaDB before we start creating databases, tables and users:

Firstly, run through ‘mysql_secure_installation’ to set your root password etc

Then In:

/etc/mysql/mariadb.conf.d/50-server.conf

Add in the following, under the MariaDB heading:

# InnoDB
innodb_log_file_size = 64M
default-storage-engine=INNODB
innodb_buffer_pool_size=1G
innodb_buffer_pool_instances = 2
innodb_autoinc_lock_mode = 2
innodb_doublewrite = 1
innodb_file_per_table = 1
innodb_flush_log_at_trx_commit = 2
innodb_lock_wait_timeout = 60
innodb_locks_unsafe_for_binlog = 1
innodb_stats_on_metadata = 0
transaction-isolation=READ-COMMITTED

Then restart MariaDB with

systemctl restart mariadb

Next, log into MariaDB as root with

mysql -u root -p

and add enter the following:

SET GLOBAL binlog_format = 'ROW';

I think that is the golden bit that a lot of guides have been missing! As once i did this on my test network, and reset up replication it started working. While we are still logged in, lets create our database, user and tables:

CREATE DATABASE powerdns;

Then create the user:

GRANT ALL ON powerdns.* TO 'powerdns'@'localhost' \
IDENTIFIED BY '<SECRET_PASSWORD>';

FLUSH PRIVILEGES;
USE powerdns;

Now we need to create the tables:

CREATE TABLE domains (
  id                    INT AUTO_INCREMENT,
  name                  VARCHAR(255) NOT NULL,
  master                VARCHAR(128) DEFAULT NULL,
  last_check            INT DEFAULT NULL,
  type                  VARCHAR(6) NOT NULL,
  notified_serial       INT UNSIGNED DEFAULT NULL,
  account               VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
  PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';

CREATE UNIQUE INDEX name_index ON domains(name);
CREATE TABLE records (
  id                    BIGINT AUTO_INCREMENT,
  domain_id             INT DEFAULT NULL,
  name                  VARCHAR(255) DEFAULT NULL,
  type                  VARCHAR(10) DEFAULT NULL,
  content               VARCHAR(64000) DEFAULT NULL,
  ttl                   INT DEFAULT NULL,
  prio                  INT DEFAULT NULL,
  change_date           INT DEFAULT NULL,
  disabled              TINYINT(1) DEFAULT 0,
  ordername             VARCHAR(255) BINARY DEFAULT NULL,
  auth                  TINYINT(1) DEFAULT 1,
  PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';

CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
CREATE INDEX ordername ON records (ordername);
CREATE TABLE supermasters (
  ip                    VARCHAR(64) NOT NULL,
  nameserver            VARCHAR(255) NOT NULL,
  account               VARCHAR(40) CHARACTER SET 'utf8' NOT NULL,
  PRIMARY KEY (ip, nameserver)
) Engine=InnoDB CHARACTER SET 'latin1';

CREATE TABLE comments (
  id                    INT AUTO_INCREMENT,
  domain_id             INT NOT NULL,
  name                  VARCHAR(255) NOT NULL,
  type                  VARCHAR(10) NOT NULL,
  modified_at           INT NOT NULL,
  account               VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
  comment               TEXT CHARACTER SET 'utf8' NOT NULL,
  PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';

CREATE INDEX comments_name_type_idx ON comments (name, type);
CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);
CREATE TABLE domainmetadata (
  id                    INT AUTO_INCREMENT,
  domain_id             INT NOT NULL,
  kind                  VARCHAR(32),
  content               TEXT,
  PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';

CREATE INDEX domainmetadata_idx ON domainmetadata (domain_id, kind);

CREATE TABLE cryptokeys (
  id                    INT AUTO_INCREMENT,
  domain_id             INT NOT NULL,
  flags                 INT NOT NULL,
  active                BOOL,
  content               TEXT,
  PRIMARY KEY(id)
) Engine=InnoDB CHARACTER SET 'latin1';

CREATE INDEX domainidindex ON cryptokeys(domain_id);

CREATE TABLE tsigkeys (
  id                    INT AUTO_INCREMENT,
  name                  VARCHAR(255),
  algorithm             VARCHAR(50),
  secret                VARCHAR(255),
  PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';

CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);

and finally, quit:

quit;

Next we need to stop and disable the sytemd resolver running on machine.

systemctl disable systemd-resolved

systemctl stop systemd-resolved

Now we need to install pdns, the mysql backend and also pdns-recursor if we are running both a recursive and Authoritive DNS server. If you are just running an Authoritive DNS server then dont add in pdns-recursor.

apt-get install pdns-server pdns-recursor pdns-backend-mysql

Once that lot is installed we can go about configuring it to use MariaDB for the backend.

First i’d suggest removing the current ‘bind’ config:

rm -rf /etc/powerdns/pdns.d/bind.conf

Then create and open a new file:

/etc/powerdns/pdns.d/pdns.local.gmysql.conf

Put in the database details you setup earlier, like this:

# SQL Configuration (MariaDB)
# Launch gmysql backend
launch=gmysql
# gmysql parameters
gmysql-host=localhost
gmysql-port=3306
gmysql-dbname=powerdns
gmysql-user=powerdns
gmysql-password=pasword
gmysql-dnssec=yes

Next lets rename the default config, we dont want to remove it as its a good resource for checking out diferent settings:

mv /etc/powerdns/pdns.conf /etc/powerdns/pdns.conf-orig

Now create a new pdns.conf with the following settings:

# PowerDNS configuration file
# Replace ns1.example.com with your primary nameserver's hostname
default-soa-name=ns1.example.com
include-dir=/etc/powerdns/pdns.d
launch=
security-poll-suffix=
setgid=pdns
setuid=pdns
# Starts the API service
api=yes
# Sets webserver ad API to listen on specific address
webserver-address=192.168.0.70
# Allows access to the web server and api server to only these IP ranges.
webserver-allow-from=192.168.0.0/24,172.16.22.0/24
# Sets the port for the webserver
webserver-port=8081
# Enables the webserver
webserver=yes
# Replace <RANDOM_KEY> with a randomly generated key for API access
api-key=stupid-random-api-KEY
# Sets this server as the master server
master=yes
# Sets this server to NOT be a slave
slave=no
# Sets the serial of zones to default
default-soa-edit=INCEPTION-INCREMENT
# Defualt zone admin email
default-soa-mail=admin.haynet.host
# Only include the following 3 if you are running a recursive AND authoritive DNS server on the same machine
local-ipv6=
# Set the authoritive server to listen only on loopback interface
local-address=127.0.0.1
# Sets the authoritive servers listening port.
local-port=5300

I have commented on each section as to what they do. NOTE On the master server, leave ‘Master=yes’ and ‘slave=no’ On the secondary server, change it to ‘master=no’ slave=yes’ Also, if you arent going to run a recursive server, remove the last 3 settings:

# Only include the following 3 if you are running a recursive AND authoritive DNS server on the same machine
local-ipv6=
# Set the authoritive server to listen only on loopback interface
local-address=127.0.0.1
# Sets the authoritive servers listening port.
local-port=5300

Now restart PowerDNS:

systemctl restart pdns

It should restart fine, but likely wont be working properly yet.

Now we need to do the same for recursor.conf, rename it so we can reference it later if we need to:

mv /etc/powerdns/recursor.conf /etc/powerdns/recursor.conf-orig

Then create a new recursor.conf with the following entries:

# allow-from    If set, only allow these comma separated netmasks to recurse
allow-from=127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10
# api-key   Static pre-shared authentication key for access to the REST API
api-key=testing123
# config-dir    Location of configuration directory (recursor.conf)
config-dir=/etc/powerdns
# forward-zones Zones for which we forward queries, comma separated domain=ip pairs
forward-zones=example.com=127.0.0.1:5300
# hint file If set, load root hints from this file
hint-file=/usr/share/dns/root.hints
# include-dir   Include *.conf files from this directory
include-dir=/etc/powerdns/recursor.d
# local-address IP addresses to listen on, separated by spaces or commas. Also accepts ports.
local-address=0.0.0.0
# lua-config-file   More powerful configuration options
lua-config-file=/etc/powerdns/recursor.lua
# quiet Suppress logging of questions and answers
quiet=yes
# security-poll-suffix  Domain name from which to query security update notifications
# security-poll-suffix=secpoll.powerdns.com.
security-poll-suffix=
# setgid    If set, change group id to this gid for more security
setgid=pdns
# setuid    If set, change user id to this uid for more security
setuid=pdns
# webserver Start a webserver (for REST API)
webserver=yes
# webserver-address IP Address of webserver to listen on
webserver-address=0.0.0.0
# webserver-allow-from  Webserver access is only allowed from these subnets
webserver-allow-from=192.168.0.0/24, 172.16.22.0/24
# webserver-password    Password required for accessing the webserver
# webserver-password=
# webserver-port    Port of webserver to listen on
webserver-port=8082

Make sure you change the ‘example.com’ to the domain you are authoritive for. You can add extra lines underneath for me. Restart pdns-recursor with:

systemctl restart pdns-recursor

and you should now be able to get replies from your DNS servers.

MariaDB Replication – Master only

These settings apaply to the master only.

In this file:

/etc/mysql/mariadb.conf.d/50-server.cnf

make the following changes:

bind-address = 0.0.0.0
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
binlog_do_db = powerdns

Then restart MariaDB with:

systemctl restart mariadb

Next we need to add the slave user:

mysql -u root -p
GRANT REPLICATION SLAVE ON *.* TO 'pdns-secondary'@'<SECONDARY_SERVER_IP>' IDENTIFIED BY '<SECRET_PASSWORD>';

Remember to set the secondary IP and the secondary password.

FLUSH PRIVILEGES;

You can see if its working by running:

show master status;

Which will give you something like this:

+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 |      634 | powerdns     |                  |
+------------------+----------+--------------+------------------+

1 row in set (0.01 sec)

The ‘File’ and ‘Position’ are important, we will come back to them shortly.

MariaDB Replication – Slave server

On the slave server we are again editing the same file:

/etc/mysql/mariadb.conf.d/50-server.cnf

This time, editing to look like this:

server-id=2
relay-log=slave-relay-bin
relay-log-index=slave-relay-bin.index
replicate-do-db=powerdns

If you are adding more serveres, you can bump the server-id on each one.

Now restart MariaDB with:

systemtctl restart mariadb

Now is the time we need those two details from the ‘show master status’ so it would be wise to run it again. Take note of the ‘File’ and the ‘Position’. On the secondary, log into MariaDB.

mysql -u root -p

Enter the following, its all one line so copy it into notepad, edit it, then paste it into the MariaDB console.

change master to
master_host='<PRIMARY_SERVER_IP>',
master_user='pdns-secondary',
master_password='<SECRET_PASSWORD>', master_log_file='mysql-bin.000001', master_log_pos=634;

Here, change master_host to the IP of the master server, set the msater_password to the password of the secondary user you created earlier, set the master_log to the ‘file’ from master status, same as the position. Then start the slave:

start slave;

You can see the status of the slave with:

show slave status\G

It will show you an output a bit like this:

MariaDB [(none)]> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.0.253
Master_User: pdns-secondary
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000002
Read_Master_Log_Pos: 363730
Relay_Log_File: slave-relay-bin.000002
Relay_Log_Pos: 208937
Relay_Master_Log_File: mysql-bin.000002
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB: powerdns
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 363730
Relay_Log_Space: 209246
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
Master_SSL_Crl:
Master_SSL_Crlpath:
Using_Gtid: No
Gtid_IO_Pos:
Replicate_Do_Domain_Ids:
Replicate_Ignore_Domain_Ids:
Parallel_Mode: conservative
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
Slave_DDL_Groups: 0
Slave_Non_Transactional_Groups: 0
Slave_Transactional_Groups: 182
1 row in set (0.001 sec)

That is it for the PowerDNS config. You should now have 2 DNS servers in-sync which (if you chose) are both recursive and authoritive.

PowerDNS Admin setup

The final part of this is to setup PowerDNS Admin. I have opted to run this on a seperate server, so somethings like install mariadb you dont need to so if you are installing it on your NS1.

Firstly, install MariaDB:

apt-get install mariadb-server 

Log into MariaDB and create the database and user:

mysql -u root -p
CREATE DATABASE pdnsadmin CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON pdnsadmin.* TO 'pdnsadminuser'@'%' IDENTIFIED BY '<SECRET_PASSWORD>';
FLUSH PRIVILEGES;
quit;

Now install the dependencies:

apt-get install python3-dev libmariadb-dev-compat libmariadb-dev python-mysqldb libsasl2-dev libffi-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev pkg-config virtualenv -y

Next we need to install ‘yarn’

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
apt update && apt install yarn -y

Now we grab PowerDNS Admin:

wget https://github.com/ngoduykhanh/PowerDNS-Admin/archive/master.tar.gz

Create new directory for it:

mkdir /opt/web

Extract the archive into that directory:

tar -C /opt/web -xvf master.tar.gz

Rename the directory to make it a little cleaner:

mv /opt/web/PowerDNS-Admin-master /opt/web/powerdns-admin

Finally, change into that directory and setup the virtualenv.

virtualenv -p python3 flask

Now we activate that virtualenv:

. ./flask/bin/activate

and install the PowerDNS Admin requirements:

pip install -r requirements.txt

A quick note that it may look like the process has locked up on

"Collecting xmlsec>=0.6.0"

It hasnt, this just seems to take a while to install. Finally, we install our last dependency:

pip install python-dotenv

Now we need to rename the default config:

cp config_template.py config.py

and configure the databse with details you createed earlier in this section:

SQLA_DB_USER = 'pdnsadminuser'
SQLA_DB_PASSWORD = '<SECRET_PASSWORD>'
SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_NAME = 'pdnsadmin'
SQLALCHEMY_TRACK_MODIFICATIONS = True

Now we need to create the DB Schema:

export FLASK_APP=app/__init__.py
flask db upgrade

and initialise the DB.

flask db migrate -m "Init DB"

Now we need to generate the assets:

yarn install --pure-lockfile

and then build them:

flask assets build

Again this may take a little time.

Once that is done, you can check that the app runs without errors with:

./run.py

You should get an output like the following:

(flask) root@dnsadmin:/opt/web/powerdns-admin# ./run.py
 * 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-10-29 20:44:01,051] [INFO] |  * Running on http://127.0.0.1:9191/ (Press CTRL+C to quit)
[2019-10-29 20:44:01,053] [INFO] |  * Restarting with stat
[2019-10-29 20:44:02,798] [WARNING] |  * Debugger is active!
[2019-10-29 20:44:02,799] [INFO] |  * Debugger PIN: 355-347-408

You can stop it with CTRL + C.

Next we need to create the sytsemd service file:

Create the following file:

/etc/systemd/system/powerdns-admin.service

And add the following into it:

[Unit]
Description=PowerDNS-Admin
After=network.target
[Service]
User=root
Group=root
WorkingDirectory=/opt/web/powerdns-admin
ExecStart=/opt/web/powerdns-admin/flask/bin/gunicorn --workers 2 --bind unix:/opt/web/powerdns-admin/powerdns-admin.sock app:app
[Install]
WantedBy=multi-user.target

Now, run a daemon-reload, enable and start:

systemctl daemon-reload
systemctl start powerdns-admin
systemctl enable powerdns-admin

A ‘systemctl status powerdns-admin’ should look like this:

● powerdns-admin.service - PowerDNS-Admin
   Loaded: loaded (/etc/systemd/system/powerdns-admin.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2019-10-29 20:55:01 GMT; 21s ago
 Main PID: 5317 (gunicorn)
    Tasks: 3 (limit: 4915)
   Memory: 69.3M
   CGroup: /system.slice/powerdns-admin.service
           ├─5317 /opt/web/powerdns-admin/flask/bin/python3 /opt/web/powerdns-admin/flask/bin/gunicorn --workers 2 --bind unix:/opt/web/powerd
           ├─5320 /opt/web/powerdns-admin/flask/bin/python3 /opt/web/powerdns-admin/flask/bin/gunicorn --workers 2 --bind unix:/opt/web/powerd
           └─5321 /opt/web/powerdns-admin/flask/bin/python3 /opt/web/powerdns-admin/flask/bin/gunicorn --workers 2 --bind unix:/opt/web/powerd
Oct 29 20:55:01 dnsadmin.haynet.host systemd[1]: Started PowerDNS-Admin.
Oct 29 20:55:02 dnsadmin.haynet.host gunicorn[5317]: [2019-10-29 20:55:02 +0000] [5317] [INFO] Starting gunicorn 19.7.1
Oct 29 20:55:02 dnsadmin.haynet.host gunicorn[5317]: [2019-10-29 20:55:02 +0000] [5317] [INFO] Listening at: unix:/opt/web/powerdns-admin/powe
Oct 29 20:55:02 dnsadmin.haynet.host gunicorn[5317]: [2019-10-29 20:55:02 +0000] [5317] [INFO] Using worker: sync
Oct 29 20:55:02 dnsadmin.haynet.host gunicorn[5317]: [2019-10-29 20:55:02 +0000] [5320] [INFO] Booting worker with pid: 5320
Oct 29 20:55:02 dnsadmin.haynet.host gunicorn[5317]: [2019-10-29 20:55:02 +0000] [5321] [INFO] Booting worker with pid: 5321

The final part of the setup is to insatll NGINX:

apt-get install nginx

Create a new config file here:

/etc/nginx/conf.d/powerdns-admin.conf

And add the following into it, remembering to change the domain name:

server {
  listen *:80;
  server_name               <YOUR_DOMAIN_NAME>;
  index                     index.html index.htm index.php;
  root                      /opt/web/powerdns-admin;
  access_log                /var/log/nginx/powerdns-admin.local.access.log combined;
  error_log                 /var/log/nginx/powerdns-admin.local.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 /opt/web/powerdns-admin/app;
    location ~*  \.(jpg|jpeg|png|gif)

{
    expires 365d;
}
location ~\* ^.+.(css|js)

{
    expires 7d;
}
}
location / {
    proxy\_pass http://unix:/opt/web/powerdns-admin/powerdns-admin.sock;
    proxy\_read\_timeout 120;
    proxy\_connect\_timeout 120;
    proxy\_redirect off;
}

}

Get NGINX to check the config is sane with:

nginx -t

Then reload NGINX

systemctl reload nginx

That should be it. You should now be able to hit your PowerDNS Admin interface by hostname.

All you need to do now is put in 3 details in PowerDNS Admin to connect to your NS1 via its API.

First create an account on PowerDNS Admin by going to:

http://yourdomain/register

Then login, you will get a box asking you for:

  • PDNS API URL
  • PDNS API KEY
  • PDNS VERSION

Because i have PDNS listening on the outside interface, rather than the loopback adapter, i need to set the URL to the public IP of the master server.

The API key is set in the master’s PDNS.conf

The veresion i used was 4.1.6.

You can find the version with pdns –version on the master.

That should be it. You can now add domain via the interface and they get replicated out to slaves via MariaDB replication.