Введение
Это руководство предоставляет подробные инструкции по настройке конфигурации 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):
Обновите вашу систему:
sudo apt update && sudo apt upgrade -y
Установите PowerDNS и бэкенд MySQL:
sudo apt install pdns-server pdns-backend-mysql mariadb-server -y
Защитите вашу установку MySQL:
sudo mysql_secure_installation
Создайте базу данных и пользователя для 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;
Импортируйте схему PowerDNS:
sudo mysql -u root -p pdns < /usr/share/doc/pdns-backend-mysql/schema.mysql.sql
Часть 2: Настройка PowerDNS
На мастер-сервере (ns1):
Отредактируйте конфигурационный файл PowerDNS:
sudo nano /etc/powerdns/pdns.conf
Добавьте или измените следующие строки:
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_ПОДЧИНЕННОГО_СЕРВЕРА
Вот как можно сгенерировать безопасный API-ключ:
openssl rand -base64 30
На подчиненном сервере (ns2):
Отредактируйте конфигурационный файл PowerDNS:
sudo nano /etc/powerdns/pdns.conf
Добавьте или измените следующие строки:
launch=gmysql gmysql-host=localhost gmysql-user=pdns gmysql-dbname=pdns gmysql-password=ваш_безопасный_пароль slave=yes superslave=yes
Добавьте мастер-сервер как супермастер:
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: Тестирование настройки
На мастер-сервере создайте тестовую зону:
sudo pdnsutil create-zone example.com ns1.example.com sudo pdnsutil add-record example.com www A 192.0.2.1
Уведомите подчиненный сервер:
sudo pdns_control notify example.com
На подчиненном сервере проверьте, передалась ли зона:
sudo pdnsutil list-zone example.com
Часть 6: Внедрение скрипта управления зонами
Установите jq на мастер-сервере:
sudo apt install jq -y
Создайте файл скрипта:
sudo nano /usr/local/bin/pdns_zone_monitor.sh
Вставьте следующее содержимое:
#!/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
Сделайте скрипт исполняемым:
sudo chmod +x /usr/local/bin/pdns_zone_monitor.sh
Создайте файл службы systemd:
sudo nano /etc/systemd/system/pdns-zone-monitor.service
Добавьте следующее содержимое:
[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
Включите и запустите службу:
sudo systemctl daemon-reload sudo systemctl enable pdns-zone-monitor sudo systemctl start pdns-zone-monitor
Часть 7: Внедрение скрипта импорта зон
Создайте скрипт импорта PowerDNS:
sudo nano /usr/local/bin/powerdns_importer.py
Вставьте следующее содержимое:
#!/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()
Сделайте скрипт исполняемым:
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: Внедрение скрипта обновления подчиненных серверов
Создайте скрипт обновления всех подчиненных серверов:
sudo nano /usr/local/bin/update_all_slaves.py
Вставьте следующее содержимое:
#!/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("Не удалось обновить все подчиненные серверы")
Сделайте скрипт исполняемым:
sudo chmod +x /usr/local/bin/update_all_slaves.py
Часть 9: Использование скриптов
- Для автоматического мониторинга и управления зонами: Скрипт
pdns_zone_monitor.sh
будет работать как служба, постоянно отслеживая новые зоны и управляя ими. Для импорта зон из текстовых файлов:
python3 /usr/local/bin/powerdns_importer.py /путь/к/директории/с/файлами/зон
Для обновления всех подчиненных серверов:
python3 /usr/local/bin/update_all_slaves.py
Часть 10: Мониторинг и обслуживание
Проверьте статус PowerDNS:
sudo systemctl status pdns
Отслеживайте скрипт управления зонами:
sudo journalctl -u pdns-zone-monitor -f
Просмотрите журнал скрипта:
tail -f /var/log/pdns_zone_monitor.log
Регулярно выполняйте резервное копирование базы данных 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