Monday, May 3, 2010

cPanel secondary master DNS without cPanel

At work I manage a cPanel server for over 200 clients that we needed to setup a secondary DNS server for. Since we wanted to use the machine for more then just backup DNS, and we wanted to run Debian, the cPanel DNS Only solution is no solution for us.

Here is how to slave cPanel to a Bind9 server without breaking cPanel or having cPanel undo the sync changes.

On the slave server I installed bind (apt-get install bind9) and made a directory "/Scripts" where I write two scripts, one is a class to wrap up cPanel API functions, the other is the actual DNS zone sync script. Both were written in PHP since it is my language of choice and is very easy to interface to the cPanel API with.

The class
=================================
<?PHP
        class cPanel {
                private $host;
                private $user;
                private $hash;

                private function __construct($host, $user, $hash) {
                        $this->host = $host;
                        $this->user = $user;
                        $this->hash = preg_replace("/[\t\r\n ]/", "", $hash);
                }

                private static $instance;
                public static function getInstance($host, $user, $hash) {
                        if (!(cPanel::$instance instanceof cPanel))
                                cPanel::$instance = new cPanel($host, $user, $hash);
                        return cPanel::$instance;
                }

                private function getJSON($request) {
                        $query = "https://{$this->host}:2087/json-api/" . $request;
                        $curl = curl_init();
                        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 1);
                        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1);
                        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
                        $header = array("Authorization: WHM {$this->user}:{$this->hash}");
                        curl_setopt($curl,CURLOPT_HTTPHEADER,$header);
                        curl_setopt($curl, CURLOPT_URL, $query);
                        $result = curl_exec($curl);
                        curl_close($curl);
                        if ($result == false)
                                throw new Exception("curl_exec threw error \"" . curl_error($curl) . "\" for $query");
                        return json_decode($result);
                }

                public function listzones() {
                        $info = $this->getJSON('listzones');
                        $zones = array();
                        foreach($info->zone as $zone)
                                $zones[$zone->domain] = $zone->zonefile;
                        return $zones;
                }
        }
?>

=================================
 

And the script:
=================================
#!/usr/bin/php
<?PHP ob_start(); ?
>
PUT YOUR cPanel ACCESS HASH HERE 
<?PHP
        $hash   = ob_get_clean();
        $master = '123.123.123.123'; //YOUR MASTER SERVER IP
        require('/Scripts/cPanel.php');
        $cpanel = cPanel::getInstance('www.YOUR_HOST.com', 'root', $hash);
        $zones = $cpanel->listzones();
        if (count($zones) == 0)
                throw new Exception('Unable to retrieve the server\'s zone list');

        $fp = fopen('/etc/bind/cpanel.zones', 'w');
        foreach($zones as $zone => $file) {
                $zone =
                        "zone \"{$zone}\" {\n" .
                        "       type slave;\n" .
                        "       file \"{$file}\";\n" .
                        "       masters { {$master}; };\n" .
                        "};\n";

                fwrite($fp, $zone);
        }
        fclose($fp);
        shell_exec('/usr/sbin/rndc reload');
?>

================================= 

I then ran the script to create the zone file, and then created a symlink in /etc/cron.hourly to the sync script, so every hour the zones are fetched from the server, the configuration is re-built and bind is reloaded.

Then I modified /etc/bind/named.conf.options with the following two options, the first for security, the second to accept notify events from the master server to tell us to update our records.

options {
        ........ default options ........

    recursion no;
    allow-notify { 123.123.123.123; };
};

replace 123.123.123.123 with the master server's ip address. Then I just had to make a change to /etc/bind/name.conf.local to include the zone list built from the master server.

include "/etc/bind/cpanel.zones"

And thats it for the slave... now the master needs a minor modification to tell the client to update and to allow zone transfers to the slave. Add the following two lines to the options section in /etc/named.conf

allow-transfer { 123.123.123.123; };
also-notify { 123.123.123.123; };

Again, replacing the 123.123.123.123 with the IP address of the slave server.

11 comments:

  1. Does this work with the current version of cPanel?

    In the 3rd paragraph you mention that you made a /Scripts directory where did you put this?

    ReplyDelete
    Replies
    1. Exactly where it says, just run "mkdir /scripts"
      And yes this works perfectly in the latest version of cPanel, it is still in use today.

      Delete
  2. Hi Geoffrey,
    I have tried to get this working, throws up a "Curl Exception"
    Installed PHP5-curl and put curl into php.ini, nothing,

    Is this script working for you, as of right now?

    ReplyDelete
    Replies
    1. Hi Burt,

      It is indeed still in use, what is exception you are getting?

      Delete
  3. PHP Warning: curl_error(): 5 is not a valid cURL handle resource in /scripts/cPanel.php on line 32
    Hi Geofrey,

    To be honest, you really have something good going, to the point where I would consider buying it, maybe I can get your help to fix up this Curl issue?

    PHP Fatal error: Uncaught exception 'Exception' with message 'curl_exec threw error "" for https://vps.*****.com.au:2087/json-api/listzones' in /scripts/cPanel.php:32
    Stack trace:
    #0 /scripts/cPanel.php(37): cPanel->getJSON('listzones')
    #1 /scripts/DNS.php(9): cPanel->listzones()
    #2 {main}
    thrown in /scripts/cPanel.php on line 32

    ReplyDelete
    Replies
    1. Off the top of my head, that error could be one of a two things:

      1) There is a firewall blocking access to port 2087 on the remote host.
      2) You do not have a valid SSL certificate for your FQDN, in which case, you need to change CURLOPT_SSL_VERIFYHOST and CURLOPT_SSL_VERIFYPEER to 0.

      As regards to selling this, feel free to email me personally (geoff at spacevs dot com) as I do not quite understand what you mean.

      Delete
  4. This works perfectly with the latest WHM and on Debian Wheezy.

    I put the two scripts together to make it a bit simpler and just located it in /etc/cron.hourly so it updates every hour.

    Thanks!

    ReplyDelete
    Replies
    1. Hi Josh,

      Great to hear, thanks for letting me know. We are still using this today on a few of our client's servers. We also use our secondary DNS servers as postfix MX backups, which is why we like to use Debian for these servers also.

      Delete
  5. Hi Goeffrey,

    I found your post while searching for ways to do that very same thing. One part I'm not sure I understand is the need for the hourly script run to generate the slave zone file if you have setup the slave server to be a bind slave and be notified by the master. Wouldn't the zone transfer that happens then be sufficient to get the new zone info across? What is the purpose of the hourly sync script in that context?

    Thanks
    Christian at gonetworkconsulting.com

    ReplyDelete
    Replies
    1. A DNS slave still needs the zones configured... the zone records will automatically transfer, but if a new zone is created on the DNS master, the slave wont know to sync the new zone until it is configured on the slave, this is what the hourly script does.

      Delete
    2. Thanks Geoffrey, that makes perfect sense!

      Delete