BIND-02 Secondary DNS Deployment

Deploy bind-02 as secondary DNS on kvm-02 for DNS high availability with automatic zone transfers from bind-01.

Prerequisites

Complete before starting:

Architecture

BIND DNS HA Topology
kvm-01                    kvm-02
┌──────────────┐         ┌──────────────┐
│  bind-01     │  AXFR   │  bind-02     │
│  10.50.1.90   │────────►│  10.50.1.91│
│  PRIMARY     │  IXFR   │  SECONDARY   │
│  (master)    │         │  (slave)     │
└──────────────┘         └──────────────┘
        ↓                        ↓
   Authoritative          Zone Transfers
   Zone Updates           on NOTIFY

Zone Transfer Protocol:

  • NOTIFY (53/UDP): Master alerts slave of changes

  • AXFR (53/TCP): Full zone transfer (initial sync)

  • IXFR (53/TCP): Incremental zone transfer (updates only)

Node IP Role Status

bind-01

10.50.1.90

Primary (master)

Active

bind-02

10.50.1.91

Secondary (slave)

Planned


Phase 1: Configure bind-01 for Zone Transfers

1.0 Pre-flight Validation

# Load Vault SSH credentials
ds d000 dev/vault && vault-ssh-sign
# SSH into bind-01
ssh bind-01

1.0.1 PRE-CHANGE Validation (run on bind-01)

Capture current state before any changes using grep with line numbers:

echo "=== PRE-CHANGE VALIDATION $(date) ===" && \
echo -e "\n--- Forward zone (grep -n -A6) ---" && \
sudo grep -n "inside.domusdigitalis.dev" -A6 /etc/named.conf | grep -E "zone|type|file|allow|notify|\};" && \
echo -e "\n--- Reverse zone ---" && \
sudo grep -n "1.50.10.in-addr.arpa" -A6 /etc/named.conf | grep -E "zone|type|file|allow|notify|\};" && \
echo -e "\n--- Current serial ---" && \
sudo grep -oP '\d{10}' /var/named/inside.domusdigitalis.dev.zone | head -1 && \
echo -e "\n--- DNS test ---" && \
dig @localhost vault-01.inside.domusdigitalis.dev +short && \
echo -e "\n--- named status ---" && \
systemctl is-active named
Expected output (2026-03-10) - NO allow-transfer or also-notify yet
=== PRE-CHANGE VALIDATION Tue Mar 10 12:42:32 AM UTC 2026 ===

--- Forward zone (grep -n -A6) ---
56:  zone "inside.domusdigitalis.dev" IN {
57-      type master;
58-      file "inside.domusdigitalis.dev.zone";
59-      allow-update { none; };
62-  };

--- Reverse zone ---
64:  zone "1.50.10.in-addr.arpa" IN {
65-      type master;
66-      file "10.50.1.rev";
67-      allow-update { none; };
70-  };

--- Current serial ---
2026030104

--- DNS test ---
10.50.1.60

--- named status ---
active
Zone blocks should have allow-update { none; }; but NO allow-transfer or also-notify yet.

1.1 Backup Current Configuration

ssh bind-01 "sudo cp /etc/named.conf /etc/named.conf.before-ha && echo 'Backup created: /etc/named.conf.before-ha'"
# Verify backup exists
ssh bind-01 "ls -la /etc/named.conf.before-ha"

1.2 Check Current Zone Blocks

# View current zone configuration with line numbers
ssh bind-01 "sudo awk '/zone \"inside.domusdigitalis.dev\"/,/};/' /etc/named.conf"
# View reverse zone configuration
ssh bind-01 "sudo awk '/zone \"1.50.10.in-addr.arpa\"/,/};/' /etc/named.conf"

1.3 Add Zone Transfer Directives

We add allow-transfer and also-notify directly to each zone block rather than using a global ACL for clarity.
# Add allow-transfer and also-notify to forward zone
ssh bind-01 "sudo sed -i '/zone \"inside.domusdigitalis.dev\"/,/};/{
    s/allow-update { none; };/allow-update { none; };\n    allow-transfer { 10.50.1.91; };\n    also-notify { 10.50.1.91; };/
}' /etc/named.conf"
# Add allow-transfer and also-notify to reverse zone
ssh bind-01 "sudo sed -i '/zone \"1.50.10.in-addr.arpa\"/,/};/{
    s/allow-update { none; };/allow-update { none; };\n    allow-transfer { 10.50.1.91; };\n    also-notify { 10.50.1.91; };/
}' /etc/named.conf"

1.4 Validate Configuration Changes

# Verify forward zone now has transfer directives
result=$(ssh bind-01 "sudo awk '/zone \"inside.domusdigitalis.dev\"/,/};/' /etc/named.conf") && echo "$result"
Expected output
zone "inside.domusdigitalis.dev" IN {
    type master;
    file "inside.domusdigitalis.dev.zone";
    allow-update { none; };
    allow-transfer { 10.50.1.91; };
    also-notify { 10.50.1.91; };
};
# Verify reverse zone
result=$(ssh bind-01 "sudo awk '/zone \"1.50.10.in-addr.arpa\"/,/};/' /etc/named.conf") && echo "$result"

1.5 View Current Zone File and Serial

# View forward zone header with line numbers (serial is typically line 3-5)
ssh bind-01 "sudo awk 'NR>=1 && NR<=15 {print NR\": \"$0}' /var/named/inside.domusdigitalis.dev.zone"
# Extract current serial number
current_serial=$(ssh bind-01 "sudo grep -oP '\d{10}' /var/named/inside.domusdigitalis.dev.zone | head -1") && echo "Current serial: $current_serial" || echo "ERROR: Could not extract serial"

1.6 Increment Serial Number

BIND requires serial increment for slaves to pull updates. Use YYYYMMDDNN format.
# Calculate new serial (date-based: YYYYMMDDNN)
new_serial=$(date +%Y%m%d01)
echo "New serial will be: $new_serial"
# Update serial in zone file (replace 10-digit serial with new one)
ssh bind-01 "sudo sed -i 's/[0-9]\{10\}/${new_serial}/' /var/named/inside.domusdigitalis.dev.zone"
# Verify serial was updated
result=$(ssh bind-01 "sudo grep -oP '\d{10}' /var/named/inside.domusdigitalis.dev.zone | head -1") && echo "Updated serial: $result" || echo "ERROR"

1.7 Add bind-02 DNS Records

# Check if bind-02 A record already exists (should return nothing)
ssh bind-01 "sudo grep 'bind-02' /var/named/inside.domusdigitalis.dev.zone" || echo "bind-02 not found (expected)"
# Add bind-02 A record
ssh bind-01 "sudo tee -a /var/named/inside.domusdigitalis.dev.zone > /dev/null << 'EOF'
bind-02                 IN  A       10.50.1.91
EOF"
# Verify A record was added
result=$(ssh bind-01 "sudo grep 'bind-02' /var/named/inside.domusdigitalis.dev.zone") && echo "Added: $result" || echo "ERROR: Record not found"
# View current NS records
ssh bind-01 "sudo grep -n 'IN.*NS' /var/named/inside.domusdigitalis.dev.zone"
# Add NS record for bind-02 (after existing NS record line)
ssh bind-01 "sudo sed -i '/IN.*NS.*bind-01/a\                        IN  NS      bind-02.inside.domusdigitalis.dev.' /var/named/inside.domusdigitalis.dev.zone"
# Verify NS records (should show both bind-01 and bind-02)
ssh bind-01 "sudo grep -n 'IN.*NS' /var/named/inside.domusdigitalis.dev.zone"

1.8 Add Reverse PTR for bind-02

# Check current reverse zone for bind-02 (should not exist)
ssh bind-01 "sudo grep '91' /var/named/1.50.10.in-addr.arpa" || echo "PTR for .91 not found (expected)"
# Add PTR record for bind-02
ssh bind-01 "sudo tee -a /var/named/1.50.10.in-addr.arpa > /dev/null << 'EOF'
91                      IN  PTR     bind-02.inside.domusdigitalis.dev.
EOF"
# Verify PTR was added
result=$(ssh bind-01 "sudo grep '91.*PTR' /var/named/1.50.10.in-addr.arpa") && echo "Added: $result" || echo "ERROR"
# Increment reverse zone serial too
ssh bind-01 "sudo sed -i 's/[0-9]\{10\}/${new_serial}/' /var/named/1.50.10.in-addr.arpa"

1.9 Validate and Reload bind-01

# Check named.conf syntax
result=$(ssh bind-01 "sudo named-checkconf 2>&1") && echo "named.conf: OK" || echo "ERROR: $result"
# Check forward zone syntax
ssh bind-01 "sudo named-checkzone inside.domusdigitalis.dev /var/named/inside.domusdigitalis.dev.zone"
# Check reverse zone syntax
ssh bind-01 "sudo named-checkzone 1.50.10.in-addr.arpa /var/named/1.50.10.in-addr.arpa"
# Reload BIND (not restart - preserves cache)
sudo systemctl reload named && echo 'named reloaded successfully'

1.9.1 POST-CHANGE Validation (run on bind-01)

Verify changes applied correctly using grep with line numbers and context:

echo "=== POST-CHANGE VALIDATION $(date) ===" && \
echo -e "\n--- Forward zone (grep -A6 shows 6 lines after match) ---" && \
sudo grep -n "inside.domusdigitalis.dev" -A6 /etc/named.conf | grep -E "zone|type|file|allow|notify" && \
echo -e "\n--- Reverse zone ---" && \
sudo grep -n "1.50.10.in-addr.arpa" -A6 /etc/named.conf | grep -E "zone|type|file|allow|notify" && \
echo -e "\n--- Config syntax ---" && \
sudo named-checkconf && echo 'SYNTAX OK' && \
echo -e "\n--- named status ---" && \
systemctl is-active named && \
echo -e "\n--- DNS resolves ---" && \
dig @localhost vault-01.inside.domusdigitalis.dev +short
Expected output (2026-03-10)
=== POST-CHANGE VALIDATION Tue Mar 10 12:46:10 AM UTC 2026 ===

--- Forward zone (grep -A6 shows 6 lines after match) ---
56:  zone "inside.domusdigitalis.dev" IN {
57-      type master;
58-      file "inside.domusdigitalis.dev.zone";
59-      allow-update { none; };
60-      allow-transfer { 10.50.1.91; };
61-      also-notify { 10.50.1.91; };

--- Reverse zone ---
64:  zone "1.50.10.in-addr.arpa" IN {
65-      type master;
66-      file "10.50.1.rev";
67-      allow-update { none; };
68-      allow-transfer { 10.50.1.91; };
69-      also-notify { 10.50.1.91; };

--- Config syntax ---
SYNTAX OK

--- named status ---
active

--- DNS resolves ---
10.50.1.60
grep -n shows line numbers, -A6 shows 6 lines after match. Pipe to second grep to filter relevant lines.

Alternative: awk with line numbers (for debugging)

# Show lines 56-70 with line numbers (forward + reverse zones)
sudo awk 'NR>=56 && NR<=75 {print NR": "$0}' /etc/named.conf
# Find all zone blocks and their line numbers
sudo grep -n "^[[:space:]]*zone" /etc/named.conf

Phase 2: Deploy bind-02 VM on kvm-02

SSH into kvm-02 first, then run all commands locally. Do NOT use SSH wrappers for each command.
ssh kvm-02

2.1 Create VM Disk (Copy and Resize)

Copy the base image, then resize. This matches the working bind-01 pattern.
# Copy Rocky cloud image (creates standalone disk, not overlay)
sudo cp /var/lib/libvirt/images/Rocky-9-GenericCloud.qcow2 /var/lib/libvirt/images/bind-02.qcow2
# Resize to 20GB
sudo qemu-img resize /var/lib/libvirt/images/bind-02.qcow2 20G
# Verify disk size
qemu-img info /var/lib/libvirt/images/bind-02.qcow2
Expected output
virtual size: 20 GiB (21474836480 bytes)

2.2 Create Cloud-Init Configuration

Static IP MUST be configured in runcmd. Rocky cloud images have no DHCP fallback on infrastructure VLANs.
cat > /tmp/bind-02-cloud-init.yml << 'EOF'
#cloud-config
hostname: bind-02
fqdn: bind-02.inside.domusdigitalis.dev
manage_etc_hosts: true

users:
  - name: evanusmodestus
    groups: wheel
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    lock_passwd: false
    plain_text_passwd: changeme123
    ssh_authorized_keys:
      # Vault SSH CA signed key (8h TTL)
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIrgE9z8gkQVRVkkdbc1ejdth7vJkqpY35FrIUv8L6JB vault-signed
      # YubiKey nano
      - sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIG/EGu00HuV3jnisul7DUBuk9jLtrE3yR4BZCwGb2YpCAAAABHNzaDo= d000-nano-35641207
      # YubiKey primary
      - sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFHfsGSAFAkqwYj6EGS9sA2MROjs28zM6LJds3gagsCkAAAACHNzaDpkMDAw evanusmodestus@d000-yubikey
      # YubiKey secondary
      - sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIEBZ+kus4aTHzQt1zNnEnGxJs+Lf56vrCdcyvqLhpp9hAAAACHNzaDpkMDAw ssh:d000
      # Fallback ed25519
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL3vaIABqHOwy88p/5GcX3ZNU044GAz/3T5dH8GIU7DS evanusmodestus@d000

runcmd:
  # Configure static IP (interface is eth0 on Rocky cloud images)
  - nmcli connection delete 'Wired connection 1' 2>/dev/null || true
  - nmcli con add type ethernet ifname eth0 con-name mgmt ipv4.addresses 10.50.1.91/24 ipv4.gateway 10.50.1.1 ipv4.dns 10.50.1.90 ipv4.method manual
  - nmcli con up mgmt
  # Install BIND
  - dnf install -y bind bind-utils
  - mkdir -p /var/named/slaves
  - chown named:named /var/named/slaves
  - systemctl enable named

final_message: "Cloud-init completed. bind-02 ready for secondary DNS configuration."
EOF

Password: Replace changeme123 with gopass-generated password: gopass generate -s v3/domains/d000/servers/bind-02/evanusmodestus 24

Interface Name: Rocky 9 cloud images use eth0, not enp1s0.

2.3 Create meta-data

cat > /tmp/bind-02-meta-data << 'EOF'
instance-id: bind-02
local-hostname: bind-02
EOF

2.4 Create Cloud-Init ISO

Files MUST be named user-data and meta-data inside the ISO. Cloud-init ignores other filenames.
# Rename to required cloud-init filenames
cp /tmp/bind-02-cloud-init.yml /tmp/user-data
cp /tmp/bind-02-meta-data /tmp/meta-data
sudo genisoimage -output /var/lib/libvirt/images/bind-02-cidata.iso \
  -volid cidata -joliet -rock \
  /tmp/user-data /tmp/meta-data

2.5 Download Rocky Linux Cloud Image (if not cached)

ls /var/lib/libvirt/images/Rocky-9-GenericCloud*.qcow2 2>/dev/null || \
  sudo curl -Lo /var/lib/libvirt/images/Rocky-9-GenericCloud.qcow2 \
  https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2

2.6 Create VM with virt-install

Rocky 9 cloud images do NOT have serial console enabled by default. Use VNC graphics for console access.
sudo virt-install \
  --name bind-02 \
  --memory 2048 \
  --vcpus 2 \
  --disk path=/var/lib/libvirt/images/bind-02.qcow2 \
  --disk path=/var/lib/libvirt/images/bind-02-cidata.iso,device=cdrom \
  --os-variant rocky9 \
  --network bridge=br-mgmt \
  --graphics vnc,listen=0.0.0.0 \
  --import \
  --noautoconsole
# Verify VM is running
sudo virsh domstate bind-02
# Get VNC port for console access
sudo virsh vncdisplay bind-02
Access console via Cockpit: kvm-02.inside.domusdigitalis.dev:9090 → Virtual Machines → bind-02 → Console.

2.6.1 Fix Bridge VLAN Tagging

kvm-02 uses bridge VLAN filtering. New vnets default to VLAN 1 only - must add management VLANs manually.
# Identify bind-02's vnet interface
VNET=$(sudo virsh domiflist bind-02 | awk '/br-mgmt/{print $1}')
echo "bind-02 vnet: $VNET"
# Check current VLAN state (will show only VLAN 1)
bridge vlan show dev $VNET
# Add all management VLANs (match other working VMs)
VNET=$(sudo virsh domiflist bind-02 | awk '/br-mgmt/{print $1}') && \
for vid in 10 20 30 40 110 120; do sudo bridge vlan add vid $vid dev $VNET; done && \
sudo bridge vlan add vid 100 dev $VNET pvid untagged && \
sudo bridge vlan del vid 1 dev $VNET
# Verify VLANs applied
bridge vlan show dev $VNET
Expected output
port              vlan-id
vnet76            10
                  20
                  30
                  40
                  100 PVID Egress Untagged
                  110
                  120
This is NOT persistent across VM restart. Update /etc/libvirt/hooks/qemu on kvm-02 to auto-tag bind-02’s vnet.

2.7 Verify Static IP (Cloud-Init)

Cloud-init configures static IP automatically via runcmd. Verify via VNC console or wait for SSH:

# Test ping from kvm-02 (after cloud-init completes)
ping -c3 10.50.1.91

If cloud-init failed, access VNC console via Cockpit and configure manually:

# Inside VM (via VNC console) - manual fallback only
sudo nmcli connection delete 'Wired connection 1' 2>/dev/null || true
sudo nmcli con add type ethernet ifname eth0 con-name mgmt \
  ipv4.addresses 10.50.1.91/24 \
  ipv4.gateway 10.50.1.1 \
  ipv4.dns 10.50.1.90 \
  ipv4.method manual
sudo nmcli con up mgmt

2.8 Verify SSH Access

ssh 10.50.1.91 "hostname; ip -br addr show"

Phase 3: Configure bind-02 as Secondary

3.1 Configure named.conf

Rocky’s default named.conf only listens on localhost. Must fix listen-on AND append slave zones.

3.1.1 PRE-CHANGE Validation

ssh bind-02 "echo '=== PRE-CHANGE ===' && \
  echo '--- listen-on (default: localhost only) ---' && \
  sudo grep 'listen-on' /etc/named.conf && \
  echo '--- allow-query (default: localhost only) ---' && \
  sudo grep 'allow-query' /etc/named.conf && \
  echo '--- zones (should be empty) ---' && \
  sudo grep -c 'inside.domusdigitalis.dev' /etc/named.conf || echo '0 (expected)'"

3.1.2 Fix listen-on to Accept External Queries

Default listen-on { 127.0.0.1; } means bind-02 won’t answer queries from other hosts!
# Change listen-on from localhost to any
ssh bind-02 "sudo sed -i 's/listen-on port 53 { 127.0.0.1; };/listen-on port 53 { any; };/' /etc/named.conf"
# Change allow-query from localhost to local network
ssh bind-02 "sudo sed -i 's/allow-query.*{ localhost; };/allow-query { localhost; 10.50.0.0\/16; };/' /etc/named.conf"

3.1.3 Append Slave Zone Definitions

ssh bind-02 "sudo tee -a /etc/named.conf << 'EOF'

// inside.domusdigitalis.dev zone (SECONDARY)
zone \"inside.domusdigitalis.dev\" IN {
    type slave;
    masters { 10.50.1.90; };
    file \"slaves/inside.domusdigitalis.dev.zone\";
};

// Reverse zone for 10.50.1.x (SECONDARY)
zone \"1.50.10.in-addr.arpa\" IN {
    type slave;
    masters { 10.50.1.90; };
    file \"slaves/10.50.1.rev\";
};
EOF"

3.1.4 POST-CHANGE Validation

ssh bind-02 "echo '=== POST-CHANGE ===' && \
  echo '--- listen-on (should be: any) ---' && \
  sudo grep 'listen-on port 53' /etc/named.conf && \
  echo '--- allow-query (should include 10.50.0.0/16) ---' && \
  sudo grep 'allow-query' /etc/named.conf && \
  echo '--- zones (should show 2) ---' && \
  sudo grep -c 'type slave' /etc/named.conf"
Expected POST-CHANGE output
=== POST-CHANGE ===
--- listen-on (should be: any) ---
    listen-on port 53 { any; };
--- allow-query (should include 10.50.0.0/16) ---
    allow-query { localhost; 10.50.0.0/16; };
--- zones (should show 2) ---
2

3.2 Create Slaves Directory

ssh bind-02 "sudo mkdir -p /var/named/slaves && sudo chown named:named /var/named/slaves"

3.3 Verify Configuration

ssh bind-02 "sudo named-checkconf && echo 'Config OK'"

3.4 Start BIND Service

Cloud-init already enabled named. Use restart to ensure new config is loaded (works whether named is running or not).
ssh bind-02 "sudo systemctl restart named && sudo systemctl status named --no-pager"

3.5 Open Firewall

Rocky cloud images don’t include firewalld. Install it first.
ssh bind-02 "sudo dnf install -y firewalld && sudo systemctl enable --now firewalld"
ssh bind-02 "sudo firewall-cmd --permanent --add-service=dns && sudo firewall-cmd --reload"
# Verify DNS port is open
ssh bind-02 "sudo firewall-cmd --list-services"

Phase 4: Verify Zone Transfers

4.1 Check Zone Files on bind-02

Zone transfer happens automatically on service start:

ssh bind-02 "sudo ls -la /var/named/slaves/"

Expected output:

-rw-r--r-- 1 named named 2048 Mar  1 12:00 inside.domusdigitalis.dev.zone
-rw-r--r-- 1 named named 1024 Mar  1 12:00 10.50.1.rev

4.2 Test DNS Resolution on bind-02

ssh bind-02 "dig @localhost bind-01.inside.domusdigitalis.dev +short"

Expected: 10.50.1.90

ssh bind-02 "dig @localhost inside.domusdigitalis.dev SOA +short"

4.3 Test Reverse Lookup

ssh bind-02 "dig @localhost -x 10.50.1.90 +short"

Expected: bind-01.inside.domusdigitalis.dev.

4.4 Compare Zone Serials

echo "=== bind-01 Serial ===" && \
dig @10.50.1.90 inside.domusdigitalis.dev SOA +short | awk '{print $3}' && \
echo "=== bind-02 Serial ===" && \
dig @10.50.1.91 inside.domusdigitalis.dev SOA +short | awk '{print $3}'

Serials should match after transfer.


Phase 5: Test Failover

5.1 Test with bind-01 Stopped

# Stop bind-01
ssh bind-01 "sudo systemctl stop named"
# Query bind-02 directly
dig @10.50.1.91 vault-01.inside.domusdigitalis.dev +short
# Restart bind-01
ssh bind-01 "sudo systemctl start named"

5.2 Verify Zone Update Propagation

# Add test record on bind-01
ssh bind-01 "sudo bash -c 'echo \"test-ha    IN  A    10.50.1.250\" >> /var/named/inside.domusdigitalis.dev.zone'"

# Increment serial (required for transfer)
ssh bind-01 "sudo sed -i 's/\([0-9]{10}\)/echo \$((\1+1))/e' /var/named/inside.domusdigitalis.dev.zone"

# Reload bind-01
ssh bind-01 "sudo systemctl reload named"

# Wait for NOTIFY (usually instant)
sleep 5

# Verify on bind-02
dig @10.50.1.91 test-ha.inside.domusdigitalis.dev +short

Expected: 10.50.1.250


Phase 6: Update VyOS DNS Forwarding

6.1 PRE-CHANGE Validation

echo "=== PRE-CHANGE VyOS DNS ===" && \
echo "--- vyos-01 ---" && \
ssh vyos-01 "show configuration commands | grep 'dns forwarding name-server'" && \
echo "--- vyos-02 ---" && \
ssh vyos-02 "show configuration commands | grep 'dns forwarding name-server'"
Expected: Only bind-01
set service dns forwarding name-server 10.50.1.90

6.2 Add bind-02 as Secondary DNS Server

ssh vyos-01 "configure; set service dns forwarding name-server 10.50.1.91; commit; save; exit"
ssh vyos-02 "configure; set service dns forwarding name-server 10.50.1.91; commit; save; exit"

6.3 POST-CHANGE Validation

echo "=== POST-CHANGE VyOS DNS ===" && \
echo "--- vyos-01 ---" && \
ssh vyos-01 "show configuration commands | grep 'dns forwarding name-server'" && \
echo "--- vyos-02 ---" && \
ssh vyos-02 "show configuration commands | grep 'dns forwarding name-server'"
Expected: Both bind-01 and bind-02
set service dns forwarding name-server 10.50.1.90
set service dns forwarding name-server 10.50.1.91

6.2 Verify DNS Configuration

# Check VyOS DNS forwarding config
ssh vyos-01 "show configuration commands | grep dns"
Expected output
set service dns forwarding name-server 10.50.1.90
set service dns forwarding name-server 10.50.1.91
set service dns forwarding listen-address 10.50.1.1

6.3 Verify DNS Resolution Order

# From any client, verify both DNS servers respond
dig @10.50.1.90 vault-01.inside.domusdigitalis.dev +short
dig @10.50.1.91 vault-01.inside.domusdigitalis.dev +short

Validation Checklist

echo "=== BIND HA Validation ===" && \
echo -e "\n--- bind-01 Status ---" && \
ssh bind-01 "systemctl is-active named" && \
echo -e "\n--- bind-02 Status ---" && \
ssh bind-02 "systemctl is-active named" && \
echo -e "\n--- Zone Files on bind-02 ---" && \
ssh bind-02 "ls -la /var/named/slaves/" && \
echo -e "\n--- Serial Comparison ---" && \
echo "bind-01: $(dig @10.50.1.90 inside.domusdigitalis.dev SOA +short | awk '{print $3}')" && \
echo "bind-02: $(dig @10.50.1.91 inside.domusdigitalis.dev SOA +short | awk '{print $3}')" && \
echo -e "\n--- Cross-Query Test ---" && \
echo "bind-01 → vault-01: $(dig @10.50.1.90 vault-01.inside.domusdigitalis.dev +short)" && \
echo "bind-02 → vault-01: $(dig @10.50.1.91 vault-01.inside.domusdigitalis.dev +short)"

Rollback Plan

If Zone Transfer Fails

# Check bind-01 logs
ssh bind-01 "sudo journalctl -u named -n 50 | grep -i transfer"

# Check bind-02 logs
ssh bind-02 "sudo journalctl -u named -n 50 | grep -i transfer"

# Common issues:
# - Firewall blocking 53/tcp (AXFR uses TCP)
# - ACL misconfiguration on bind-01
# - File permissions on /var/named/slaves

Remove bind-02 Completely

# Delete VM
ssh kvm-02 "sudo virsh destroy bind-02; sudo virsh undefine bind-02 --remove-all-storage"

# Revert bind-01 config
ssh bind-01 "sudo cp /etc/named.conf.before-ha /etc/named.conf && sudo systemctl reload named"