PowerDNS Master-Slave Configuratie Opzetten met Automatisch Zonebeheer en Import

Introductie

Deze handleiding biedt een gedetailleerde uitleg voor het opzetten van een PowerDNS master-slave configuratie op Debian 12, inclusief installatie, configuratie en implementatie van scripts voor automatisch zonebeheer en import. Deze setup is ideaal voor het beheren van meerdere DNS-zones met automatische replicatie tussen servers en efficiënte zone-imports.


Voorwaarden

  • SSH-toegang: Zorg voor wachtwoordloze SSH-toegang van uw masterserver (ns1) naar uw slaveserver (ns2). Dit is cruciaal voor geautomatiseerde zone-overdrachten en beheer. U kunt dit bereiken door SSH-sleutels op te zetten.
  • Netwerkconnectiviteit: Controleer of beide servers (ns1 en ns2) met elkaar kunnen communiceren op de vereiste poorten (voornamelijk poort 53 voor DNS en poort 8081 voor de API, indien ingeschakeld). Gebruik tools zoals ping en telnet of nc om de connectiviteit te testen.
  • Nauwkeurige server-IP-adressen: Zorg dat u de juiste IP-adressen voor zowel ns1 als ns2 bij de hand heeft, aangezien u deze voor de configuratie nodig zult hebben.

Deel 1: PowerDNS Installeren

Op zowel de Master (ns1) als de Slave (ns2) server:

  1. Update uw systeem:

    sudo apt update && sudo apt upgrade -y
  2. Installeer PowerDNS en de MySQL-backend:

    sudo apt install pdns-server pdns-backend-mysql mariadb-server -y
  3. Beveilig uw MySQL-installatie:

    sudo mysql_secure_installation
  4. Maak een database en gebruiker aan voor PowerDNS:

    sudo mysql -u root -p

    In de MySQL-prompt:

    CREATE DATABASE pdns;
    CREATE USER 'pdns'@'localhost' IDENTIFIED BY 'uw_veilige_wachtwoord';
    GRANT ALL PRIVILEGES ON pdns.* TO 'pdns'@'localhost';
    FLUSH PRIVILEGES;
    EXIT;
  5. Importeer het PowerDNS-schema:

    sudo mysql -u root -p pdns < /usr/share/doc/pdns-backend-mysql/schema.mysql.sql

Deel 2: PowerDNS Configureren

Op de Masterserver (ns1):

  1. Bewerk het PowerDNS-configuratiebestand:

    sudo nano /etc/powerdns/pdns.conf
  2. Voeg deze regels toe of wijzig ze:

    launch=gmysql
    gmysql-host=localhost
    gmysql-user=pdns
    gmysql-dbname=pdns
    gmysql-password=uw_veilige_wachtwoord
    api=yes
    api-key=genereer_een_veilige_api_sleutel
    webserver=yes
    webserver-port=8081
    webserver-address=0.0.0.0
    webserver-allow-from=127.0.0.1,ANDER_IP,ANDER_IP
    allow-axfr-ips=IP_VAN_SLAVE_SERVER
    master=yes
    slave=no
    also-notify=IP_VAN_SLAVE_SERVER
  3. Zo kunt u een veilige API-sleutel genereren:

    openssl rand -base64 30

Op de Slaveserver (ns2):

  1. Bewerk het PowerDNS-configuratiebestand:

    sudo nano /etc/powerdns/pdns.conf
  2. Voeg deze regels toe of wijzig ze:

    launch=gmysql
    gmysql-host=localhost
    gmysql-user=pdns
    gmysql-dbname=pdns
    gmysql-password=uw_veilige_wachtwoord
    slave=yes
    superslave=yes
  3. Voeg de masterserver toe als een supermaster:

    sudo mysql -u pdns -p pdns

    In de MySQL-prompt:

    INSERT INTO supermasters (ip, nameserver, account) VALUES ('IP_VAN_MASTER_SERVER', 'ns1.uwdomein.com', 'admin');
    EXIT;

Deel 3: Firewall Configuratie

Configureer op beide servers de firewall om alleen noodzakelijk verkeer toe te staan van vertrouwde bronnen. We staan poort 53 toe van overal, aangezien de setup bedoeld is voor openbare Autoritatieve Nameservers:

# DNS-verkeer toestaan
sudo ufw allow 53/tcp
sudo ufw allow 53/udp

# API-verkeer op poort 8081 toestaan (beperkt tot specifieke IP's)
# Vervang 192.0.2.1 door het eigenlijke IP-adres dat toegang nodig heeft
sudo ufw allow from 192.0.2.1 to any port 8081 proto tcp

# Als meerdere IP's toegang nodig hebben, voeg elk afzonderlijk toe
# sudo ufw allow from 10.0.0.1 to any port 8081 proto tcp

# SSH-toegang alleen toestaan vanuit specifieke jumphost/vertrouwde IP's
# Vervang 198.51.100.5 door uw jumphost/management server IP
sudo ufw allow from 198.51.100.5 to any port 22 proto tcp

# Alle andere SSH-verbindingen weigeren
sudo ufw deny 22/tcp

# De firewall inschakelen
sudo ufw enable

# De regels controleren
sudo ufw status verbose

Deel 4: PowerDNS Starten

Op beide servers:

sudo systemctl start pdns
sudo systemctl enable pdns

Deel 5: De Setup Testen

  1. Maak op de masterserver een testzone aan:

    sudo pdnsutil create-zone voorbeeld.com ns1.voorbeeld.com
    sudo pdnsutil add-record voorbeeld.com www A 192.0.2.1
  2. Informeer de slave:

    sudo pdns_control notify voorbeeld.com
  3. Controleer op de slave of de zone is overgedragen:

    sudo pdnsutil list-zone voorbeeld.com

Deel 6: Het Zonebeheersscript Implementeren

  1. Installeer jq op de masterserver:

    sudo apt install jq -y
  2. Maak het scriptbestand aan:

    sudo nano /usr/local/bin/pdns_zone_monitor.sh
  3. Plak de volgende inhoud:

    #!/bin/bash
    
    # Configuratie
    API_KEY="API_SLEUTEL"
    API_URL="http://IP_VAN_MASTER_SERVER:8081"  # Vervang door het IP van uw masterserver
    NS1="ns1.voorbeeld.com"
    NS2="ns2.voorbeeld.com"
    LOG_FILE="/var/log/pdns_zone_monitor.log"
    STATE_FILE="/var/log/pdns_processed_zones.txt"
    
    # Functie om berichten te loggen
    log_message() {
        echo "$(date): $1" >> $LOG_FILE
    }
    
    # Functie om SOA- en NS-records voor een zone te repareren
    fix_zone_records() {
        local zone=$1
        local serial=$(date +%Y%m%d%H)
    
        # SOA- en NS-records bijwerken
        pdnsutil set-meta "$zone" SOA-EDIT-API INCREMENT-WEEKS
        pdnsutil set-kind "$zone" MASTER
        pdnsutil add-record "$zone" @ SOA "$NS1 hostmaster.$zone $serial 10800 3600 604800 3600"
        pdnsutil add-record "$zone" @ NS "$NS1"
        pdnsutil add-record "$zone" @ NS "$NS2"
    
        # Serienummer verhogen en slaves informeren
        pdnsutil increase-serial "$zone"
        pdns_control notify "$zone"
    
        log_message "Records gerepareerd voor zone $zone"
    }
    
    # Functie om zone van ns2 te verwijderen
    remove_zone_from_ns2() {
        local zone=$1
        ssh root@ns2 "pdnsutil delete-zone $zone"
        log_message "Zone $zone verwijderd van ns2"
    }
    
    # Functie om ns2 op te schonen door zones te verwijderen die niet op ns1 staan
    clean_ns2() {
        log_message "Opschoonproces op ns2 gestart"
        local primary_zones=$(pdnsutil list-all-zones | awk '{print $1}')
        local secondary_zones=$(ssh root@ns2 "pdnsutil list-all-zones" | awk '{print $1}')
    
        log_message "Primaire zones op ns1: $primary_zones"
        log_message "Secundaire zones op ns2: $secondary_zones"
    
        # Zorg ervoor dat de zones correct worden vergeleken
        primary_zones_array=($primary_zones)
        secondary_zones_array=($secondary_zones)
    
        for zone in "${secondary_zones_array[@]}"; do
            if [[ ! " ${primary_zones_array[*]} " =~ " $zone " ]]; then
                log_message "Zone $zone wordt opgeschoond van ns2"
                remove_zone_from_ns2 "$zone"
            fi
        done
    }
    
    # Verwerkte zones laden
    if [ -f "$STATE_FILE" ]; then
        mapfile -t processed_zones < "$STATE_FILE"
    else
        touch "$STATE_FILE"
        processed_zones=()
    fi
    
    # Lijst van huidige zones ophalen
    current_zones=$(curl -s -H "X-API-Key: $API_KEY" "$API_URL/api/v1/servers/localhost/zones" | jq -r '.[].name')
    
    # current_zones omzetten naar een array
    current_zones_array=($current_zones)
    
    # Hoofdlus
    while true; do
        # Nieuwe lijst van zones ophalen
        new_zones=$(curl -s -H "X-API-Key: $API_KEY" "$API_URL/api/v1/servers/localhost/zones" | jq -r '.[].name')
        
        # new_zones omzetten naar een array
        new_zones_array=($new_zones)
        
        # Controleren op nieuwe zones en deze verwerken
        for zone in "${new_zones_array[@]}"; do
            if [[ ! " ${current_zones_array[*]} " =~ " $zone " ]] && [[ ! " ${processed_zones[*]} " =~ " $zone " ]]; then
                log_message "Nieuwe zone gedetecteerd: $zone"
                fix_zone_records "$zone"
                echo "$zone" >> "$STATE_FILE"
                processed_zones+=("$zone")
            fi
        done
        
        # Controleren op verwijderde zones en deze verwerken
        for zone in "${current_zones_array[@]}"; do
            if [[ ! " ${new_zones_array[*]} " =~ " $zone " ]]; then
                log_message "Zone verwijderd: $zone"
                remove_zone_from_ns2 "$zone"
                processed_zones=("${processed_zones[@]/$zone}")
                sed -i "\|$zone|d" "$STATE_FILE"
            fi
        done
        
        # ns2 opschonen om zones te verwijderen die niet op ns1 staan
        clean_ns2
        
        # Lijst met huidige zones bijwerken
        current_zones_array=("${new_zones_array[@]}")
        
        # 1 minuut wachten voordat opnieuw wordt gecontroleerd
        sleep 60
    done
  4. Maak het script uitvoerbaar:

    sudo chmod +x /usr/local/bin/pdns_zone_monitor.sh
  5. Maak een systemd-servicebestand aan:

    sudo nano /etc/systemd/system/pdns-zone-monitor.service
  6. Voeg de volgende inhoud toe:

    [Unit]
    Description=PowerDNS Zone Monitor
    After=pdns.service
    
    [Service]
    ExecStart=/usr/local/bin/pdns_zone_monitor.sh
    Restart=always
    User=root
    
    [Install]
    WantedBy=multi-user.target
  7. Schakel de service in en start deze:

    sudo systemctl daemon-reload
    sudo systemctl enable pdns-zone-monitor
    sudo systemctl start pdns-zone-monitor

Deel 7: Het Zone Import Script Implementeren

  1. Maak het PowerDNS-importscript aan:

    sudo nano /usr/local/bin/powerdns_importer.py
  2. Plak de volgende inhoud:

    #!/usr/bin/env python3
    import os
    import sys
    import subprocess
    import logging
    
    # Logging instellen
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    
    def run_command(command):
        try:
            result = subprocess.run(command, capture_output=True, text=True, check=True)
            return result.stdout.strip()
        except subprocess.CalledProcessError as e:
            logging.error(f"Fout bij uitvoeren commando {' '.join(command)}. Fout: {e.stderr.strip()}")
            return None
    
    def import_zone(zone_file):
        with open(zone_file, 'r') as f:
            lines = f.readlines()
    
        zone_name = None
        ttl = "3600"
    
        for line in lines:
            line = line.strip()
            if line.startswith('$ORIGIN'):
                zone_name = line.split()[1].rstrip('.')
                break
    
        if not zone_name:
            logging.error(f"Kon de zonenaam niet bepalen uit {zone_file}")
            return False
    
        try:
            # Verwijder de zone als deze bestaat
            run_command(['pdnsutil', 'delete-zone', zone_name])
            logging.info(f"Bestaande zone verwijderd: {zone_name}")
        except:
            logging.warning(f"Zone {zone_name} bestond niet, doorgaan met aanmaken")
    
        # Zone aanmaken
        run_command(['pdnsutil', 'create-zone', zone_name])
        logging.info(f"Nieuwe zone aangemaakt: {zone_name}")
    
        for line in lines:
            line = line.strip()
            if not line or line.startswith('$ORIGIN'):
                continue
            if line.startswith('$TTL'):
                ttl = line.split()[1]
                continue
    
            parts = line.split(maxsplit=5)
            if len(parts) < 5:
                continue
    
            name, record_ttl, _, record_type, content = parts[:5]
            
            # Domeinnaam correct afhandelen
            if name == zone_name or name == "@":
                name = "@"
            else:
                name = name.rstrip('.')
                if name.endswith(zone_name):
                    name = name[:-len(zone_name)-1]  # Zonenaam aan het einde verwijderen
                if not name:
                    name = "@"
    
            if record_type == 'MX':
                if len(parts) == 6:
                    priority, mx = parts[4], parts[5]
                else:
                    priority, mx = "10", parts[4]
                content = f"{priority} {mx}"
            elif record_type == 'TXT':
                content = f'"{" ".join(parts[4:])}"'
            elif record_type in ['CNAME', 'NS', 'MX'] and not content.endswith('.'):
                content = f"{content}."
    
            result = run_command(['pdnsutil', 'add-record', zone_name, name, record_type, record_ttl, content])
            if result is not None:
                logging.info(f"{record_type}-record toegevoegd: {name} {record_ttl} IN {record_type} {content}")
            else:
                logging.error(f"Kon record niet toevoegen: {line}")
    
        # Zone rectificeren
        run_command(['pdnsutil', 'rectify-zone', zone_name])
        logging.info(f"Zone gerectificeerd: {zone_name}")
    
        return True
    
    def sync_to_ns2(zone_name):
        try:
            run_command(['pdns_control', 'notify', zone_name])
            logging.info(f"Notificatie geactiveerd voor {zone_name} naar ns2")
            return True
        except Exception as e:
            logging.error(f"Fout bij synchroniseren van {zone_name} naar ns2: {str(e)}")
            return False
    
    def main():
        if len(sys.argv) < 2:
            print("Gebruik: python powerdns_importer.py ")
            sys.exit(1)
    
        input_directory = sys.argv[1]
        if not os.path.isdir(input_directory):
            logging.error(f"Fout: {input_directory} is geen geldige directory")
            sys.exit(1)
    
        for filename in os.listdir(input_directory):
            if filename.endswith('.txt'):
                zone_file = os.path.join(input_directory, filename)
                logging.info(f"Verwerken van {zone_file}")
                if import_zone(zone_file):
                    sync_to_ns2(os.path.splitext(filename)[0])
    
    if __name__ == "__main__":
        main()
  3. Maak het script uitvoerbaar:

    sudo chmod +x /usr/local/bin/powerdns_importer.py

Het Zone Import script werkt met geëxporteerde CloudFlare Zones. Hieronder staat een script om alle zones van CloudFlare te exporteren (vergeet niet uw API-sleutel in te voeren):

#!/usr/bin/env python3

import requests
import os
import sys

# Stel uw Cloudflare API-gegevens in
api_token = 'UW_CLOUDFLARE_API_SLEUTEL'
headers = {
    'Authorization': f'Bearer {api_token}',
    'Content-Type': 'application/json'
}

# Cloudflare API-eindpunten
base_url = 'https://api.cloudflare.com/client/v4'
zones_endpoint = f'{base_url}/zones'

def fetch_paginated_results(url, params=None):
    all_results = []
    while url:
        response = requests.get(url, headers=headers, params=params)
        if response.status_code == 200:
            data = response.json()
            all_results.extend(data['result'])
            info = data['result_info']
            if info['page'] < info['total_pages']:
                params = {'page': info['page'] + 1, 'per_page': info['per_page']}
            else:
                break
        else:
            print(f"Ophalen data mislukt: {response.status_code} - {response.text}")
            return None
    return all_results

def fetch_zones():
    return fetch_paginated_results(zones_endpoint)

def fetch_zone_records(zone_id):
    records_endpoint = f'{base_url}/zones/{zone_id}/dns_records'
    return fetch_paginated_results(records_endpoint)

def export_zone_to_file(zone_name, records):
    filename = f'{zone_name}.txt'
    with open(filename, 'w') as file:
        file.write(f"$ORIGIN {zone_name}.\n")
        file.write("$TTL 3600\n\n")
        for record in records:
            name = record['name'].replace(f".{zone_name}", "") or "@"
            file.write(f"{name}\t{record['ttl']}\tIN\t{record['type']}\t{record['content']}\n")
    print(f"{zone_name} geëxporteerd naar {filename}")

def main():
    if api_token == 'UW_CLOUDFLARE_API_TOKEN':
        print("Stel uw Cloudflare API-token in het script in.")
        sys.exit(1)

    zones = fetch_zones()
    if not zones:
        print("Geen zones gevonden of ophalen van zones mislukt.")
        return
    
    os.makedirs('cloudflare_zones', exist_ok=True)
    os.chdir('cloudflare_zones')
    
    for zone in zones:
        zone_id = zone['id']
        zone_name = zone['name']
        print(f"Records ophalen voor zone: {zone_name}")
        records = fetch_zone_records(zone_id)
        if records:
            export_zone_to_file(zone_name, records)

if __name__ == "__main__":
    main()

Deel 8: Het Slave Update Script Implementeren

  1. Maak het script voor het bijwerken van alle slaves aan:

    sudo nano /usr/local/bin/update_all_slaves.py
  2. Plak de volgende inhoud:

    #!/usr/bin/env python3
    import subprocess
    import logging
    
    # Logging instellen
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    
    def run_command(command):
        try:
            result = subprocess.run(command, capture_output=True, text=True, check=True)
            return result.stdout.strip()
        except subprocess.CalledProcessError as e:
            logging.error(f"Fout bij uitvoeren commando {' '.join(command)}. Fout: {e.stderr.strip()}")
            return None
    
    def update_all_slaves():
        try:
            # Lijst van alle zones ophalen
            zones_output = run_command(['pdnsutil', 'list-all-zones'])
            if zones_output is None:
                logging.error("Ophalen van zoneslijst mislukt")
                return False
    
            zones = zones_output.split('\n')
    
            # Elke zone notificeren
            for zone in zones:
                result = run_command(['pdns_control', 'notify', zone])
                if result is not None:
                    logging.info(f"Notificatie geactiveerd voor {zone} naar slaves")
                else:
                    logging.warning(f"Notificatie mislukt voor zone: {zone}")
    
            logging.info("Notificaties voor alle zones voltooid")
            return True
        except Exception as e:
            logging.error(f"Fout bij bijwerken van alle slaves: {str(e)}")
            return False
    
    if __name__ == "__main__":
        if update_all_slaves():
            logging.info("Alle slave-servers succesvol bijgewerkt")
        else:
            logging.error("Bijwerken van alle slave-servers mislukt")
  3. Maak het script uitvoerbaar:

    sudo chmod +x /usr/local/bin/update_all_slaves.py

Deel 9: De Scripts Gebruiken

  1. Voor automatisch monitoren en beheren van zones: Het pdns_zone_monitor.sh script draait als een service en monitort continu op nieuwe zones en beheert deze.
  2. Voor het importeren van zones vanuit tekstbestanden:

    python3 /usr/local/bin/powerdns_importer.py /pad/naar/zone/bestanden/directory
  3. Voor het bijwerken van alle slave-servers:

    python3 /usr/local/bin/update_all_slaves.py

Deel 10: Monitoring en Onderhoud

  1. Controleer de status van PowerDNS:

    sudo systemctl status pdns
  2. Monitor het zonebeheersscript:

    sudo journalctl -u pdns-zone-monitor -f
  3. Bekijk het logbestand van het script:

    tail -f /var/log/pdns_zone_monitor.log
  4. Maak regelmatig een backup van uw PowerDNS-database:

    sudo mysqldump pdns > pdns_backup_$(date +%Y%m%d).sql

Deel 11: Zone Backups

import subprocess
import os
from datetime import datetime


def run_pdnsutil(command):
  return subprocess.run(['pdnsutil'] + command.split(),
                        capture_output=True,
                        text=True).stdout.strip()


# Haal de huidige datum en tijd op
current_date = datetime.now().strftime("%Y-%m-%d")

# Haal alle zones op
zones = run_pdnsutil('list-all-zones').split('\n')

# Maak output-directory aan
output_dir = f'/root/backups/output_{current_date}'
os.makedirs(output_dir, exist_ok=True)

# Backup elke zone
for zone in zones:
  print(f"Zone {zone} back-uppen")
  content = run_pdnsutil(f'list-zone {zone}')
  with open(os.path.join(output_dir, f'{zone}.txt'), 'w') as f:
    f.write(content)

print(f"Backup voltooid. Bestanden opgeslagen in {output_dir}.")
crontab -e

0 4 * * * /usr/bin/python3 /root/scripts/zone_backups.py
  • 31 gebruikers vonden dit artikel nuttig
Was dit antwoord nuttig?

Gerelateerde artikelen

How to protect your .htaccess file?

For security purposes, we recommend you to prevent access to your .htaccess file from...

ArkHost Standaard Nameservers

Hallo daar, en welkom bij ArkHost! Als je nieuw bent op ons platform of overweegt om je domein...

ArkHost Standaard Nameservers

Hallo daar, en welkom bij ArkHost! Als je nieuw bent op ons platform of overweegt om je domein...

Hoe lang duurt een domeinnaam verhuizen meestal?

Het kan tot 14 dagen duren voordat een domeinverhuizing is voltooid.Het is moeilijk om een exact...

Hoe u uw domein naar ArkHost kunt overzetten

Het overzetten van uw domein naar ArkHost omvat verschillende belangrijke stappen. Volg deze gids...