For a long time, I served secondary DNS for Plesk domains using a non-Plesk server, but I was manually creating the zones on the secondary DNS machine. Too much “hands-on,” and too many places for human error. I took a closer look at automating the solution, using a Plesk server as primary DNS, and a non-Plesk server as secondary DNS. This article describes my solution.

To keep things simple, I’m describing the solution for a two server, master-to-slave setup. However, this can be easily adapted to a many-to-one or one-to-many solution.

Needed for this implementation:

1) A recent (v.10 or later) Plesk server, acting as a web host and primary DNS. My example is based on Plesk for Linux. I believe it will also work for a Plesk server running on a Windows server, but the firewall issues are probably more challenging, since the Plesk/Windows firewall implementation isn’t as granular as Plesk with iptables firewall rules on Linux.

2) A Linux server running BIND, acting as a secondary DNS. My own solution uses a dedicated CentOS 6.3 server, but this can be adapted to other Linux flavors running BIND, including virtual and cloud solutions.

3) Preferably, root access to the secondary DNS machine, since this automation requires creating a shell script, a PHP script that will run from the shell, firewall exceptions, and remote access the Plesk’s MySQL psa database on the Plesk Server from the secondary server.

Setting up appropriate firewall rules and a limited access MySQL user account is strongly recommended. If you don’t know how to safely open up the Plesk firewall to allow only the secondary DNS server to access the psa database, via a MySQL account with very limited permissions, do some additional study or hire an expert to do it for you. Taking security shortcuts here invites trouble.

Other than a Plesk firewall change to allows MySQL access from the secondary DNS machine, and the MySQL user setup, nothing needs to happen on the Plesk server where the hosting and primary DNS is taking place. You run it like any Plesk server, adding and deleting subscriptions/domains in the normal course of business. The secondary DNS machine will run scripts that will keep secondary DNS in synch with the Plesk server.

In the example I’ll show, 192.168.10.12 is our Plesk web server and a primary DNS. 10.10.1.2 is our CentOS server acting as the secondary DNS in a different data center.

We need to add one critical line to BIND’s named.conf file on the secondary server:

options {
	directory "/etc";
	pid-file "/var/run/named/named.pid";
	allow-notify {10.10.1.2;};
	listen-on port 53 {
		10.10.1.2;
		};
	recursion no;
	};

zone "." {
	type hint;
	file "/etc/db.cache";
	};

// zone include file generated by /dns-zone-includes/check-dns-master.sh cron job:
include "/dns-zone-includes/plesk-primary.zones";
zone "myfirstdomain.com" { type slave; file "/var/named/slaves/myfirstdomain.com.hosts"; masters { 192.168.10.12; }; };
zone "anotherdomain.com" { type slave; file "/var/named/slaves/anotherdomain.com.com.hosts"; masters { 192.168.10.12; }; };
zone "thelastdomain.com" { type slave; file "/var/named/slaves/thelastdomain.com.hosts"; masters { 192.168.10.12; }; };
logging {
};

The added line to named.conf is line 17. That include statement tells BIND to include a file specified by the path/filename, as if it were part of named.conf.

With the include file, we have an easy way to manage a list of valid zones for the secondary DNS, without editing the secondary’s named.conf. We just need a process to build the include file with the domains we want to serve secondary DNS for.

How do we generate the list of domains into that include file? From the Plesk primary server’s MySQL database, which holds a list of domains on the Plesk server. We can query that database from the secondary server, build the include file, save it, and reload the secondary DNS zone file as often as needed, including on a scheduled basis.

Let’s examine the simple script that manages this process, called “check-dns-master.sh”:

# run PHP script that checks zones at Plesk master DNS
php /dns-zone-includes/build-dns-zones.php
# if we exited with a code of 1, new DNS include file was built, so reload DNS config
if [[ $? -eq 1 ]]; then
    /usr/sbin/rndc reload
fi

The shell script calls a PHP script that does the database query and all of the include file building. It compares the Plesk domain list from the primary server’s MySQL psa database with the zones currently in the include file. If needed, it rebuilds the include file to account for any new or deleted domains. Here’s the build-dns-zones.php script:

"; // "\n";

if(!empty($_GET['messages']))
        echo "DNS secondar zone include file build, process started: ".Date($dateformat)."
"; // Plesk database connection info for the Primary DNS server; the MySQL database user should have only select privileges on the Plesk psa database dns_zone table $dbhostname = '192.168.10.12'; $dbuser='psazones'; $dbpw='mypassword1234'; $dbname='psa'; // this is the list of master DNS servers that can update the secondary DNS zones $masters="192.168.10.12;"; // this is the text file we will build from our zone query; file can be anywhere on server if run from shell (no open_basedir restrictions); $dnsincludefile="/dns-zone-includes/plesk-primary.zones"; // open the database connection, stopping the script if there is a problem connecting mysql_connect($dbhostname,$dbuser,$dbpw); if (!mysql_select_db($dbname)) die("An error occurred while accessing the database.\n"); // query the Plesk dns_zone table, getting a list of all zone names // check the zones from the Plesk master against the current zone include file; if no difference between the two, we can stop processing if(file_exists($dnsincludefile)) { $handle=fopen($dnsincludefile, 'r'); // build a list of zones from the previously generated zone include file while($textline = fgets($handle)) if($textline[0] != "#") { $startpos=strpos($textline,"/slaves/")+8; $endpos=strpos($textline,".hosts"); $zones[]=substr($textline,$startpos,$endpos-$startpos); } fclose($handle); // debug info: if(!empty($_GET['messages'])) { echo "Include file zone count: ".count($zones)."
"; // foreach($zones as $zoneline) //echo $zoneline.$cr; } $dbzonecount=0; $result=mysql_query("select name from dns_zone where id <> 1 order by name"); while ($dbrow = mysql_fetch_array($result)) { if(!empty($_GET['messages'])) echo "db: ".$dbrow['name']." / file: ".$zones[$dbzonecount]."
"; if($dbrow['name'] != $zones[$dbzonecount]) // if a record from the zone database table doesn't match a corresponding record from the zone include file, a master DNS change took place: $dnschange = true; $dbzonecount++; } // debug info: if(!empty($_GET['messages'])) echo "Database zone count: ".$dbzonecount."
"; if($dbzonecount != count($zones)) // if the count from the zone database table doesn't match the count from the zone include file, a master DNS change took place: $dnschange = true; } else // there's no zone include file, so generate one $dnschange=true; if($dnschange) { // query the Plesk psa dns_zone table process the zone names returned by the query, exclude id = 1 because it is a duplicate/default zone $result=mysql_query("select name from dns_zone where id <> 1 order by name"); // if the query is successful, start processing if($result) { while ($dbrow = mysql_fetch_array($result)) { // build a string from the BIND zone records: $zonefiletext.="zone \"".$dbrow['name']."\" { type slave; file \"/var/named/slaves/".$dbrow['name'].".hosts\"; masters {".$masters."}; };\n"; $zonefiletextemail.="zone \"".$dbrow['name']."\" { type slave; file \"/var/named/slaves/".$dbrow['name'].".hosts\"; masters {".$masters."}; };
"; } // as a final check, make sure we actually have some zone records in our string before overwriting any previous version of the includes file if(!empty($zonefiletext)) { $zonefiletext="# this file was generated by ".$_SERVER['PHP_SELF']." on ".Date($dateformat)."\n".$zonefiletext; $zonefiletextemail="# this file was generated by ".$_SERVER['PHP_SELF']." on ".Date($dateformat)."
".$zonefiletextemail; $handle = fopen($dnsincludefile, 'w'); fwrite($handle, $zonefiletext); fclose($handle); // notify server admin that a new file has been generated: $headers="From: \n"; $headers.="MIME-Version: 1.0\nContent-type: text/html; charset=iso-8859-1"; mail('','New plesk-primary.zones file generated',$zonefiletextemail,$headers); // exit with a value of 1, letting the shell know we need to reload the BIND config if(!empty($_GET['messages'])) echo "new zone include file generated
"; $exitcode=1; } } } else if(!empty($_GET['messages'])) echo "no master DNS changes detected.".$cr; if(!empty($_GET['messages'])) echo "DNS secondar zone include file build, process ended: ".Date($dateformat)."
"; exit($exitcode); ?>

Don’t let the length of the PHP script put you off. Much of the code is for comparing the Plesk psa database list of domains with the domains contained in the existing include, in order to determine if a change happened since the previous include file creations, indicating a new include file is needed.

In my own implementation, the shell script runs every five minutes on the secondary DNS. If a difference between the Plesk server’s domain list is found when compared to the zone include file, a new zone include file is built. When that happens, the shell script receives a return value of “1” from the PHP script, letting the shell script know to reload BIND’s zones. The new domain’s individual zone file, on the Plesk server, will be transferred to the secondary DNS during the BIND reload. You can examine the “messages” log file on the Plesk server (/var/log/messages on CentOS) to verify the zones are transferring to the secondary DNS.

Two-server Plesk implementations keep DNS synchronized without this type of automation, with both servers acting as masters their own zones, and slaves for the other server’s zones. However, one of the advantages of the solution described here is not needing a Plesk license for a secondary DNS server. It also allows use of a very low powered (and low cost) secondary server, since this process requires very little processing power, RAM or drive space. In fact, this could be an ideal setup for running a secondary DNS in a minimal cloud or virtual server, running Linux, PHP and BIND. You could easily use this to set up additional DNS slaves on inexpensive resources across a large geographic area, for operational resiliency.

There is also no reason why this setup couldn’t be used to completely offload DNS from Plesk. In my own operation, I find it convenient to use Plesk’s DNS management tools and template capabilities as a primary DNS, to manage zones. It’s ideal for reseller setups. But if your operation requires completely separating DNS from Plesk servers, these scripts can be adopted for that purpose.