PowerDNS Logo

PowerDNS

PowerDNS is a versatile nameserver which supports a large number of different backends ranging from simple zonefiles to relational databases and load balancing/failover algorithms. PowerDNS tries to emphasize speed and security.

Prerequisites

IP Addresses

The server will need a dedicated private IPv4 address and global IPv6 address. See Network.

Troughout this document we will use 192.0.2.41 as IPv4 address and 2001:db8::41 as IPv6 address.

Also you need to know your public IPv4 address on your gateway. In this document we will use 198.51.100.240 as an example address.

Firewall-Gateway

The Router needs to forward incoming external IPv4 traffic on TCP and UDP port 53 to the private IPv4 address of this DNS server. IPv6 traffic on TCP and UDP port 53 to this servers IP address must be allowed to pass the firewall.

Database

Database Server must be installed and running.

Domain Registration

You need to register your domain with one of the official DNS registrars. The registrar must support IPv6, DNSSEC and allow you to define your own name servers.

Secondary Domain Name Servers

You will need a 3rd-Party providing you with secondary servers. The provider must support IPv6 records, NOTIFY, AXFR and DNSSEC on its servers.

Their DNS servers must be reachable over IPv6 and be able to receive DNS NOTIFY messages and transfer zones from the master over IPv6.

Software Installation

The PowerDNS server software is in the Ubuntu software package repository. We install the server and the MySQL database backend.

$ sudo apt-get install pdns-server pdns-backend-mysql

You will be asked for the password of the MySQL root user, so the database can be created.

The following happens during installation:

  • The following configuration file are created:

    • /etc/powerdns/pdns.conf.

    • /etc/default/pdns.

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

    • /etc/powerdns/pdns.d/pdns.simplebind.conf

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

  • A user and group pdns is created.

  • A Systemd service /lib/systemd/system/pdns.service is created and started.

Systemd Service Dependencies

Since we use MariaDB as database backend for PowerDNS, we want to ensure that PowerDNS is always able to connect to the database server before running.

The Systemd services file for the PowerDNS server /lib/systemd/system/pdns.service is created as part of the software package installation. The recommended method is to create a Systemd override-file and not change anything in the provided service file, as it would be lost on software package updates.

You can create a override file easily with the help of the systemctl command:

$ sudo systemctl edit pdns.service

This will start your editor with an empty file, where you can add your own custom Systemd service configuration options.

[Unit]
BindsTo=mariadb.service

The configuration statement After=mariadb.service ensures that mariadb.service is fully started up before the pdns.service is started. It is already included in the service file installed by the software package.

The line BindsTo=mariadb.service ensures that if the Database service is stopped, the PowerDNS server will be stopped too.

After you save and exit of the editor, the file will be saved as /etc/systemd/system/pdns.service.d/override.conf and Systemd will reload its configuration:

systemctl show pdns.service |grep -E "After=|BindsTo="

DNS Server Database

All our DNS data will stored in a MySQL database.

Remove BIND Backend

By default PowerDNS uses BIND style zone-files to store DNS data.

As only one backend can be active at any time and we installed the MySQL backend, we need to remove the default “simple BIND backend”. This is done simply by deleting its configuration file.

$ sudo rm /etc/powerdns/pdns.d/pdns.simplebind.conf

Database Server

The following setting needs to changed in /etc/powerdns/pdns.d/pdns.local.gmysql.conf:

# MySQL Configuration
#
# Launch gmysql backend
launch=gmysql

# gmysql parameters
#gmysql-host=localhost
#gmysql-port=
gmysql-dbname=pdns
gmysql-user=pdns
gmysql-password=********
gmysql-dnssec=yes
gmysql-socket=/var/run/mysqld/mysqld.sock

Preparing the database

Create a new emtpy database called pdns on the MySQL server:

$ mysqladmin -u root -p create pdns

Create a database user for PowerDNS-server to access the database:

$ mysql -u root -p
GRANT SELECT ON pdns.* TO 'pdns'@'127.0.0.1'
    IDENTIFIED BY '********';
FLUSH PRIVILEGES;
EXIT;

The file powerdns.sql contains the PowerDNS database structure as shown in the PoweDNS documentation Basic setup: configuring database connectivity:

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 DEFAULT NULL,
  account               VARCHAR(40) DEFAULT NULL,
  PRIMARY KEY (id)
) Engine=InnoDB;

CREATE UNIQUE INDEX name_index ON domains(name);


CREATE TABLE records (
  id                    INT 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;

CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
CREATE INDEX recordorder ON records (domain_id, ordername);


CREATE TABLE supermasters (
  ip                    VARCHAR(64) NOT NULL,
  nameserver            VARCHAR(255) NOT NULL,
  account               VARCHAR(40) NOT NULL,
  PRIMARY KEY (ip, nameserver)
) Engine=InnoDB;


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) NOT NULL,
  comment               VARCHAR(64000) NOT NULL,
  PRIMARY KEY (id)
) Engine=InnoDB;

CREATE INDEX comments_domain_id_idx ON comments (domain_id);
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;

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;

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;

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

ALTER TABLE `records` ADD CONSTRAINT `records_ibfk_1` FOREIGN KEY (`domain_id`)
REFERENCES `domains` (`id`) ON DELETE CASCADE;

To create this table structures in our new PowerDNS database:

$ mysql -u root -p pdns < powerdns.sql

Configuration

The following settings need to be changed in /etc/powerdns/pdns.conf:

REST API

Create a random string to be used as API key to access the server by other apps:

$ pwgen -cns 64 1
# cache-ttl=20

#################################
# chroot	If set, chroot to this directory for more security
#
# chroot=/var/spool/powerdns

#################################
# config-dir	Location of configuration directory (pdns.conf)
#
config-dir=/etc/powerdns

#################################
# config-name	Name of this virtual configuration - will rename the binary image
#
# config-name=

#################################
# control-console	Debugging switch - don't use

Allowed Zone Transfers

#################################
# allow-axfr-ips    If enabled, restrict zonetransfers to originate from these
#                   IP addresses
allow-axfr-ips=127.0.0.1 ::1 192.0.2.0/24 2001:db8::/64

#################################

Enable Zone Transfers

#################################
# disable-axfr	Disable zonetransfers but do allow TCP queries
#
disable-axfr=no

#################################

Server IP Address

#################################
# local-address	Local IP address to which we bind
#
local-address=192.0.2.41

#################################
# local-ipv6	Local IP address to which we bind
#
local-ipv6=2001:db8::41

#################################

Act as Master Server

#################################
# master	Act as a master
#
master=yes

#################################

Source Address

By default PowerDNS will use the last defined IP address as source address to send out DNS NOTIFY messages to slaves.

The slave servers, will not accept any NOTIFY messages, if they are not coming from the defined master server of a domain. Here is how we tell PowerDNS to use its dedicated IPv4 and IPv6 addresses for outgoing connections:

#################################
# query-local-address   The IP address to use as a source address for sending
#                       queries.
query-local-address=192.0.2.41
query-local-address6=2001:db8::41

#################################

Server Restart

$ sudo service pdns restart

Import Zone-Files

If you already have zone files, from previous DNS servers or 3rd-party providers, you can import them as follows:

$ zone2sql --zone=example.net.zone \
           --zone-name=example.net \
           --gmysql --transactions --verbose \
           > example.net.zone.sql
1 domains were fully parsed, containing 49 records
$ mysql -u root -p pdns < example.net.zone.sql
Enter password:

And done. Very easy.

Secondary Server

Let’s assume our master server has the IP address 2001:db8::41 and the new slave will have the IP address 2001:db8::42.

In the real world a DNS slave would be on entirely another subnet.

To set up a PowerDNS as secondary slave DNS server.

Install MariaDB and PowerDNS

See above. Also add the MySQL tables as above.

Copy the configuration file from the master and change following things:

Slave Server IP Addresses

#################################
# local-address Local IP address to which we bind
#
local-address=192.0.2.42

#################################
# local-ipv6    Local IP address to which we bind
#
local-ipv6=2001:db8::42

Setup PowerDNS as a Slave

#################################
# master    Act as a master
#
master=no
#################################
# slave Act as a slave
#
slave=yes

Restart the slave server:

$ sudo service pdns restart

Add Domain Record on Slave Server

Open a MySQL database server sesssion:

slave$ mysql -u root -p pdns

Add the the domain along with the IP address of the master server as follows:

INSERT INTO `domains` (`name`, `master`, `type`)
    VALUES('example.net', '2001:db8::41', 'SLAVE');

Add Slave Record on Master Server

Open a MySQL database server sesssion:

master$ mysql -u root -p pdns

Add a NS record and IP addresses of the new slave to the domain:

INSERT INTO `records` (`domain_id`, `name`, `type`, `content`)
    VALUES(
        (SELECT `id` FROM `domains` WHERE `name` = 'example.net'),
        'example.net',
        'NS',
        'ns2.example.net'
);
INSERT INTO `records` (`domain_id`, `name`, `type`, `content`)
    VALUES(
        (SELECT `id` FROM `domains` WHERE `name` = 'example.net'),
        'ns2.example.net',
        'A',
        '192.0.2.42'
);
INSERT INTO `records` (`domain_id`, `name`, `type`, `content`)
    VALUES(
        (SELECT `id` FROM `domains` WHERE `name` = 'example.net'),
        'ns2.example.net',
        'AAAA',
        '2001:db8::42'
);

Delete a Domain

Let say you want to remove the domain example.org completely.

DELETE FROM `domainmetadata` WHERE `domain_id` = (
    SELECT `id` FROM `domains` WHERE `name` = "example.org"
);
DELETE FROM `records` WHERE `domain_id` = (
    SELECT `id` FROM `domains` WHERE `name` = "example.org"
);
DELETE FROM `comments` WHERE `domain_id` = (
    SELECT `id` FROM `domains` WHERE `name` = "example.org"
);
DELETE FROM `cryptokeys` WHERE `domain_id` = (
    SELECT `id` FROM `domains` WHERE `name` = "example.org"
);
DELETE FROM `domains` WHERE `name` = "example.org";

This same procedure needs to be done on every master or slave sever.