Published on Sep 10 2020 in BIND DNS Virtualmin

One of possible slave DNS setups can be achieved using Virtuialmin hook for create/delete account and BIND notifications when only DNS records change. The two servers can then be used as ns1/ns2 for domains hosted on the first one where Virtualmin is running. The other one is DNS-only server.

Prepare 2 Centos 8 servers. Full hostnames: srv7.temporary-domain.net 10.10.10.80 and srv9.temporary-domain.net 10.10.10.90. srv7 will host Virtualmin (and master DNS server managed by VA), srv9 will be slave DNS updated via SSH/script (when new zone is created) or via DNS NOTIFY when zone is updated.

ns1.temporary-domain.net should be pointed with ‘A’ DNS record to IP of srv7.

ns2.temporary-domain.net should be pointed with ‘A’ DNS record to IP of srv9.

ns1.temporary-domain.net should be registered as nameserver with IP of srv7.

ns2.temporary-domain.net should be registered as nameserver with IP of srv9.

These ns1/ns2 can be used at the registrar as nameservers for domains hosted on srv7.

srv7# hostnamectl set-hostname srv7.temporary-domain.net

In real setup install Virtualmin on srv7 - this will install BIND but for now we will use generic bind.

srv7# yum -y install bind bind-utils
srv7# systemctl enable --now named
srv7# systemctl status named

In VA set nameservers to ns1.temporary-domain.net and ns2.temporary-domain.net. This can be done in post installation wizard or later. DNS zones created by Virtualmin will include these nameservers in NS records. Generate root’s key on master. We will copy it’s public part to srv9 later.

srv7# ssh-keygen -P "" -f ~/.ssh/id_rsa -C "id_rsa_default"

Install bind on slave

srv9# hostnamectl set-hostname srv9.temporary-domain.net
srv9# yum -y install bind bind-utils
srv9# systemctl enable --now named
srv9# systemctl status named

This dnssync user will have rights to update DNS configs

srv9# adduser dnssync
srv9# PASS=`openssl rand -base64 9`; echo $PASS && echo $PASS | passwd --stdin dnssync
srv9# sed -i -r 's/^(PasswordAuthentication).*/\1 yes/' /etc/ssh/sshd_config
srv9# systemctl restart sshd

srv7# ssh-copy-id dnssync@srv9.temporary-domain.net

srv9# sed -i -r 's/^(PasswordAuthentication).*/\1 no/' /etc/ssh/sshd_config
srv9# systemctl restart sshd

Make named listen on all interfaces

srv7# sed -i -r 's/(listen-on ).*/\1 port 53 { any; };/' /etc/named.conf
srv7# sed -i -r 's/(listen-on-v6 ).*/\1 port 53 { any; };/' /etc/named.conf
srv7# sed -i -r 's/(allow-query ).*/\1 { any; };/' /etc/named.conf

Adjust master named.conf and use SRV9 IP

srv7# sed -i -r 's|(options \{)|\1\n\tallow-transfer { 10.10.10.90; }; // transfer to slave\n|' /etc/named.conf
srv7# sed -i -r 's|(options \{)|\1\n\talso-notify { 10.10.10.90; }; // notify\n|' /etc/named.conf

The above commands modifying named.conf may differ slightly when VA is installed as VA may insert its own versions of the directives.

Here you need to use SRV7 IP in from=

srv9# sed -i -e 's|^|from="10.10.10.80",command="/usr/bin/sudo -E /usr/local/sbin/zone_updater.sh" |' /home/dnssync/.ssh/authorized_keys

Make named listen on all interfaces

srv9# sed -i -r 's/(listen-on ).*/\1 port 53 { any; };/' /etc/named.conf
srv9# sed -i -r 's/(listen-on-v6 ).*/\1 port 53 { any; };/' /etc/named.conf
srv9# sed -i -r 's/(allow-query ).*/\1 { any; };/' /etc/named.conf
srv9# sed -i -r 's|(options \{)|\1\n\tmasterfile-format text;\n|' /etc/named.conf

Allow user dnssync to run updater with root rights

srv9# cat >/etc/sudoers.d/dnssync<<'EOF'
dnssync ALL = (root) NOPASSWD:SETENV:/usr/local/sbin/zone_updater.sh *
EOF

Zone updater script for slave

srv9# cat >/usr/local/sbin/zone_updater.sh<<'EOF'
#!/bin/bash

IFS=' ' read SUDOCOM ENVSWITCH COM DOMAIN IP ACTION <<< $SSH_ORIGINAL_COMMAND
LOG=/var/log/zone_updater.log
echo `date`" $SSH_ORIGINAL_COMMAND" >> $LOG
echo $DOMAIN | grep -qP '(?=^.{5,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)'

if [ $? -ne 0 ]; then
    echo "$DOMAIN is not a valid domain" >> $LOG
    exit 1
fi

if [ "$ACTION" != "add" -a "$ACTION" != "del" ]; then
    echo "$ACTION is not a valid action" >> $LOG
    exit 1
fi

if [ "$ACTION" == "add" ]; then
    # update zone file no matter if the zone is active or not
    cat - > /var/named/${DOMAIN}.hosts
    chown named: /var/named/${DOMAIN}.hosts
    chmod 644 /var/named/${DOMAIN}.hosts

    # activate zone if missing
    grep -q -P "zone \"${DOMAIN}\" \{" /etc/named.conf
    if [ $? -ne 0 ]; then
        echo "Adding zone $DOMAIN to named.conf" >> $LOG
        echo -e "zone \"$DOMAIN\" { type slave; masters { $IP; }; file \"/var/named/$DOMAIN.hosts\"; };" >> /etc/named.conf
    fi
fi

if [ "$ACTION" == "del" ]; then
    [ -f /var/named/${DOMAIN}.hosts ] && rm -f /var/named/${DOMAIN}.hosts
    echo "Removing zone $DOMAIN from named.conf" >> $LOG
    perl -i -0777 -spe '{$_ =~ s/^zone "$d" \{.*?\};.*?\};\n//ms}' -- -d=$DOMAIN /etc/named.conf
fi

rndc reload
echo  "rndc reload RC = $?" >> $LOG
EOF

srv7# chmod +x /usr/local/sbin/zone_updater.sh

DNS sync script

srv7# cat>/usr/local/sbin/dns_sync.pl<<'EOF'
#!/usr/bin/perl -w
use strict;
use 5.010;

unless ($ARGV[0] and $ARGV[0] =~ /(?=^.{5,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)/ 
        and $ARGV[1] and ($ARGV[1] eq 'add' or $ARGV[1] eq 'del')) {
    say "Usage: $0 domain.com add|del";
    say "This script sync master zone to slave server for domain.com";
    exit 1;
}

my $domain = $ARGV[0];
my $hostname = `hostname -f`; chomp $hostname;
my $ip = `hostname -I`; $ip =~ s/\s+$//; # chomp $ip;
my $action = $ARGV[1];

my $config;
open F, "/etc/webmin/virtual-server/config" or die "$!";
{ local $/; $config = <F>; }

my ($master) = $config =~ /^bind_master=(.*?)$/ms;
my (@slaves) = $config =~ /^dns_ns=(.*?)$/msg;

say "master = $master, slaves = ".join(" ",@slaves);

# create
if (-f "/var/named/$domain.hosts" && $action eq 'add') {
    for my $ns (@slaves) {
        system("cat /var/named/$domain.hosts | \
        ssh -p2017 -v dnssync\@$ns \"/usr/bin/sudo -E /usr/local/sbin/zone_updater.sh ${domain} ${ip} ${action}\" 2>/dev/null;");
    }
}

# delete
if ($action eq 'del') {
    for my $ns (@slaves) {
        system("cat /dev/null | \
        ssh -p2017 -v dnssync\@$ns \"/usr/bin/sudo -E /usr/local/sbin/zone_updater.sh ${domain} ${ip} ${action}\" 2>/dev/null;");
    }
}
EOF
srv7# chmod +x /usr/local/sbin/dns_sync.pl

Virtualmin hook

Settings -> Virtualmin Config -> Actions upon user and server creation -> Command to run after making changes to a server: /usr/local/bin/post_virtual_modification.sh

It will be called at new domain creation and will call /usr/local/bin/dns_sync.pl This hook does not run for DNS modification so we need to use other method: DNS notify (master notifies slave automatically).

srv7# cat >/usr/local/bin/post_virtual_modification.sh<<'EOF'
#!/bin/bash

LOG=/var/log/`basename $0`.log

if [ $VIRTUALSERVER_ACTION == "CREATE_DOMAIN" ]; then
    echo `date`" /usr/local/sbin/dns_sync.pl $VIRTUALSERVER_DOM add" >> $LOG
    /usr/local/sbin/dns_sync.pl $VIRTUALSERVER_DOM add
elif [ $VIRTUALSERVER_ACTION == "DELETE_DOMAIN" ]; then
    echo `date`" /usr/local/sbin/dns_sync.pl $VIRTUALSERVER_DOM del" >> $LOG
    /usr/local/sbin/dns_sync.pl $VIRTUALSERVER_DOM del
fi
EOF
srv7# chmod +x /usr/local/bin/post_virtual_modification.sh