Managing highly available DNS clusters is a critical requirement in resilient hosting infrastructures. Within the HestiaCP ecosystem, each new server provisioned with the BIND9 DNS daemon can act as either a primary (Master) or secondary (Slave) node. Updates made on a master node must be replicated quickly and securely to secondary nameservers (ns01 and ns02) to maintain data integrity and ensure rapid resolution responses to public DNS queries.
In this detailed technical guide, we document the step-by-step procedure to integrate and authorize new servers in your HestiaCP-managed DNS cluster. This covers panel-level API whitelisting, key security adjustments in the BIND daemon configurations (named.conf.options), network security rules, and the CLI utilities required to register, synchronize, and test zone propagation.
1. Topology and cluster data path#
Each new server added to the HestiaCP cluster acts as the primary authority (Master) for the DNS zones created under its accounts. All initial zone records are updated locally on this server. HestiaCP then handles zone transfers via the AXFR protocol (dig @server AXFR) using its internal REST API to trigger asynchronous synchronization to the secondary nameservers (ns01 and ns02).
This real-time replication flow operates under the following data paths:
- Replication triggered by external integrations (e.g., API / WHMCS):
WHMCS -> API -> Master Server -> NS01/NS02 (real-time sync)
- Replication executed directly from the HestiaCP web interface:
Master Server -> NS01/NS02 (real-time sync)
Zone propagation updates are applied only on the configured secondary nameservers, keeping the records synchronized across the public authoritative DNS servers.
2. Cluster node prerequisites and security checks#
Before enabling a new server in the HestiaCP DNS cluster, verify that you have gathered the following networking details and credentials:
- Internal IP Addresses: The local/internal IP mapping of the servers (essential so that API and BIND socket traffic are restricted to private RFC 1918 networks).
- API User: For cluster replication, HestiaCP utilizes the default system administrator account (
admin) or custom restricted API keys. - HTTPS API Communication: Ensure that the API communication occurs strictly over secure HTTPS connections (Port 8083). If your secondary server does not have a valid SSL certificate for the panel hostname, configure it beforehand:
v-add-letsencrypt-host admin your-panel-hostname
- dns-cluster User Audit on Slave: HestiaCP stores and isolates the replicated zones under the special
dns-clustersystem user on the secondary servers. Before registering the remote host, check if this user exists on the Slave server (ns01/ns02):
# Check if the user exists on the secondary server
sudo /usr/local/hestia/bin/v-list-user dns-cluster
# If it doesn't exist, create it
sudo /usr/local/hestia/bin/v-add-user dns-cluster 'YOUR_SECURE_PASSWORD'
sudo /usr/local/hestia/bin/v-add-user-dns-cluster dns-cluster
3. Authorizing HestiaCP API communication#
To prevent API connection refusals, the IP addresses of the secondary servers (ns01 and ns02) must be whitelisted in the API configuration of the new Master server, and vice versa.
Step 1: Whitelisting the master server on the secondary nodes (ns01/ns02)#
- Log in to the HestiaCP panel on port
8083of each secondary DNS server using theadminaccount via HTTPS. - In the top-right menu, click Server Settings (gear icon) and select Configure.
- Select the Security tab -> System -> API.
- Locate the Allowed IP addresses for API field.
- Add a new line containing the internal IP address of the new Master server.
- Click Save.
Step 2: Whitelisting the secondary nodes on the master server#
On the panel of the new Master server being added to the cluster, repeat the process to authorize API calls originating from the secondary nodes.
- Access the HestiaCP panel of the new Master server (
https://your-vps:8083). - Navigate to Server Settings >> Configure >> Security >> System >> API.
- Under the Allowed IP addresses for API field, enter the authorized IP addresses. Ensure you whitelist the loopback interface and the internal IPs of both secondary nodes:
127.0.0.1(Local loopback)172.16.45.5(NS01 internal IP)172.16.45.6(NS02 internal IP)
- Click Save to apply the changes.
4. Securing and tuning BIND9 (named.conf.options / named.conf)#
To allow secure zone transfers (AXFR) from the new Master to the secondary nodes (ns01 and ns02), the configuration options file for the BIND daemon must be adjusted.
Step 0: Mandatory backup#
Before modifying the BIND9 configuration, create backup copies of your configuration files for rapid recovery:
Debian/Ubuntu:
# Backup named.conf.options with a timestamp suffix
sudo cp /etc/bind/named.conf.options /etc/bind/named.conf.options.backup.$(date +%Y%m%d-%H%M%S)
# Full backup of the BIND configuration directory
sudo tar czf /root/bind-backup-$(date +%Y%m%d).tar.gz /etc/bind/
RHEL/Rocky Linux/AlmaLinux:
# Backup named.conf with a timestamp suffix
sudo cp /etc/named.conf /etc/named.conf.backup.$(date +%Y%m%d-%H%M%S)
Step 1: Modifying the configuration#
Open the appropriate file on the Master server in your preferred text editor:
Debian/Ubuntu:
sudo nano /etc/bind/named.conf.options
RHEL/Rocky Linux/AlmaLinux:
sudo nano /etc/named.conf
Modify or configure the options block to match the following structure:
options {
directory "/var/cache/bind"; // On RHEL/Rocky use "/var/named"
dnssec-validation auto;
auth-nxdomain no;
// Limits DNS recursion exclusively to the localhost interface
allow-recursion { 127.0.0.1; };
// Restricts zone transfers (AXFR) exclusively to NS01 and NS02
allow-transfer { 172.16.45.5; 172.16.45.6; };
// Notifies secondary (NS) nodes of local zone updates
allow-notify { 172.16.45.5; 172.16.45.6; };
also-notify { 172.16.45.5; 172.16.45.6; };
hostname none;
server-id none;
version none;
};
Step 2: Syntax validation and service restart#
Save your changes, then validate the configuration files syntax using the named-checkconf tool before reloading the service:
# Validate on Debian/Ubuntu (if the command runs silently, the syntax is correct)
sudo named-checkconf /etc/bind/named.conf.options
# Validate on RHEL/Rocky Linux/AlmaLinux
sudo named-checkconf /etc/named.conf
If a syntax error occurs, the terminal will return the line number and error details. Correct the file before proceeding.
If the validation completes successfully, restart the appropriate DNS service:
# Restart on Debian/Ubuntu
sudo systemctl restart bind9
# Restart on RHEL/Rocky Linux/AlmaLinux
sudo systemctl restart named
Emergency rollback procedure#
If the DNS service fails to restart or stops responding:
Debian/Ubuntu:
# 1. Restore the configuration backup
sudo cp /etc/bind/named.conf.options.backup.* /etc/bind/named.conf.options
# 2. Validate syntax and restart
sudo named-checkconf /etc/bind/named.conf.options
sudo systemctl restart bind9
RHEL/Rocky Linux/AlmaLinux:
# 1. Restore the configuration backup
sudo cp /etc/named.conf.backup.* /etc/named.conf
# 2. Validate syntax and restart
sudo named-checkconf /etc/named.conf
sudo systemctl restart named
5. Firewall configuration for zone transfers (AXFR)#
Standard DNS resolution queries use UDP port 53. However, zone replication and zone transfers (AXFR) between primary and secondary nameservers use TCP port 53. You must whitelist incoming TCP port 53 traffic specifically between the IP addresses of your cluster nodes.
Configuration via UFW (debian/ubuntu)#
# Allow incoming TCP/UDP connections on port 53 for cluster IPs
sudo ufw allow from 172.16.45.5 to any port 53 proto tcp
sudo ufw allow from 172.16.45.6 to any port 53 proto tcp
sudo ufw reload
Configuration via raw iptables#
sudo iptables -A INPUT -p tcp -s 172.16.45.5 --dport 53 -j ACCEPT
sudo iptables -A INPUT -p tcp -s 172.16.45.6 --dport 53 -j ACCEPT
# Persist the ruleset
sudo netfilter-persistent save || sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
6. Registering and synchronizing remote hosts in HestiaCP#
With the API whitelists and BIND daemon options configured, register the secondary nodes on the HestiaCP Master server.
Safe node registration via environment variables or API keys#
I recommend generating a dedicated API Key inside the panel at Server Settings -> API -> Generate for isolated access credentials.
If using the remote admin password, register the nodes using temporary environment variables wrapped by shell directives that temporarily disable command history logging:
# 1. Temporarily disable command history logging in the terminal
set +o history
# 2. Export the password to the local shell environment
export NS_PASS="YOUR_NS01_PASSWORD"
# 3. Register NS01 under the dedicated dns-cluster system account
sudo /usr/local/hestia/bin/v-add-remote-dns-host 172.16.45.5 8083 'admin' "$NS_PASS" 'api' 'dns-cluster'
# 4. Register NS02
export NS_PASS="YOUR_NS02_PASSWORD"
sudo /usr/local/hestia/bin/v-add-remote-dns-host 172.16.45.6 8083 'admin' "$NS_PASS" 'api' 'dns-cluster'
# 5. Clean up environment variables and re-enable history logging
unset NS_PASS
set -o history
The added hosts are recorded in the cluster configuration file located at dns-cluster.conf.
Manual immediate synchronization#
HestiaCP runs background replication tasks every 1-5 minutes via cron. To trigger manual zone synchronization and test connections immediately, run:
sudo /usr/local/hestia/bin/v-sync-dns-cluster
To verify the zone replication was successful, access the shell on either secondary nameserver and list the imported zones under the dns-cluster account:
sudo /usr/local/hestia/bin/v-list-dns-domains dns-cluster
7. Diagnostics and troubleshooting#
System log auditing#
If synchronization errors occur, audit the detailed log outputs:
- BIND9 syslog entries:
sudo tail -f /var/log/syslog | grep named(on RHEL, checkmessages) - HestiaCP system logs:
sudo tail -f /usr/local/hestia/log/system.log(checksystem.log) and/usr/local/hestia/log/history.log(checkhistory.log) - Cluster replication issues:
sudo grep "dns-cluster" /var/log/syslog(ormessageson RHEL)
Query diagnostics using dig#
To verify that the DNS zones have propagated and resolve authoritatively from outside the private network, use query diagnostic tools like dig.
A healthy and synchronized cluster requires the SOA (Start of Authority) serial numbers returned on the Master and Slaves to be exactly identical:
# Querying the SOA record on NS01
dig SOA +short @ns01.domain.com dev.domain.com
# Output: ns01.domain.com. root.dev.domain.com. 2024091201 7200 3600 1209600 180
# Querying the SOA record on NS02 (must match the master/ns01 serial)
dig SOA +short @ns02.domain.com dev.domain.com
# Output: ns01.domain.com. root.dev.domain.com. 2024091201 7200 3600 1209600 180
If the serial numbers returned diverge (e.g., 2024091201 on NS01 and 2024091202 on NS02), the zones are out of sync. Force manual synchronization or check the BIND9 syslog entries for zone transfer errors.
8. DNS cluster integrity validation script#
The script below checks connection pings, validates HestiaCP API responses over HTTPS, audits the BIND9 daemon active status, and compares zone SOA serial numbers across servers to ensure cluster integrity:
#!/bin/bash
# validate_dns_cluster.sh - Validate HestiaCP DNS Cluster Integrity
# ⚠️ Run this script with appropriate privileges
set -e
# Configure infrastructure IP addresses
MASTER_IP="172.16.45.4"
SLAVES=("172.16.45.5" "172.16.45.6")
DOMAIN="dev.domain.com"
# Terminal output formatting colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[OK]${NC} $1"; }
error() { echo -e "${RED}[FAIL]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
echo "=== Testing Network Connectivity (Ping) ==="
for slave in "${SLAVES[@]}"; do
if ping -c 1 -W 2 "$slave" &>/dev/null; then
log "Slave $slave responding to ICMP packets"
else
error "Slave $slave unreachable via network"
fi
done
echo ""
echo "=== Testing HestiaCP API via HTTPS (Port 8083) ==="
for slave in "${SLAVES[@]}"; do
HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "https://$slave:8083/")
if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "401" ]]; then
log "API on $slave active (HTTP $HTTP_CODE)"
else
error "API on $slave failed (HTTP $HTTP_CODE)"
fi
done
echo ""
echo "=== Checking SOA Serial Synchronization ==="
MASTER_SERIAL=$(dig SOA +short @${MASTER_IP} "$DOMAIN" | awk '{print $3}')
if [[ -z "$MASTER_SERIAL" ]]; then
error "Could not retrieve SOA serial from Master (${MASTER_IP})"
else
log "Serial on Master: $MASTER_SERIAL"
for slave in "${SLAVES[@]}"; do
SLAVE_SERIAL=$(dig SOA +short @"$slave" "$DOMAIN" | awk '{print $3}')
if [[ "$MASTER_SERIAL" == "$SLAVE_SERIAL" ]]; then
log "Slave $slave successfully synchronized ($SLAVE_SERIAL)"
else
warn "Slave $slave is out of sync! Master: $MASTER_SERIAL, Slave: $SLAVE_SERIAL"
fi
done
fi
echo ""
echo "=== Audit Completed ==="
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments