I have been an avid PowerDNS Authoritative Server (hereafter referred to as PowerDNS - there is also a separate Recursor available that we shall ignore for now - both available at http://www.powerdns.com) user since early in the 2.x series of releases. I replaced a global BIND infrastructure with PowerDNS for many reasons - instant provisioning to an easily replicated MySQL backend being the main one. PowerDNS is also RFC-compliant, powerful, and reliable.
The infrastructure I commissioned served well over 200,000 zones across three nameserver sites - each site receiving well over 100,000,000 queries per day. Two servers at each site, each with their own MySQL backend, replicated to from a hidden MySQL master to which we provisioned, handled this load with ease. Of course, this will depend entirely on the server specifications you use to host PowerDNS. PowerDNS offers support for multiple backends, however MySQL suits my needs well - I’m familiar with it, it’s more suited to DNS provisioning than LDAP (IMHO) and supports native replication (unlike PostgreSQL).
Rather than hacking a provisioning solution around BIND 9, moving to PowerDNS provided a technical advantage as well as a business advantage - customers could have their DNS data provisioned near-instantly - something that BIND 9 with a large number of zones and a cron’d rndc reconfig/reload would not achieve. PowerDNS 3.x introduces support for DNSSEC, something PowerDNS 2.x didn’t have - so it’s time to move to PowerDNS 3.x where possible.
I will use this article to walk through an installation of PowerDNS 3.2 from source on CentOS 6.3, perform basic configuration, load a basic zone, and serve the zone data authoritatively. This article will only scrape the surface of what PowerDNS has to offer, and further articles will be written in due course to cover interesting concepts in finer detail.
Have a good read over the manual available on the PowerDNS documentation site (http://doc.powerdns.com) too.
Preparation
Whilst not really required for a lab/test installation, I will chroot PowerDNS for security. In order to do this, I’ll create a dedicated logical volume to house the chroot. As I use VMware for all lab/test installs, I can add a new disk to an existing VM with ease. Whilst LVM and associated filesystem concepts are beyond the scope of a PowerDNS article, I’ll show you how I created the chroot filesystem for completeness. I added a new disk to the VM, which was presented as /dev/sdb.
First step - partition the disk with a single primary partition of type 8e (Linux LVM) spanning the entire disk:
|
1 2 3 |
# sfdisk /dev/sdb <<_EOF_ > 0,,8e > _EOF_ |
Next, create an LVM physical volume using this newly created partition:
|
1 |
# pvcreate /dev/sdb1 |
Create a new volume group, vg_data, utilising this PV:
|
1 |
# vgcreate vg_data /dev/sdb1 |
Then create a 5GB logical volume in the vg_data VG:
|
1 |
# lvcreate -L 5G -n lv_pdns vg_data |
Create an ext4 filesystem on the new logical volume:
|
1 |
# mkfs -t ext4 /dev/vg_data/lv_pdns |
Create a directory for the mountpoint:
|
1 |
# mkdir -p /var/chroot/pdns |
Add an entry to /etc/fstab:
|
1 2 |
# vi /etc/fstab <code>/dev/vg_data/lv_pdns /var/chroot/pdns ext4 defaults 1 2</code> |
Mount the filesystem, and verify:
|
1 2 |
# mount /var/chroot/pdns # df -hT /var/chroot/pdns |
Software Installation
Presuming that you’ve installed a Minimal CentOS 6.3 installation, install the required prerequisite packages as follows:
|
1 2 3 4 |
# yum install -y mysql-server mysql-devel # yum install -y boost boost-devel # yum install -y gcc-c++ libstdc++-devel make # yum install -y lua-devel |
Compile PowerDNS 3.2. This assumes that you’ve placed the source tarball under /usr/local/src:
|
1 2 3 4 5 6 7 |
# cd /usr/local/src # tar xzf pdns-3.2.tar.gz # cd pdns-3.2 # ./configure --with-modules="gmysql" --prefix=/usr/local/pdns-3.2 # make # make install # ln -s /usr/local/pdns-3.2 /usr/local/pdns |
Note that we enable the gmysql module - which will compile in MySQL support and allow backend database connectivity. PowerDNS is installed to /usr/local/pdns-3.2,/usr/local/<somepackage>-<, being my preferred installation location for custom software. Don’t get me on an /opt rant; it’s lame, like /srv. But … I digress. I create a symlink back to the versioned software from /usr/local/<somepackage>. This enables very simple upgrades: compile and install the new software revision, stop the software, change the link to point to the new installation, start the software. Rollback is obviously just as easy as it’s the reverse procedure.
Software Configuration
Populate the base MySQL system tables, and configure MySQL to start on server reboot:
|
1 2 3 |
# mysql_install_db # chkconfig mysqld on # chkconfig --list mysqld |
Start MySQL:
|
1 |
# service mysqld start |
Set the MySQL root password and verify that you can connect:
|
1 2 3 |
# mysqladmin -u root password 'newpassword' # mysql -u root -p mysql> exit |
Add a user and group for PowerDNS. This is a security feature present in most good software nowadays that needs to bind to a privileged port. PowerDNS will drop privileges down to the user and group we create here once it’s bound (and configured within PowerDNS):
|
1 2 |
# groupadd -g 5353 pdns # useradd -M -d /dev/null -s /bin/bash -u 5353 -g pdns pdns |
And chown our chroot:
|
1 |
# chown pdns:pdns /var/chroot/pdns |
Edit /usr/local/pdns/etc/pdns.conf as follows (whilst reading the documentation available on the PowerDNS site so that you understand what is going on):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# cd /usr/local/pdns/etc # cp pdns.conf-dist pdns.conf # vi pdns.conf # grep '^[^#]' pdns.conf chroot=/var/chroot/pdns daemon=yes launch=gmysql master=yes setgid=pdns setuid=pdns gmysql-host=127.0.0.1 gmysql-user=pdns gmysql-password=p0w3rdns gmysql-dbname=powerdns |
Fairly self-explanatory but the main points to note are that we’re chrooting PowerDNS and dropping privileges as previously detailed, daemonising, acting as a master, and using the MySQL backend. ALWAYS use gmysql - the older mysql backend is no longer supported and will not work as expected. Ensure that the file is locked down as it contains the PowerDNS user’s MySQL password in clear text:
|
1 2 |
# chown pdns:pdns /usr/local/pdns/etc/pdns.conf # chmod 600 /usr/local/pdns/etc/pdns.conf |
There are many more configuration variables available for customisation and I suggest you read up thoroughly on what they do. A lot of them can change the way PowerDNS handles requests (out-of-zone data, for example) or can impact performance. Do not steer away from the defaults unless you know what you are doing.
Starting PowerDNS for the First Time
Even though we haven’t created the backend database yet, let’s start PowerDNS and see what happens. We’ll configure PowerDNS to start via a script in /etc/init.d soon enough - for now just start it up:
|
1 2 3 4 5 6 7 8 |
# cd /usr/local/pdns/sbin # ./pdns_server Feb 2 01:17:59 dolan pdns[1478]: Exiting because communicator thread died with error: Unable to launch gmysql connection: Unable to connect to database: Access denied for user 'pdns'@'localhost' (using password: YES) Feb 2 01:17:59 dolan pdns[1478]: TCP server is unable to launch backends - will try again when questions come in: Unable to launch gmysql connection: Unable to connect to database: Access denied for user 'pdns'@'localhost' (using password: YES) Feb 2 01:17:59 dolan pdns[1478]: About to create 3 backend threads for UDP Feb 2 01:17:59 dolan pdns[1478]: gmysql Connection failed: Unable to connect to database: Access denied for user 'pdns'@'localhost' (using password: YES) Feb 2 01:17:59 dolan pdns[1478]: Caught an exception instantiating a backend: Unable to launch gmysql connection: Unable to connect to database: Access denied for user 'pdns'@'localhost' (using password: YES) Feb 2 01:17:59 dolan pdns[1478]: Cleaning up |
Kaboom! Expected, but good to see that our compile and install were successful, and the gmysql backend works.
Database Creation
Create a database and MySQL user using the same credentials and database name entered into the various gmysql-* variables in /usr/local/pdns/etc/pdns.conf:
|
1 2 3 4 |
# mysql -u root -p mysql> create database powerdns; mysql> grant all on powerdns.* to 'pdns'@'localhost' identified by 'p0w3rdns'; mysql> exit |
The sample MySQL schema is provided in the PowerDNS Manual (same documentation link as before - Section 4). Connect as our newly created pdns user, and apply the schema:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# mysql -u pdns -p'p0w3rdns' -h localhost powerdns mysql> 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; mysql> CREATE UNIQUE INDEX name_index ON domains(name); mysql> 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, primary key(id) ) Engine=InnoDB; mysql> CREATE INDEX rec_name_index ON records(name); mysql> CREATE INDEX nametype_index ON records(name,type); mysql> CREATE INDEX domain_id ON records(domain_id); mysql> CREATE TABLE supermasters ( ip VARCHAR(25) NOT NULL, nameserver VARCHAR(255) NOT NULL, account VARCHAR(40) DEFAULT NULL ) Engine=InnoDB; mysql> exit |
This article is not a discussion of MySQL, and if you don’t understand the above you should consult a good MySQL tutorial to get a grasp of the foundations of both MySQL and SQL itself.
Startup and Verification
PowerDNS should still be running, so kill it, and start it up again:
|
1 2 3 4 5 |
# pkill -f pdns # ps -ef | grep '[p]dns' # /usr/local/pdns/sbin/pdns_server # ps -ef | grep '[p]dns' pdns 1530 1 0 01:36 ? 00:00:00 /usr/local/pdns/sbin/pdns_server |
It’s worth noting here that PowerDNS has correctly dropped the privileges down to the pdns user as configured.
Checking /var/log/messages shows the successful startup:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Feb 2 01:36:05 dolan pdns[1529]: Reading random entropy from '/dev/urandom' Feb 2 01:36:05 dolan pdns[1530]: This is a standalone pdns Feb 2 01:36:05 dolan pdns[1530]: Listening on controlsocket in '/var/run/pdns.controlsocket' Feb 2 01:36:05 dolan pdns[1530]: UDP server bound to 0.0.0.0:53 Feb 2 01:36:05 dolan pdns[1530]: TCP server bound to 0.0.0.0:53 Feb 2 01:36:05 dolan pdns[1530]: PowerDNS 3.2 (C) 2001-2013 PowerDNS.COM BV (Feb 1 2013, 21:44:43, gcc 4.4.6 20120305 (Red Hat 4.4.6-4)) starting up Feb 2 01:36:05 dolan pdns[1530]: PowerDNS comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it according to the terms of the GPL version 2. Feb 2 01:36:05 dolan pdns[1530]: Chrooted to '/var/chroot/pdns' Feb 2 01:36:05 dolan pdns[1530]: Creating backend connection for TCP Feb 2 01:36:05 dolan pdns[1530]: Master/slave communicator launching Feb 2 01:36:05 dolan pdns[1530]: About to create 3 backend threads for UDP Feb 2 01:36:05 dolan pdns[1530]: No new unfresh slave domains, 0 queued for AXFR already Feb 2 01:36:05 dolan pdns[1530]: No master domains need notifications Feb 2 01:36:05 dolan pdns[1530]: Done launching threads, ready to distribute questions |
Verify the installation with a query for version.bind:
|
1 2 |
# dig CH TXT version.bind @localhost +short "Served by POWERDNS 3.2 $Id: packethandler.cc 3022 2013-01-05 13:00:10Z peter $" |
Good - it works. I don’t like all that information being exposed, however, so I’ll modify the version-string parameter in pdns.conf and restart:
|
1 2 3 4 5 |
# cd /usr/local/pdns/etc # vi pdns.conf version-string="SuperDNS Version 1.5.2" # pkill -f pdns # /usr/local/pdns/sbin/pdns_server |
Try the query again:
|
1 2 |
# dig CH TXT version.bind @localhost +short "SuperDNS Version 1.5.2" |
Perfect. To verify the server is running, you can also send it a ping, to which you should receive a PONG. No PONG == bad:
|
1 2 |
# /usr/local/pdns/bin/pdns_control rping PONG |
Loading Your First Zone
Let’s load trusty example.com into the database. zone2sql, provided with PowerDNS, will read RFC-compliant zonefiles and convert them to MySQL statements. So we can see what’s going on, I’ll execute the SQL statements manually (presuming you’re connected to the database via the MySQL monitor already):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
mysql> INSERT INTO domains (name, type) values ('example.com', 'NATIVE'); mysql> INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'example.com', mysql> INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'example.com','ns1.example.com','NS',86400,NULL); mysql> INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'example.com','192.168.0.1','A',120,NULL); mysql> INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'www.example.com','192.168.0.1','A',120,NULL); mysql> INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'mail.example.com','192.168.0.1','A',120,NULL); mysql> INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'example.com','mail.example.com','MX',120,25); |
Run some queries and verify that all is well:
|
1 2 3 4 5 6 7 8 |
# dig SOA example.com @localhost +short ns1.example.com. hostmaster.example.com. 2013020201 10800 3600 604800 3600 # dig NS example.com @localhost +short ns1.example.com. # dig A www.example.com @localhost +short 192.168.0.1 # dig MX example.com @localhost +short 25 mail.example.com. |
Provisioned near-instantly - this is a good thing and this is why I moved away from BIND for high-demand and highly-variable customer-driven DNS hosting. All that pointing and clicking (and of course sanity checking and verifying somewhere in the middleware before committing blindly to the database at a user’s complete whim) can lead to a lot of changes on the backend.
Finishing Up
Let’s stop PowerDNS again, and configure the /etc/init.d script so that it’s properly started upon reboot under the watchful gaze of a guardian process (run a ps -ef after starting with /etc/init.d/pdns to see what I mean) - the guardian will restart pdns_server-instance if it fails:
|
1 2 3 4 5 6 7 8 |
# cp /usr/local/src/pdns-3.2/pdns/pdns /etc/init.d # chmod +x /etc/init.d/pdns # chkconfig --add pdns # pkill -f pdns # service pdns start Starting PowerDNS authoritative nameserver: started # service pdns status 1674: Child running on pid 1676 |
Note that the /etc/init.d script is configured and provided at software build time.
It’s worth also noting that /etc/init.d/pdns dump will provide a lot of useful (and parseable) information about the instance (I’ll run a query to bump the counters):
|
1 2 3 4 5 6 |
# /etc/init.d/pdns dump corrupt-packets=0,deferred-cache-inserts=0,deferred-cache-lookup=0,latency=0,packetcache-hit=0,packetcache-miss=0,packetcache-size=0,qsize-q=0,query-cache-hit=0,query-cache-miss=0,recursing-answers=0,recursing-questions=0,servfail-packets=0,tcp-answers=0,tcp-queries=0,timedout-packets=0,udp-answers=0,udp-queries=0,udp4-answers=0,udp4-queries=0,udp6-answers=0,udp6-queries=0, # dig MX example.com @localhost +short 25 mail.example.com. # /etc/init.d/pdns dump corrupt-packets=0,deferred-cache-inserts=0,deferred-cache-lookup=0,latency=0,packetcache-hit=0,packetcache-miss=1,packetcache-size=0,qsize-q=0,query-cache-hit=0,query-cache-miss=4,recursing-answers=0,recursing-questions=0,servfail-packets=0,tcp-answers=0,tcp-queries=0,timedout-packets=0,udp-answers=1,udp-queries=1,udp4-answers=1,udp4-queries=1,udp6-answers=0,udp6-queries=0, |
Conclusion
This article has illustrated how to install and configure a basic PowerDNS instance serving authoritative DNS data. You can see how simple it’d now be to replicate the MySQL database to another server, and another PowerDNS instance, and quickly provision multiple slave servers.
PowerDNS really is my preferred DNS server for DNS hosting. For Corporate DNS, both internal and external, I still prefer BIND 9.
Stay tuned to tokiwinter.com for more articles around DNS servers, particularly PowerDNS, BIND 10 and DNSSEC.