Введение: Зачем нужен DANE/TLSA?
В современной экосистеме электронной почты безопасность доставки сообщений является критически важной. Протокол SMTP, будучи фундаментальным, изначально не был защищен. Для его защиты был разработан механизм SMTP TLS, который обеспечивает шифрование соединения между почтовыми серверами. Однако он уязвим к атакам "человек посередине" (MitM), если злоумышленник может подделать сертификат.
Технология DANE (DNS-based Authentication of Named Entities) решает эту проблему, используя DNSSEC в качестве корня доверия. TLSA-запись в DNS связывает доменное имя сервера с его сертификатом или открытым ключом. Получатель почты может проверить, что сертификат отправителя соответствует записи в DNS, защищенной DNSSEC, что делает подделку практически невозможной.
Для работы DANE необходимо, чтобы TLSA-записи всегда соответствовали действительным сертификатам на сервере. Этот процесс идеально подходит для автоматизации.
На помощь можно использовать Python-скрипт для автоматического обновления TLSA-записей
Представленный Python-скрипт решает задачу автоматического обновления TLSA-записей на авторитативном DNS-сервере PowerDNS при обновлении сертификатов. Это ключевой компонент для поддержания актуальности DANE в инфраструктуре.
import os
import subprocess
import requests
import json
from datetime import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import syslog
import time
# Конфигурация
CERT_DIR = "/etc/postfix/certificates/" # Дирректория с сертификатами
API_URL = "https://dns.myzone.tdl:8081/api/v1/servers/localhost/zones/myzone.tdl."
API_KEY = "aaabbbccc" # Секретный токен для взаимодействия с API PowerDNS
HEADERS = {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
}
# Настройки SMTP для отправки почты
SMTP_SERVER = "mail.myzone.tdl" # Адрес почтового сервера
SMTP_PORT = 587
SMTP_USER = "warnlog@myzone.tdl"
SMTP_PASSWORD = "xyz" # Пароль от учетной записи
EMAIL_FROM = "warnlog@myzone.tdl"
EMAIL_TO = "my@myzone.tdl" # Адрес для уведомления по почте
# Настройки Telegram
TELEGRAM_BOT_TOKEN = "" # Токен телеграмм бота
TELEGRAM_CHAT_ID = "" # Ваш ID в телеграмм
TELEGRAM_API_URL = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
# Функция для логирования в системный лог (syslog)
def log_to_syslog(message, level=syslog.LOG_INFO):
"""
Логирует сообщение в системный лог.
:param message: Текст сообщения
:param level: Уровень логирования (по умолчанию INFO)
"""
syslog.syslog(level, f"TLSA Update Script: {message}")
# Функция для извлечения хэша сертификата
def get_cert_hash(cert_path):
"""
Извлекает SHA-256 хэш сертификата с помощью OpenSSL.
:param cert_path: Путь к сертификату
:return: Хэш сертификата или None в случае ошибки
"""
cmd = f"openssl x509 -in {cert_path} -outform DER | openssl dgst -sha256" # Команда для получения хэша
result = subprocess.tdln(cmd, shell=True, capture_output=True, text=True) # Выполнение команды
if result.returncode != 0: # Если команда завершилась с ошибкой
log_to_syslog(f"Ошибка извлечения хэша для {cert_path}: {result.stderr}", syslog.LOG_ERR)
return None
return result.stdout.split("= ")[1].strip() # Возвращаем хэш
# Функция для обновления TLSA записей
def update_tlsa_records(record_name, cert_hash_rsa, cert_hash_ecc):
"""
Обновляет TLSA записи на DNS сервере через PowerDNS API.
:param record_name: Имя записи (например, "_dane.myzone.tdl.")
:param cert_hash_rsa: Хэш RSA сертификата
:param cert_hash_ecc: Хэш ECDSA сертификата
:return: True, если обновление прошло успешно, иначе False
"""
# Формируем данные для отправки в API
data = {
"rrsets": [
{
"name": record_name, # Имя записи
"type": "TLSA", # Тип записи
"ttl": 3600, # Время жизни записи
"changetype": "REPLACE", # Тип изменения (замена)
"records": [
{
"content": f"3 0 1 {cert_hash_rsa}", # TLSA запись для RSA
"disabled": False # Запись активна
},
{
"content": f"3 0 1 {cert_hash_ecc}", # TLSA запись для ECDSA
"disabled": False # Запись активна
}
]
}
]
}
try:
# Отправляем PATCH-запрос к PowerDNS API
response = requests.patch(API_URL, headers=HEADERS, json=data)
# Обрабатываем успешные статусы 200 и 204
if response.status_code in (200, 204):
log_to_syslog(f"TLSA записи успешно обновлены. Статус: {response.status_code}")
return True
else:
# Логируем ошибку, если статус не 200 или 204
log_to_syslog(f"Ошибка обновления TLSA. Код: {response.status_code}, Ответ: {response.text}", syslog.LOG_ERR)
return False
except Exception as e:
# Логируем критические ошибки (например, сетевые)
log_to_syslog(f"Критическая ошибка API: {str(e)}", syslog.LOG_CRIT)
return False
# Функция для отправки почтового уведомления
def send_email(subject, body):
"""
Отправляет email уведомление.
:param subject: Тема письма
:param body: Текст письма
"""
try:
msg = MIMEMultipart() # Создаем MIME-сообщение
msg["From"] = EMAIL_FROM # Адрес отправителя
msg["To"] = EMAIL_TO # Адрес получателя
msg["Subject"] = subject # Тема письма
msg.attach(MIMEText(body, "plain")) # Добавляем текст письма
# Подключаемся к SMTP-серверу и отправляем письмо
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls() # Включаем TLS
server.login(SMTP_USER, SMTP_PASSWORD) # Авторизуемся
server.send_message(msg) # Отправляем письмо
log_to_syslog("Email уведомление отправлено")
except Exception as e:
# Логируем ошибки отправки email
log_to_syslog(f"Ошибка отправки email: {str(e)}", syslog.LOG_ERR)
# Функция для отправки сообщения в Telegram
def send_telegram(message):
"""
Отправляет сообщение в Telegram через бота.
:param message: Текст сообщения
"""
try:
payload = {
"chat_id": TELEGRAM_CHAT_ID, # ID чата
"text": message, # Текст сообщения
"parse_mode": "Markdown", # Форматирование Markdown
"disable_web_page_preview": True # Отключаем предпросмотр ссылок
}
# Отправляем POST-запрос к API Telegram
response = requests.post(TELEGRAM_API_URL, json=payload)
response.raise_for_status() # Проверяем на ошибки
except Exception as e:
# Логируем ошибки отправки в Telegram
log_to_syslog(f"Ошибка отправки в Telegram: {str(e)}", syslog.LOG_ERR)
# Функция для перезагрузки сервисов
def restart_services():
"""
Перезагружает сервисы apache2, postfix и dovecot.
"""
services = ["apache2", "postfix", "dovecot"] # Список сервисов
for service in services:
try:
# Перезагружаем конфигурацию сервиса с помощью systemctl reload
subprocess.tdln(
["systemctl", "reload", service],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
log_to_syslog(f"Конфигурация сервиса {service} перезагружена (reload)")
except subprocess.CalledProcessError as e:
# Логируем ошибки перезагрузки сервиса
log_to_syslog(f"Ошибка перезагрузки {service}: {str(e)}", syslog.LOG_ERR)
# Основная функция
def main():
time.sleep(5) # Задержка перед выполнением (5 секунд)
# Пути к сертификатам
rsa_cert = os.path.join(CERT_DIR, "mail.fullchain.pem")
ecc_cert = os.path.join(CERT_DIR, "mail.fullchain-ecc.pem")
# Проверяем, существуют ли оба сертификата
if not all(os.path.exists(f) for f in [rsa_cert, ecc_cert]):
log_to_syslog("Не все сертификаты найдены", syslog.LOG_WARNING)
return
# Получаем хэши сертификатов
rsa_hash = get_cert_hash(rsa_cert)
ecc_hash = get_cert_hash(ecc_cert)
# Проверяем, удалось ли получить хэши
if not all([rsa_hash, ecc_hash]):
log_to_syslog("Ошибка получения хэшей", syslog.LOG_ERR)
return
# Обновляем TLSA записи на DNS сервере
success = update_tlsa_records("_dane.myzone.tdl.", rsa_hash, ecc_hash)
# Формируем сообщение о статусе обновления
status_message = f"""Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Хэш RSA: {rsa_hash[:16]}...{rsa_hash[-16:]}
Хэш ECDSA: {ecc_hash[:16]}...{ecc_hash[-16:]}
Статус: {"УСПЕШНО" if success else "ОШИБКА"}"""
# Отправляем уведомления и перезагружаем сервисы, если обновление прошло успешно
if success:
send_email("✅ Обновление TLSA", status_message)
send_telegram(f"✅ *TLSA Обновлено*\n{status_message}")
restart_services()
else:
send_email("❌ Ошибка TLSA", status_message)
send_telegram(f"❌ *Ошибка TLSA!*\n{status_message}")
# Точка входа в программу
if __name__ == "__main__":
main()
Архитектура и принцип работы
Скрипт выполняет последовательность действий, обеспечивая сквозную автоматизацию процесса.
-
Извлечение хэшей сертификатов:
-
Скрипт ожидает наличия двух сертификатов в директории
/etc/postfix/certificates/
:mail.fullchain.pem
— сертификат RSA (для совместимости).mail.fullchain-ecc.pem
— сертификат ECDSA (более современный и безопасный).
-
С помощью командной утилиты
openssl
вычисляются хэши SHA-256 от каждого сертификата в DER-формате.openssl x509 -in {cert_path} -outform DER | openssl dgst -sha256
2. Взаимодействие с PowerDNS API
Это ядро скрипта. Для обновления зоны используется HTTP PATCH-запрос к REST API PowerDNS.
Важное примечание о предварительной настройке DNS:
Перед тем как скрипт начнет работать, в DNS-зоне должны быть заранее созданы CNAME-записи, которые перенаправляют запросы на TLSA-записи для конкретных сервисов на одно общее имя. Это стандартная и рекомендуемая практика для упрощения управления DANE.Кроме этого DNS зона должна поддерживать DNSSEC и соответствующим образом настроена. Это ключевое требование при использовании технологии DANE.
Для почтового сервера обычно создаются следующие CNAME-записи:
_25._tcp.mail CNAME Active 86400 _dane.myzone.tdl. _587._tcp.mail CNAME Active 86400 _dane.myzone.tdl. _993._tcp.mail CNAME Active 86400 _dane.myzone.tdl.
Эти записи указывают, что TLSA-записи для портов 25, 587 и 993 следует искать по имени
_dane.myzone.tdl.
.
Это позволяет управлять всеми записями через один единственный ресурс, который и обновляет наш скрипт.Работа скрипта с API:
Формируется JSON-объект, содержащий новую ресурсную запись типа TLSA для имени
_dane.myzone.tdl.
-
Ключевые параметры API:
name
:_dane.myzone.tdl.
(имя записи, обязательно с точкой в конце).type
:TLSA
.changetype
:REPLACE
(полностью заменяет существующие TLSA-записи для этого имени).-
records
: Массив, содержащий две TLSA-записи в формате"3 0 1 <CERT_HASH>"
.3
— соответствие сертификату (а не только открытому ключу).0
— использование полного сертификата.1
— алгоритм хэширования SHA-256.
Запрос отправляется на арендованный или собственный DNS сервер по адресу вида
https://dns.myzone.ru:8081/api/v1/servers/localhost/zones/myzone.ru.
Аутентификация выполняется с помощью заголовка
X-API-Key
.
Результат работы скрипта:
В результате успешного выполнения скрипта в DNS-зоне появятся или будут обновлены две TLSA-записи для имени_dane.myzone.tdl.
:_dane.myzone.tdl. TLSA 3 0 1 1e9ed... (полный хэш RSA-сертификата) _dane.myzone.tdl. TLSA 3 0 1 3b4ff... (полный хэш ECDSA-сертификата)
Благодаря предварительно настроенным CNAME-записям, эти две TLSA-записи автоматически становятся действительными для всех указанных сервисов (портов 25, 587, 993), что обеспечивает безопасность по DANE для всего почтового трафика.
-
-
Уведомление о результате:
-
Для оперативного оповещения о результате работы скрипт интегрирован с двумя каналами:
Email: Отправляется письмо на почтовый ящик администратора через SMTP с указанием статуса ("УСПЕХ" или "ОШИБКА") и сокращенными хэшами сертификатов для верификации.
Telegram: Отправляется структурированное сообщение в Telegram-чат через Bot API. Поддержка Markdown делает сообщение легкочитаемым.
-
-
Применение новых сертификатов (Перезагрузка сервисов):
В случае успешного обновления DNS-записей скрипт перезагружает конфигурацию критически важных сервисов, чтобы они начали использовать новые сертификаты.
Важно: Используется команда
systemctl reload
для службapache2
,postfix
иdovecot
. Это позволяет применить новую конфигурацию без полной остановки служб, минимизируя простой.
-
Комплексное логирование:
Все этапы работы, успехи и ошибки записываются в системный журнал (syslog) с различными уровнями серьезности (
LOG_INFO
,LOG_ERR
,LOG_CRIT
). Это позволяет централизованно отслеживать выполнение скрипта и оперативно диагностировать проблемы.
Сценарий использования и интеграция
Данный скрипт предназначен для запуска в качестве пост-хука (post-hook) в процессе автоматического обновления сертификатов, например, в certbot
.
Пример сценария в certbot-renew
:
certbot
определяет, что сертификаты нуждаются в обновлении.Он получает новые сертификаты от ACME-сервера (например, Let's Encrypt).
certbot
размещает их в указанную директорию (/etc/postfix/certificates/
).certbot
запускает пост-хук, которым является наш скрипт.Скрипт вычисляет хэши новых сертификатов, обновляет записи в PowerDNS, уведомляет администратора и перезагружает сервисы.
Процесс завершен: новые сертификаты активны на сервере, а соответствующие им TLSA-записи уже опубликованы в DNS.
Заключение
Представленное решение устраняет "слабое звено" в развертывании DANE — ручное обновление DNS-записей. Автоматизируя этот процесс, мы обеспечиваем:
Безопасность: TLSA-записи всегда актуальны и соответствуют сертификатам, что гарантирует работу DANE.
Надежность: Исключается человеческий фактор, забывчивость администратора.
Оперативность: Процесс от получения сертификата до его полного развертывания занимает секунды.
Мониторинг: Администратор получает немедленное уведомление как об успехе, так и о возможных сбоях.
Интеграция скрипта с PowerDNS через его мощный API, а также с популярными системами оповещения делает его robust-решением для поддержания высокого уровня безопасности почтовой инфраструктуры.
Постскриптум
Это часть домашней инфраструктуры с собственным DNS, облаком и почтовым сервером.
Начало всего было закрытие бесплатных сервисов от Яндекса и статьей на Хабре Настраиваем домашний почтовый сервер и уходим с «бесплатной» почты
vened
Всё ж, CNAME с разных номеров сервисов на единую нестандартную запись, да ещё с другим именем хоста в её составе, – не очень хорошая идея. Лучше бы публиковать индивидуальные TLSA под теми именами/номерами, к которым они относятся. (CNAME – вообще не очень хорошая штука, за очень редкими исключениями, а в паре с DNSSEC – так ещё хуже.)
Тут бы нужно предусмотреть период ожидания для замены серверного сертификата (rollover): то есть, выкатывать новую TLSA-запись для нового сертификата заранее, рядом со старой, а переходить на новый сертификат только после того, как истечёт TTL (лучше – два TTL) старой TLSA-записи; после перехода на новый сертификат – удалять TLSA-запись для старого.
plusQ Автор
CNAME у меня работает, проверка успешно проходит на серверах microsoft и gmail. Есть рекомендация RFC 6698 на странице 28.
Со вторым замечанием согласен, конечно же новые TLSA-записи должны быть установлены заранее. Для домашнего сервера это не критично и можно уменьшить TTL для TLSA записей. А вообще конечно нужно доработать скрипт чтобы он запускался дважды, сначала добавлял две новых TLSA записи а потом оставлял только актуальные.
vened
Вы что-то перепутали. В этом RFC нет рекомендации ставить для TLSA-записей разных сервисов CNAME на одно произвольное общее имя.