Настройка конфигурации PowerDNS Master-Slave с автоматическим управлением и импортом зон

Введение

Это руководство предоставляет подробные инструкции по настройке конфигурации PowerDNS master-slave на Debian 12, включая установку, настройку и внедрение скриптов для автоматического управления и импорта зон. Эта настройка идеально подходит для управления несколькими DNS-зонами с автоматической репликацией между серверами и эффективным импортом зон.


Предварительные требования

  • Доступ по SSH: Обеспечьте доступ по SSH без пароля с вашего мастер-сервера (ns1) на подчиненный сервер (ns2). Это критически важно для автоматизированной передачи зон и управления. Вы можете сделать это, настроив SSH-ключи.
  • Сетевое соединение: Убедитесь, что оба сервера (ns1 и ns2) могут взаимодействовать друг с другом на необходимых портах (в основном порт 53 для DNS и порт 8081 для API, если он включен). Используйте такие инструменты, как ping и telnet или nc для проверки соединения.
  • Точные IP-адреса серверов: Имейте под рукой правильные IP-адреса для ns1 и ns2, поскольку они понадобятся вам для настройки.

Часть 1: Установка PowerDNS

На обоих серверах - мастере (ns1) и подчиненном (ns2):

  1. Обновите вашу систему:

    sudo apt update && sudo apt upgrade -y
  2. Установите PowerDNS и бэкенд MySQL:

    sudo apt install pdns-server pdns-backend-mysql mariadb-server -y
  3. Защитите вашу установку MySQL:

    sudo mysql_secure_installation
  4. Создайте базу данных и пользователя для PowerDNS:

    sudo mysql -u root -p

    В командной строке MySQL:

    CREATE DATABASE pdns;
    CREATE USER 'pdns'@'localhost' IDENTIFIED BY 'ваш_безопасный_пароль';
    GRANT ALL PRIVILEGES ON pdns.* TO 'pdns'@'localhost';
    FLUSH PRIVILEGES;
    EXIT;
  5. Импортируйте схему PowerDNS:

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

Часть 2: Настройка PowerDNS

На мастер-сервере (ns1):

  1. Отредактируйте конфигурационный файл PowerDNS:

    sudo nano /etc/powerdns/pdns.conf
  2. Добавьте или измените следующие строки:

    launch=gmysql
    gmysql-host=localhost
    gmysql-user=pdns
    gmysql-dbname=pdns
    gmysql-password=ваш_безопасный_пароль
    api=yes
    api-key=сгенерируйте_безопасный_api_ключ
    webserver=yes
    webserver-port=8081
    webserver-address=0.0.0.0
    webserver-allow-from=127.0.0.1,ДРУГОЙ_IP,ДРУГОЙ_IP
    allow-axfr-ips=IP_ПОДЧИНЕННОГО_СЕРВЕРА
    master=yes
    slave=no
    also-notify=IP_ПОДЧИНЕННОГО_СЕРВЕРА
  3. Вот как можно сгенерировать безопасный API-ключ:

    openssl rand -base64 30

На подчиненном сервере (ns2):

  1. Отредактируйте конфигурационный файл PowerDNS:

    sudo nano /etc/powerdns/pdns.conf
  2. Добавьте или измените следующие строки:

    launch=gmysql
    gmysql-host=localhost
    gmysql-user=pdns
    gmysql-dbname=pdns
    gmysql-password=ваш_безопасный_пароль
    slave=yes
    superslave=yes
  3. Добавьте мастер-сервер как супермастер:

    sudo mysql -u pdns -p pdns

    В командной строке MySQL:

    INSERT INTO supermasters (ip, nameserver, account) VALUES ('IP_МАСТЕР_СЕРВЕРА', 'ns1.вашдомен.com', 'admin');
    EXIT;

Часть 3: Настройка брандмауэра

На обоих серверах настройте брандмауэр, чтобы разрешить только необходимый трафик из доверенных источников. Мы разрешим порт 53 отовсюду, так как настройка предназначена для публичных авторитативных DNS-серверов:

# Разрешить DNS-трафик
sudo ufw allow 53/tcp
sudo ufw allow 53/udp

# Разрешить API-трафик на порту 8081 (ограничено конкретными IP)
# Замените 192.0.2.1 на фактический IP-адрес, которому нужен доступ
sudo ufw allow from 192.0.2.1 to any port 8081 proto tcp

# Если нескольким IP нужен доступ, добавьте каждый отдельно
# sudo ufw allow from 10.0.0.1 to any port 8081 proto tcp

# Разрешить SSH-доступ только с определенных jumphost/доверенных IP
# Замените 198.51.100.5 на IP вашего jumphost/управляющего сервера
sudo ufw allow from 198.51.100.5 to any port 22 proto tcp

# Запретить все другие SSH-соединения
sudo ufw deny 22/tcp

# Включить брандмауэр
sudo ufw enable

# Проверить правила
sudo ufw status verbose

Часть 4: Запуск PowerDNS

На обоих серверах:

sudo systemctl start pdns
sudo systemctl enable pdns

Часть 5: Тестирование настройки

  1. На мастер-сервере создайте тестовую зону:

    sudo pdnsutil create-zone example.com ns1.example.com
    sudo pdnsutil add-record example.com www A 192.0.2.1
  2. Уведомите подчиненный сервер:

    sudo pdns_control notify example.com
  3. На подчиненном сервере проверьте, передалась ли зона:

    sudo pdnsutil list-zone example.com

Часть 6: Внедрение скрипта управления зонами

  1. Установите jq на мастер-сервере:

    sudo apt install jq -y
  2. Создайте файл скрипта:

    sudo nano /usr/local/bin/pdns_zone_monitor.sh
  3. Вставьте следующее содержимое:

    #!/bin/bash
    
    # Конфигурация
    API_KEY="API_КЛЮЧ"
    API_URL="http://IP_МАСТЕР_СЕРВЕРА:8081"  # Замените на IP вашего мастер-сервера
    NS1="ns1.example.com"
    NS2="ns2.example.com"
    LOG_FILE="/var/log/pdns_zone_monitor.log"
    STATE_FILE="/var/log/pdns_processed_zones.txt"
    
    # Функция для записи сообщений в журнал
    log_message() {
        echo "$(date): $1" >> $LOG_FILE
    }
    
    # Функция для исправления записей SOA и NS для зоны
    fix_zone_records() {
        local zone=$1
        local serial=$(date +%Y%m%d%H)
    
        # Обновить записи SOA и NS
        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"
    
        # Увеличить серийный номер и уведомить подчиненные серверы
        pdnsutil increase-serial "$zone"
        pdns_control notify "$zone"
    
        log_message "Исправлены записи для зоны $zone"
    }
    
    # Функция для удаления зоны с ns2
    remove_zone_from_ns2() {
        local zone=$1
        ssh root@ns2 "pdnsutil delete-zone $zone"
        log_message "Удалена зона $zone с ns2"
    }
    
    # Функция для очистки ns2 путем удаления зон, отсутствующих на ns1
    clean_ns2() {
        log_message "Начинается процесс очистки на ns2"
        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 "Первичные зоны на ns1: $primary_zones"
        log_message "Вторичные зоны на ns2: $secondary_zones"
    
        # Убедиться, что зоны сравниваются правильно
        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 с ns2"
                remove_zone_from_ns2 "$zone"
            fi
        done
    }
    
    # Загрузить обработанные зоны
    if [ -f "$STATE_FILE" ]; then
        mapfile -t processed_zones < "$STATE_FILE"
    else
        touch "$STATE_FILE"
        processed_zones=()
    fi
    
    # Получить список текущих зон
    current_zones=$(curl -s -H "X-API-Key: $API_KEY" "$API_URL/api/v1/servers/localhost/zones" | jq -r '.[].name')
    
    # Преобразовать current_zones в массив
    current_zones_array=($current_zones)
    
    # Основной цикл
    while true; do
        # Получить новый список зон
        new_zones=$(curl -s -H "X-API-Key: $API_KEY" "$API_URL/api/v1/servers/localhost/zones" | jq -r '.[].name')
        
        # Преобразовать new_zones в массив
        new_zones_array=($new_zones)
        
        # Проверить наличие новых зон и обработать их
        for zone in "${new_zones_array[@]}"; do
            if [[ ! " ${current_zones_array[*]} " =~ " $zone " ]] && [[ ! " ${processed_zones[*]} " =~ " $zone " ]]; then
                log_message "Обнаружена новая зона: $zone"
                fix_zone_records "$zone"
                echo "$zone" >> "$STATE_FILE"
                processed_zones+=("$zone")
            fi
        done
        
        # Проверить удаленные зоны и обработать их
        for zone in "${current_zones_array[@]}"; do
            if [[ ! " ${new_zones_array[*]} " =~ " $zone " ]]; then
                log_message "Зона удалена: $zone"
                remove_zone_from_ns2 "$zone"
                processed_zones=("${processed_zones[@]/$zone}")
                sed -i "\|$zone|d" "$STATE_FILE"
            fi
        done
        
        # Очистить ns2, удалив зоны, отсутствующие на ns1
        clean_ns2
        
        # Обновить список текущих зон
        current_zones_array=("${new_zones_array[@]}")
        
        # Подождать 1 минуту перед повторной проверкой
        sleep 60
    done
  4. Сделайте скрипт исполняемым:

    sudo chmod +x /usr/local/bin/pdns_zone_monitor.sh
  5. Создайте файл службы systemd:

    sudo nano /etc/systemd/system/pdns-zone-monitor.service
  6. Добавьте следующее содержимое:

    [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. Включите и запустите службу:

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

Часть 7: Внедрение скрипта импорта зон

  1. Создайте скрипт импорта PowerDNS:

    sudo nano /usr/local/bin/powerdns_importer.py
  2. Вставьте следующее содержимое:

    #!/usr/bin/env python3
    import os
    import sys
    import subprocess
    import logging
    
    # Настройка логирования
    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"Ошибка выполнения команды {' '.join(command)}. Ошибка: {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"Не удалось определить имя зоны из {zone_file}")
            return False
    
        try:
            # Удалить зону, если она существует
            run_command(['pdnsutil', 'delete-zone', zone_name])
            logging.info(f"Удалена существующая зона: {zone_name}")
        except:
            logging.warning(f"Зона {zone_name} не существовала, продолжаем создание")
    
        # Создать зону
        run_command(['pdnsutil', 'create-zone', zone_name])
        logging.info(f"Создана новая зона: {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]
            
            # Правильно обработать доменное имя
            if name == zone_name or name == "@":
                name = "@"
            else:
                name = name.rstrip('.')
                if name.endswith(zone_name):
                    name = name[:-len(zone_name)-1]  # Удалить имя зоны с конца
                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}: {name} {record_ttl} IN {record_type} {content}")
            else:
                logging.error(f"Не удалось добавить запись: {line}")
    
        # Исправить зону
        run_command(['pdnsutil', 'rectify-zone', zone_name])
        logging.info(f"Исправлена зона: {zone_name}")
    
        return True
    
    def sync_to_ns2(zone_name):
        try:
            run_command(['pdns_control', 'notify', zone_name])
            logging.info(f"Запущено уведомление для {zone_name} на ns2")
            return True
        except Exception as e:
            logging.error(f"Ошибка синхронизации {zone_name} с ns2: {str(e)}")
            return False
    
    def main():
        if len(sys.argv) < 2:
            print("Использование: python powerdns_importer.py ")
            sys.exit(1)
    
        input_directory = sys.argv[1]
        if not os.path.isdir(input_directory):
            logging.error(f"Ошибка: {input_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"Обработка {zone_file}")
                if import_zone(zone_file):
                    sync_to_ns2(os.path.splitext(filename)[0])
    
    if __name__ == "__main__":
        main()
  3. Сделайте скрипт исполняемым:

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

Скрипт импорта зон работает с экспортированными зонами CloudFlare. Ниже приведен скрипт для экспорта всех зон из CloudFlare (не забудьте указать свой API-ключ):

#!/usr/bin/env python3

import requests
import os
import sys

# Установите ваши учетные данные API Cloudflare
api_token = 'ВАШ_CLOUDFLARE_API_КЛЮЧ'
headers = {
    'Authorization': f'Bearer {api_token}',
    'Content-Type': 'application/json'
}

# Конечные точки API Cloudflare
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"Не удалось получить данные: {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} в файл {filename}")

def main():
    if api_token == 'ВАШ_CLOUDFLARE_API_КЛЮЧ':
        print("Пожалуйста, установите ваш токен API Cloudflare в скрипте.")
        sys.exit(1)

    zones = fetch_zones()
    if not zones:
        print("Зоны не найдены или не удалось получить зоны.")
        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"Получение записей для зоны: {zone_name}")
        records = fetch_zone_records(zone_id)
        if records:
            export_zone_to_file(zone_name, records)

if __name__ == "__main__":
    main()

Часть 8: Внедрение скрипта обновления подчиненных серверов

  1. Создайте скрипт обновления всех подчиненных серверов:

    sudo nano /usr/local/bin/update_all_slaves.py
  2. Вставьте следующее содержимое:

    #!/usr/bin/env python3
    import subprocess
    import logging
    
    # Настройка логирования
    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"Ошибка выполнения команды {' '.join(command)}. Ошибка: {e.stderr.strip()}")
            return None
    
    def update_all_slaves():
        try:
            # Получить список всех зон
            zones_output = run_command(['pdnsutil', 'list-all-zones'])
            if zones_output is None:
                logging.error("Не удалось получить список зон")
                return False
    
            zones = zones_output.split('\n')
    
            # Уведомить каждую зону
            for zone in zones:
                result = run_command(['pdns_control', 'notify', zone])
                if result is not None:
                    logging.info(f"Запущено уведомление для {zone} на подчиненные серверы")
                else:
                    logging.warning(f"Не удалось отправить уведомление для зоны: {zone}")
    
            logging.info("Завершены уведомления для всех зон")
            return True
        except Exception as e:
            logging.error(f"Ошибка обновления всех подчиненных серверов: {str(e)}")
            return False
    
    if __name__ == "__main__":
        if update_all_slaves():
            logging.info("Успешно обновлены все подчиненные серверы")
        else:
            logging.error("Не удалось обновить все подчиненные серверы")
  3. Сделайте скрипт исполняемым:

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

Часть 9: Использование скриптов

  1. Для автоматического мониторинга и управления зонами: Скрипт pdns_zone_monitor.sh будет работать как служба, постоянно отслеживая новые зоны и управляя ими.
  2. Для импорта зон из текстовых файлов:

    python3 /usr/local/bin/powerdns_importer.py /путь/к/директории/с/файлами/зон
  3. Для обновления всех подчиненных серверов:

    python3 /usr/local/bin/update_all_slaves.py

Часть 10: Мониторинг и обслуживание

  1. Проверьте статус PowerDNS:

    sudo systemctl status pdns
  2. Отслеживайте скрипт управления зонами:

    sudo journalctl -u pdns-zone-monitor -f
  3. Просмотрите журнал скрипта:

    tail -f /var/log/pdns_zone_monitor.log
  4. Регулярно выполняйте резервное копирование базы данных PowerDNS:

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

Часть 11: Резервное копирование зон

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()


# Получить текущую дату и время
current_date = datetime.now().strftime("%Y-%m-%d")

# Получить все зоны
zones = run_pdnsutil('list-all-zones').split('\n')

# Создать выходной каталог
output_dir = f'/root/backups/output_{current_date}'
os.makedirs(output_dir, exist_ok=True)

# Сделать резервную копию каждой зоны
for zone in zones:
  print(f"Создание резервной копии зоны {zone}")
  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"Резервное копирование завершено. Файлы сохранены в {output_dir}.")
crontab -e

0 4 * * * /usr/bin/python3 /root/scripts/zone_backups.py
  • 31 Пользователи нашли это полезным
Помог ли вам данный ответ?

Связанные статьи

How to protect your .htaccess file?

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

Стандартные Nameservers ArkHost

Приветствуем вас и добро пожаловать в ArkHost! Если вы новичок на нашей платформе или просто...

Стандартные Nameservers ArkHost

Приветствуем вас и добро пожаловать в ArkHost! Если вы новичок на нашей платформе или просто...

Сколько времени обычно занимает передача доменного имени?

Трансфер домена может занять до 14 дней.Точные сроки назвать сложно, поскольку процедура зависит...

Как перенести свой домен на ArkHost

Передача вашего домена в ArkHost включает несколько важных шагов. Следуйте этому руководству,...