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:
Update uw systeem:
sudo apt update && sudo apt upgrade -y
Installeer PowerDNS en de MySQL-backend:
sudo apt install pdns-server pdns-backend-mysql mariadb-server -y
Beveilig uw MySQL-installatie:
sudo mysql_secure_installation
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;
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):
Bewerk het PowerDNS-configuratiebestand:
sudo nano /etc/powerdns/pdns.conf
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
Zo kunt u een veilige API-sleutel genereren:
openssl rand -base64 30
Op de Slaveserver (ns2):
Bewerk het PowerDNS-configuratiebestand:
sudo nano /etc/powerdns/pdns.conf
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
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
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
Informeer de slave:
sudo pdns_control notify voorbeeld.com
Controleer op de slave of de zone is overgedragen:
sudo pdnsutil list-zone voorbeeld.com
Deel 6: Het Zonebeheersscript Implementeren
Installeer jq op de masterserver:
sudo apt install jq -y
Maak het scriptbestand aan:
sudo nano /usr/local/bin/pdns_zone_monitor.sh
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
Maak het script uitvoerbaar:
sudo chmod +x /usr/local/bin/pdns_zone_monitor.sh
Maak een systemd-servicebestand aan:
sudo nano /etc/systemd/system/pdns-zone-monitor.service
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
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
Maak het PowerDNS-importscript aan:
sudo nano /usr/local/bin/powerdns_importer.py
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()
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
Maak het script voor het bijwerken van alle slaves aan:
sudo nano /usr/local/bin/update_all_slaves.py
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")
Maak het script uitvoerbaar:
sudo chmod +x /usr/local/bin/update_all_slaves.py
Deel 9: De Scripts Gebruiken
- 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. Voor het importeren van zones vanuit tekstbestanden:
python3 /usr/local/bin/powerdns_importer.py /pad/naar/zone/bestanden/directory
Voor het bijwerken van alle slave-servers:
python3 /usr/local/bin/update_all_slaves.py
Deel 10: Monitoring en Onderhoud
Controleer de status van PowerDNS:
sudo systemctl status pdns
Monitor het zonebeheersscript:
sudo journalctl -u pdns-zone-monitor -f
Bekijk het logbestand van het script:
tail -f /var/log/pdns_zone_monitor.log
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