Введение в термины:
SPF говорит, с каких IP домен разрешает отправку.
DKIM подписывает письмо ключом, открытая часть которого лежит в DNS.
DMARC связывает обе проверки с адресом в поле From и говорит получателю, что делать при провале. Работает только вся связка целиком, и только поверх правильного PTR: без обратной записи хорошие SPF, DKIM и DMARC не спасут письма от спама.
Порядок настройки такой: PTR у хостера → SPF → DKIM-подпись на сервере → DMARC с p=none и отчётами → через несколько недель ужесточение до quarantine и reject. Самые частые поломки: две SPF-записи на домене, превышение лимита в 10 DNS-запросов после подключения CRM, обрезанная TXT-запись DKIM и DMARC p=reject до того, как найдены все легитимные источники писем.
Примеры ниже — для домена example.com, сервера mail.example.com с IP 85.119.149.3, Postfix на Debian 12/13 и Ubuntu 24.04. Общая картина доставки писем и выбор сервера — в обзоре собственный почтовый сервер: какой выбрать и как не попасть в спам.
Как SPF, DKIM и DMARC работают вместе
Каждый механизм проверяет свой домен. Отсюда большинство недоразумений: SPF может пройти, а DMARC — провалиться.
| Механизм | Что проверяет | Какой домен | Переживает пересылку |
|---|---|---|---|
| SPF (RFC 7208) | IP подключившегося сервера есть в списке разрешённых | Envelope-from (MAIL FROM, он же Return-Path), при пустом — HELO | Нет: пересылающий сервер имеет другой IP |
| DKIM (RFC 6376) | Подпись заголовков и тела совпадает с открытым ключом из DNS | Тег d= в заголовке DKIM-Signature | Да, если письмо не изменили по дороге |
| DMARC (RFC 7489, с мая 2026 — RFC 9989) | Хотя бы один из SPF или DKIM прошёл и выровнен с From | Домен из заголовка From — тот, что видит человек | Да, если уцелел DKIM |
Выравнивание (alignment)
DMARC не просто смотрит на pass у SPF и DKIM. Он требует, чтобы прошедший домен совпадал с доменом в From. Это и есть выравнивание.
- Relaxed (по умолчанию,
adkim=r,aspf=r) — достаточно совпадения организационного домена.d=news.example.comвыровнен сFrom: info@example.com. - Strict (
adkim=s,aspf=s) — нужно точное совпадение имени.
Пример провала: сайт шлёт письмо через PHP с envelope-from www-data@vps123.hoster.net и From info@example.com без подписи. SPF для hoster.net пройдёт, но не выровнен — DMARC fail. Поэтому главная опора DMARC — DKIM с d= вашего домена.
PTR (rDNS): обязательная основа
PTR — обратная запись: IP резолвится в имя. Её проверяют до SPF и DKIM, ещё на этапе SMTP-соединения. Нужно три совпадения (так называемый FCrDNS):
- PTR для 203.0.113.10 возвращает
mail.example.com. - A-запись
mail.example.comвозвращает 85.119.149.3. - Postfix представляется этим именем в HELO/EHLO: параметр
myhostname.
PTR настраивается не в DNS-зоне вашего домена. Зону 3.149.119.85.in-addr.arpa держит владелец IP: хостер или провайдер. Ищите в панели VPS пункт «Reverse DNS», «PTR» или «rDNS», либо пишите в поддержку. PTR, созданный у вашего DNS-регистратора, ни на что не влияет, если провайдер не делегировал вам обратную зону.
Проверка с любой машины, где есть dig (пакет bind9-dnsutils, старое имя — dnsutils):
# PTR: ожидаем mail.example.com.
dig +short -x 85.119.149.3
# Прямая запись: ожидаем 85.119.149.3
dig +short A mail.example.com
# Имя в HELO на сервере
postconf myhostname
Если у сервера есть IPv6 и Postfix отправляет через него, PTR нужен и для IPv6-адреса, а в SPF — механизм ip6:. Нет возможности настроить PTR для IPv6 — отправляйте только по IPv4: postconf -e 'inet_protocols = ipv4' и systemctl restart postfix.
SPF: синтаксис, лимиты и ошибки
Синтаксис и механизмы
SPF — одна TXT-запись на имени домена, начинается с v=spf1. Механизмы проверяются слева направо, первый совпавший определяет результат. Перед механизмом может стоять квалификатор: + pass (по умолчанию), - fail, ~ softfail, ? neutral.
| Механизм | Что значит | DNS-запросов |
|---|---|---|
ip4:85.119.149.3, ip4:85.119.148.0/24 | Адрес или сеть IPv4 | 0 |
ip6:2001:db8::/64 | Адрес или сеть IPv6 | 0 |
a, a:mail.example.com | IP из A/AAAA-записи домена или указанного имени | 1 |
mx | IP всех MX-хостов домена | 1 (+ до 10 запросов адресов MX, в общий лимит не идут) |
include:_spf.provider.example | Результат SPF другого домена | 1 + всё, что внутри |
redirect=_spf.example.com | Модификатор: «используй чужую запись целиком» | 1 + всё, что внутри |
exists:, ptr | Редкие, ptr не рекомендуется (RFC 7208, 5.5) | 1 |
all | Всё остальное, ставится последним | 0 |
Запись для домена, вся почта которого уходит с одного сервера. Создаётся в DNS-зоне example.com, тип TXT, имя @:
example.com. 3600 IN TXT "v=spf1 ip4:85.119.149.3 -all"
Вариант v=spf1 mx -all из обзора тоже корректен, если MX указывает на тот же хост, с которого идёт отправка. ip4: надёжнее: не тратит DNS-запросов и не сломается, когда входящую почту переведут на другой MX.
Для имени mail.example.com тоже стоит опубликовать SPF. Он используется, когда envelope-from пустой (уведомления о недоставке), и тогда проверяется домен из HELO:
mail.example.com. 3600 IN TXT "v=spf1 a -all"
~all или -all
-all — жёсткий отказ всем, кого нет в списке. ~all — «скорее всего подделка». При работающем DMARC разница меньше, чем кажется: итоговое решение принимает политика DMARC. Практическое правило:
- Вся почта домена идёт с ваших серверов, DKIM работает —
-all. - Идёт аудит источников, подключаете CRM или рассылки —
~all, пока DMARC-отчёты не покажут полную картину. - Домен или поддомен вообще не отправляет почту —
v=spf1 -all.
Учтите: некоторые получатели отклоняют письмо по SPF fail ещё на SMTP-этапе, не дожидаясь DKIM. Пересланное письмо при -all может пострадать, даже если его DKIM-подпись цела.
Лимит 10 DNS-запросов: как считать
При проверке одной записи SPF допускается не больше 10 терминов, которые требуют DNS-запроса: include, a, mx, ptr, exists и redirect. Вложенные include считаются тоже. ip4, ip6 и all бесплатны. Превышение — permerror, и SPF не проходит ни у кого. Отдельно RFC рекомендует не больше двух «пустых» запросов (NXDOMAIN или пустой ответ): include на несуществующий домен тоже ломает проверку.
Считать вручную через dig утомительно: у одного include провайдера внутри бывает 5–7 запросов. Функция для bash, которая обходит вложенные записи и считает затраты. Положите в ~/.bashrc или выполните в текущей сессии:
# Подсчёт DNS-запросов в SPF-записи (рекурсивно)
spfcount() {
local rec t n=0
# склеиваем строки длинной TXT-записи и оставляем только v=spf1
rec=$(dig +short TXT "$1" | sed 's/" "//g; s/"//g' | grep -i '^v=spf1')
for t in $rec; do
t=${t#[+~?-]} # убираем квалификатор
case "${t,,}" in
include:*|redirect=*)
n=$((n + 1 + $(spfcount "${t#*[:=]}"))) ;;
a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*)
n=$((n + 1)) ;;
esac
done
echo "$n"
}
spfcount example.com
# 3 — примерно так; всё, что больше 10, даст permerror
Это оценка: макросы и пустые ответы функция не проверяет. Для перепроверки подойдёт онлайн-валидатор SPF с деревом запросов.
Типичные ошибки SPF
- Две записи
v=spf1на одном имени. Подключили сервис рассылок и добавили «его» запись рядом со своей. Результат —permerrorдля обеих. Нужна одна запись, в которую добавленincludeсервиса. +all— разрешение отправлять от имени домена с любого IP в интернете. Такая запись хуже отсутствующей. Встречается в старых инструкциях хостеров «чтобы точно работало».ptr— медленный, ненадёжный, официально не рекомендуется. Часть получателей игнорирует его или считает ошибкой. Замените наip4/a.- Механизмы после
all— игнорируются: всё, что правееall, не проверяется.
DKIM: ключи, подпись и публикация
Селектор и ключ
Получатель берёт из заголовка DKIM-Signature два значения: домен d= и селектор s=. Открытый ключ он ищет в TXT-записи <селектор>._domainkey.<домен>. Селектор — просто имя ключа. Он позволяет держать несколько ключей одновременно: для своего сервера, для CRM, старый и новый при ротации. Имена выбирайте так, чтобы было видно поколение ключа: s1, s2 или 202610.
Ключ — RSA 2048 бит. 1024 бита ещё принимают, но это минимум, а 512 и 768 многие получатели не считают подписью вообще. Ed25519 можно добавить второй подписью, но не вместо RSA.
Подпись ставит что-то одно: rspamd или OpenDKIM, не оба сразу. В сборках вроде Mailcow или iRedMail DKIM уже встроен, ключ генерируется в их админке или утилитах.
Вариант 1: подпись в rspamd
Если rspamd уже стоит как антиспам, подпись удобнее делать им. Версии в репозиториях: Debian 12 — 3.4, Ubuntu 24.04 — 3.8, Debian 13 — 3.12. Разработчики rspamd рекомендуют свой репозиторий со свежей версией. Модуль dkim_signing есть во всех.
- Сгенерируйте ключ. Приватный ключ ляжет в файл, TXT-запись для DNS выведется на экран и сохранится в
example.com.s1.txt:sudo mkdir -p /var/lib/rspamd/dkim
sudo rspamadm dkim_keygen -s s1 -b 2048 -d example.com \
-k /var/lib/rspamd/dkim/example.com.s1.key | sudo tee /var/lib/rspamd/dkim/example.com.s1.txt
sudo chown -R _rspamd:_rspamd /var/lib/rspamd/dkim
sudo chmod 440 /var/lib/rspamd/dkim/*.key - Создайте файл
/etc/rspamd/local.d/dkim_signing.conf. Файлы вlocal.dдополняют стандартные настройки модуля и переживают обновление пакета:# Ключи ищутся по шаблону: /var/lib/rspamd/dkim/example.com.s1.key
enabled = true;
selector = "s1";
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
# Подписывать письма авторизованных пользователей и локальных сетей
sign_authenticated = true;
sign_local = true;
# Подпись от имени example.com и для From вида user@news.example.com
use_esld = true;
# Логины SASL без домена (ivan вместо ivan@example.com):
# раскомментируйте, иначе такие письма не будут подписаны
# allow_username_mismatch = true; - Проверьте конфигурацию и перечитайте её:
sudo rspamadm configtest
sudo systemctl reload rspamd - Подключите rspamd к Postfix, если он ещё не подключён. Прокси-воркер rspamd по умолчанию слушает milter на
localhost:11332:sudo postconf -e 'smtpd_milters = inet:localhost:11332' \
'non_smtpd_milters = $smtpd_milters' \
'milter_default_action = accept'
sudo systemctl reload postfix
milter_default_action = accept: если rspamd упал, почта уходит без подписи, а не отклоняется. Падение надо мониторить.
По умолчанию rspamd сверяет домен в From с доменом envelope-from и логина. При несовпадении письмо не подписывается, причина видна в /var/log/rspamd/rspamd.log. Не отключайте сверку From и envelope-from (allow_hdrfrom_mismatch) без причины: иначе авторизованный пользователь сможет подписать письмо чужим доменом, если ключ для него есть на сервере.
Вариант 2: OpenDKIM
OpenDKIM подходит, если rspamd нет и не нужен, например для сервера, который только отправляет почту сайта. В Debian 12, Debian 13 и Ubuntu 24.04 одна и та же версия — 2.11.0~beta2. Postfix в Debian и Ubuntu работает в chroot /var/spool/postfix, поэтому проще всего соединить его с OpenDKIM по TCP на localhost, а не через Unix-сокет.
- Установите пакеты и сгенерируйте ключ. Каталог
/etc/dkimkeysсоздаёт пакет, права на него — 700 для пользователя opendkim:sudo apt install opendkim opendkim-tools
sudo mkdir -p /etc/dkimkeys/example.com
sudo opendkim-genkey -b 2048 -d example.com -s s1 -D /etc/dkimkeys/example.com
sudo chown -R opendkim:opendkim /etc/dkimkeys/example.com
sudo chmod 600 /etc/dkimkeys/example.com/s1.private
# s1.private — закрытый ключ, s1.txt — готовая запись для DNS - Приведите
/etc/opendkim.confк такому виду. Остальные строки из пакета можно оставить:Syslog yes
SyslogSuccess yes
Canonicalization relaxed/simple
OversignHeaders From
UserID opendkim
UMask 007
# TCP на localhost: не нужно думать о chroot Postfix
Socket inet:8891@localhost
PidFile /run/opendkim/opendkim.pid
# Какой ключ для какого отправителя
KeyTable /etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
# Кого подписывать без авторизации (локальная отправка)
InternalHosts /etc/opendkim/trusted.hosts
TrustAnchorFile /usr/share/dns/root.key - Создайте каталог
/etc/opendkim/(sudo mkdir -p /etc/opendkim) и в нём три таблицы. Файлkey.table: имя ключа → домен:селектор:путь к ключу.s1._domainkey.example.com example.com:s1:/etc/dkimkeys/example.com/s1.private
Файлsigning.table: адрес в From → имя ключа. Префиксrefile:в конфиге разрешает шаблоны со звёздочкой:*@example.com s1._domainkey.example.com
Файлtrusted.hosts. Только локальные адреса: пользователи на 587/465 проходят SASL-авторизацию, и их письма OpenDKIM подписывает по макросу{auth_type}, который передаёт Postfix:127.0.0.1
::1
localhost - Перезапустите OpenDKIM и проверьте, что он слушает порт:
sudo systemctl restart opendkim
sudo ss -lntp | grep 8891
# LISTEN 0 4096 127.0.0.1:8891 ... users:(("opendkim",...)) — примерно так - Подключите milter в Postfix. Если rspamd тоже стоит как антиспам, перечислите оба через запятую, а подпись в rspamd выключите (
enabled = false;вdkim_signing.conf):sudo postconf -e 'smtpd_milters = inet:localhost:8891' \
'non_smtpd_milters = $smtpd_milters' \
'milter_default_action = accept'
sudo systemctl reload postfix
Файл /etc/default/opendkim в Debian 12/13 службой systemd не читается: все настройки — в /etc/opendkim.conf.
Не добавляйте в trusted.hosts внешние сети «чтобы подписывалось»: OpenDKIM будет подписывать всё, что пришло с этих адресов, включая чужие письма через ваш сервер. Ручная настройка Postfix с SASL на 587/465 и запретом релея описана в статье Postfix и Dovecot вручную.
Длинная TXT-запись: разбиение на строки
Открытый ключ 2048 бит в base64 — около 400 символов. Одна строка (character-string) внутри TXT-записи ограничена 255 байтами. Поэтому запись публикуют как одну TXT-запись из нескольких строк в кавычках. Получатель склеивает их без пробелов. Так выглядит файл s1.txt от opendkim-genkey; у rspamadm формат такой же:
s1._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; "
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1x...первая часть...Qx7"
"Zc3...вторая часть...IDAQAB" ) ; ----- DKIM key s1 for example.com
Как публиковать, зависит от DNS-панели:
- Свой BIND или файл зоны — вставьте как есть, со скобками и кавычками.
- Панель, которая сама режет длинные значения — вставьте склеенную строку без кавычек:
v=DKIM1; k=rsa; p=MIIBIjAN...IDAQAB. - Панель, которая требует кавычки — вставьте
"v=DKIM1; k=rsa; p=MIIB...часть1" "часть2"в одно поле.
Ломают подпись две отдельные TXT-записи вместо одной, пробел внутри ключа и хвост ключа, молча обрезанный панелью. После публикации сверяйте ключ из DNS с закрытым (см. «Проверка»).
Ротация ключей
Ключ меняют по расписанию (разумно раз в год, отраслевые рекомендации M3AAWG — чаще) и сразу при подозрении на утечку. Порядок без потери подписи:
- Сгенерируйте ключ с новым селектором
s2той же командой, что и первый. - Опубликуйте
s2._domainkey.example.com. Старыйs1не трогайте. - Убедитесь, что новая запись видна через публичные резолверы, и подождите TTL.
- Переключите подпись на
s2: в rspamd —selector = "s2";и reload; в OpenDKIM — новая строка вkey.table, правкаsigning.tableи restart. - Оставьте
s1в DNS ещё на одну–две недели: письма из очередей и отложенные проверки должны успеть проверить старую подпись. - Отзовите старый ключ: замените запись на
v=DKIM1; p=(пустойp=по RFC 6376 означает отозванный ключ) или удалите её. Закрытый ключ удалите с сервера.
DMARC: политика и отчёты
DMARC публикуется в TXT-записи _dmarc.example.com. В мае 2026 года вышел RFC 9989, который заменил RFC 7489. Основные теги остались, часть удалена: подробности ниже.
| Тег | Значение | Пример |
|---|---|---|
v | Версия, всегда первым | v=DMARC1 |
p | Политика для домена: none, quarantine, reject | p=none |
sp | Политика для поддоменов. Нет тега — действует p | sp=reject |
np | Политика для несуществующих поддоменов (RFC 9989) | np=reject |
adkim, aspf | Выравнивание: r — relaxed (по умолчанию), s — strict | adkim=r |
rua | Куда слать агрегатные XML-отчёты | rua=mailto:dmarc@example.com |
ruf | Куда слать отчёты по отдельным письмам | ruf=mailto:dmarc-f@example.com |
fo | Когда слать ruf: 0 — провалились оба, 1 — провалилось хоть что-то | fo=1 |
pct | Доля писем под политикой, 0–100. Удалён в RFC 9989 | pct=25 |
t | Режим тестирования (RFC 9989): y — применять политику на ступень мягче | t=y |
Поэтапное внедрение: none → quarantine → reject
- Мониторинг, 2–4 недели. Политика ничего не меняет в доставке, но получатели начинают присылать отчёты.
_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com" - Разбор отчётов. Находите все IP, которые шлют от имени домена. Легитимные источники (сайт, CRM, бухгалтерия, сервис рассылок) доводите до DKIM pass с выравниванием. Источники, которых вы не знаете, — кандидаты в подделку.
- Карантин. Когда в отчётах легитимная почта проходит, а остаётся только подделка:
_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com" - Отказ. Ещё через несколько недель без сюрпризов:
_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc@example.com"
Strict-выравнивание (adkim=s, aspf=s) включайте, только если точно знаете, что все отправители подписывают письма ровно вашим доменом. Иначе письма CRM с d=crm.example.com перестанут проходить. Для большинства компаний relaxed — правильный выбор.
pct и новый стандарт
По RFC 7489 запись p=quarantine; pct=25 означала «к 25% непрошедших писем применять карантин». На практике получатели обрабатывали промежуточные значения по-разному. Поэтому RFC 9989 удалил pct и ввёл t=y: политика применяется на ступень мягче, reject работает как quarantine. Неизвестные теги получатели игнорируют, так что старая запись с pct не ломается. Но строить поэтапное внедрение на pct=10…50 сейчас не стоит. Надёжнее ступенчатая смена самой политики: none → quarantine → reject.
Отчёты rua и ruf: как читать
Агрегатные отчёты (rua) приходят раз в сутки от каждого крупного получателя: XML в архиве .zip или .gz. В отчёте — сколько писем от имени вашего домена пришло с каждого IP и прошли ли они DKIM и SPF с выравниванием. Отчёты по отдельным письмам (ruf) многие крупные получатели не присылают вовсе: в них персональные данные. Рассчитывать на ruf не стоит.
Заведите для отчётов отдельный ящик. Если адрес rua в чужом домене, например у сервиса-анализатора, этот домен должен подтвердить приём записью example.com._report._dmarc.analyzer.example с содержимым v=DMARC1. Без неё получатели отчёты не отправят. Сервисы обычно публикуют такую запись сами.
Фрагмент отчёта. Главное — блок policy_evaluated: это уже результат с выравниванием:
<record>
<row>
<source_ip>85.119.148.5</source_ip>
<count>3</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers><header_from>example.com</header_from></identifiers>
</record>
Три письма с 85.119.148.5 не прошли ни DKIM, ни SPF. Если это ваш сайт или CRM — источник надо настроить. Если IP вам неизвестен — это подделка, и именно от неё защитит p=reject.
Для небольшого домена хватит скрипта, который превращает пачку отчётов в таблицу. Сохраните как /usr/local/bin/dmarc-summary.py, запускайте на каталоге с вложениями: python3 /usr/local/bin/dmarc-summary.py reports/*.
#!/usr/bin/env python3
# Сводка по агрегатным отчётам DMARC: файлы .xml, .xml.gz, .zip
import sys, gzip, zipfile
import xml.etree.ElementTree as ET
def load(path):
if path.endswith('.gz'):
return gzip.open(path).read()
if path.endswith('.zip'):
with zipfile.ZipFile(path) as z:
return z.read(z.namelist()[0])
return open(path, 'rb').read()
for path in sys.argv[1:]:
root = ET.fromstring(load(path))
for el in root.iter(): # убираем XML-namespace, если он есть
el.tag = el.tag.split('}')[-1]
org = root.findtext('report_metadata/org_name')
for rec in root.iter('record'):
row = rec.find('row')
pe = row.find('policy_evaluated')
print(org, row.findtext('source_ip'), row.findtext('count'),
pe.findtext('disposition'),
'dkim=' + (pe.findtext('dkim') or '-'),
'spf=' + (pe.findtext('spf') or '-'),
rec.findtext('identifiers/header_from'), sep='\t')
Для нескольких доменов удобнее parsedmarc — свободный парсер на Python: забирает отчёты по IMAP и строит дашборды в Elasticsearch или OpenSearch. У онлайн-агрегаторов с бесплатными тарифами проверьте политику хранения данных: в отчётах IP ваших серверов и контрагентов.
Проверка: DNS, подпись, заголовки
Проверка DNS-записей. Выполняйте с внешней машины или через публичный резолвер, чтобы не смотреть на кэш своего сервера:
# SPF: ровно одна строка с v=spf1
dig +short TXT example.com @1.1.1.1
# DKIM: одна запись, может состоять из нескольких строк в кавычках
dig +short TXT s1._domainkey.example.com @1.1.1.1
# DMARC
dig +short TXT _dmarc.example.com @1.1.1.1
Проверка, что ключ в DNS совпадает с закрытым ключом на сервере. Утилита из пакета opendkim-tools подходит и для ключей rspamd:
sudo opendkim-testkey -d example.com -s s1 -k /etc/dkimkeys/example.com/s1.private -vvv
# ...
# opendkim-testkey: key not secure
# opendkim-testkey: key OK
key OK — ключ найден и совпадает. key not secure означает лишь, что зона не подписана DNSSEC; на работу DKIM это не влияет. Ошибка record not found — запись не опубликована или опечатка в селекторе. Несовпадение ключей — в DNS старый или обрезанный ключ.
Если opendkim-tools ставить не хочется, сравните открытый ключ вручную. Подставьте путь к своему ключу:
# Открытый ключ из закрытого, одной строкой
sudo openssl rsa -in /var/lib/rspamd/dkim/example.com.s1.key -pubout -outform DER 2>/dev/null | base64 -w0; echo
# Ключ из DNS: значение после p=
dig +short TXT s1._domainkey.example.com | sed 's/" "//g; s/"//g; s/.*p=//'
Подписывает ли сервер письма, видно в логах. Для OpenDKIM в Debian 12/13 без rsyslog — journalctl:
sudo journalctl -u opendkim --since "10 min ago"
# opendkim[1234]: 4XyZ1abc: DKIM-Signature field added (s=s1, d=example.com) — примерно так
sudo grep -i dkim /var/log/rspamd/rspamd.log | tail
Финальная проверка — живое письмо. Отправьте его с ящика на сервере через клиент или swaks на адрес в Gmail или Mail.ru. Откройте исходник («Показать оригинал» в Gmail) и найдите заголовок Authentication-Results, который добавил сервер получателя:
Authentication-Results: mx.google.com;
dkim=pass header.i=@example.com header.s=s1 header.b=AbCd1234;
spf=pass (google.com: domain of info@example.com designates 85.119.149.3 as permitted sender) smtp.mailfrom=info@example.com;
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=example.com
Все три должны быть pass, а header.s — вашим текущим селектором. Затем отправьте письмо на адрес, который выдаст mail-tester.com: сервис проверит PTR, SPF, DKIM, DMARC и чёрные списки в одном отчёте. Если записи в порядке, а письма всё равно в спаме, причина в репутации или содержимом. Разбор таких случаев — в статье почему письма попадают в спам: диагностика.
Поддомены и сторонние отправители
Типичная картина: сервер компании, CRM шлёт счета, сервис рассылок — новости, сайт — уведомления. Каждый источник должен пройти DMARC, а SPF не должен превысить 10 запросов.
Правила
- SPF на поддомены не наследуется. Для
news.example.comнужна своя запись. Запись example.com на него не действует. - DMARC наследуется. Если у поддомена нет своей записи
_dmarc, действуетsp(илиp) основного домена. - Для DMARC стороннему сервису важнее DKIM, чем SPF. Envelope-from у сервиса рассылок часто свой (
bounce@mailer.provider.example), и SPF в любом случае не будет выровнен с вашим From. А DKIM сd=example.comилиd=news.example.comвыровнен в режиме relaxed.
Как подключить сервис
- Выделите сервису поддомен:
news.example.comдля рассылок,crm.example.comдля CRM. У каждого поддомена свой бюджет в 10 DNS-запросов. Репутация рассылок не смешивается с деловой перепиской. - В настройках сервиса укажите «свой домен отправки». Сервис выдаст DKIM-запись: TXT с ключом или CNAME на свой ключ, например
crm1._domainkey.crm.example.com CNAME crm1.dkim.provider.example. CNAME удобнее: ротацию ключей сервис делает сам. - Если сервис поддерживает собственный bounce-домен (Return-Path), настройте и его: SPF тогда выровнен тоже.
- Добавьте
includeсервиса в SPF поддомена, а не основного домена:example.com. TXT "v=spf1 ip4:85.119.149.3 -all"
news.example.com. TXT "v=spf1 include:_spf.mailer-provider.example ~all"
crm.example.com. TXT "v=spf1 include:spf.crm-provider.example ~all"
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com" - Посчитайте запросы функцией
spfcountдля каждого имени и отправьте тестовое письмо через сервис. В Authentication-Results должен бытьdkim=passсheader.dвашего домена или поддомена.
Если сервис обязан слать именно от @example.com, посчитайте бюджет SPF до публикации. «SPF flattening» (замена include списком ip4) помогает, но список устаревает, когда провайдер меняет адреса.
Для поддоменов, которые не отправляют почту (www, shop), публикуйте v=spf1 -all. Несуществующие поддомены закрывает тег np=reject, но его поддержка получателями появится не сразу.
Ошибка → симптом → решение
| Ошибка | Симптом | Решение |
|---|---|---|
| Нет PTR или шаблонный PTR хостера | Отказы и спам при pass по SPF и DKIM; в отказах упоминается reverse DNS | Задать PTR у хостера IP на mail.example.com, совпадающий с A-записью и myhostname |
Две записи v=spf1 | spf=permerror в Authentication-Results | Слить в одну запись с нужными include |
| Больше 10 DNS-запросов в SPF | spf=permerror, началось после подключения нового сервиса | spfcount, вынести сервисы на поддомены, заменить a/mx на ip4 |
+all в SPF | Спам от имени домена проходит SPF, домен теряет репутацию | Заменить на ~all или -all |
| Нет IPv6 в SPF, сервер шлёт по IPv6 | spf=fail только у части получателей (Gmail) | Добавить ip6: и PTR для IPv6 или inet_protocols = ipv4 |
| Обрезанная или разбитая на две записи DKIM-запись | dkim=fail или dkim=permerror, opendkim-testkey ругается на ключ | Одна TXT-запись из нескольких строк, сверить ключ из DNS с закрытым |
| Подпись не ставится | Нет заголовка DKIM-Signature в отправленном письме | Проверить smtpd_milters, лог milter, права на ключ, signing.table или allow_username_mismatch |
| Подписывают и rspamd, и OpenDKIM | Две подписи DKIM-Signature, одна из них fail | Оставить один механизм подписи |
| CRM шлёт от вашего домена без своего DKIM | В DMARC-отчётах IP CRM с dkim=fail | Настроить в CRM свой домен отправки и DKIM-запись |
p=reject сразу | Пропадают письма забытого источника, отправитель о них не знает | Вернуть p=none, разобрать отчёты, ужесточать по шагам |
| Адрес rua в чужом домене без подтверждения | Отчёты не приходят | Запись example.com._report._dmarc в домене получателя отчётов |
Если не хотите сопровождать почту сами
SPF, DKIM и DMARC настраиваются один раз, но дальше их нужно поддерживать: ротация ключей, разбор DMARC-отчётов, новые сервисы рассылок, смена IP. Если в компании нет человека, который будет этим заниматься, есть готовый корпоративный почтовый сервер с администрированием: сервер выделен под компанию, DNS и доставляемость берёт на себя провайдер.
Частые вопросы
Нужен ли DKIM, если SPF уже проходит?
Да. SPF ломается при любой пересылке: пересылающий сервер не входит в ваш список IP. Кроме того, SPF проверяет envelope-from, а не адрес в From, поэтому для DMARC он часто не выровнен. DKIM переживает пересылку и привязан к вашему домену. Gmail, Yahoo и Microsoft для массовых отправителей требуют оба механизма плюс DMARC.
Сколько ждать, пока DNS-записи начнут работать?
Новая запись видна через минуты, изменённая — после истечения TTL старой в кэшах резолверов. Перед заменой SPF или DKIM полезно заранее снизить TTL до 300–600 секунд, а после стабилизации вернуть 3600.
Почему dmarc=fail, хотя spf=pass и dkim=pass?
Нет выравнивания. SPF прошёл для домена из envelope-from, DKIM — для домена из d=, но ни один из них не совпадает с доменом в From. Типичный случай — сторонний сервис, который подписывает письма своим доменом. Посмотрите в Authentication-Results значения smtp.mailfrom, header.d и header.from: хотя бы одно из первых двух должно быть вашим доменом или его поддоменом.
Нужна ли DMARC-запись для домена, который не отправляет почту?
Да: v=spf1 -all и v=DMARC1; p=reject защитят его от подделки. Такие домены спамеры любят, потому что владельцы не ждут писем от их имени.
Что делать с DMARC-отчётами, если их слишком много?
Направьте их в отдельный ящик и разбирайте автоматически: скриптом из статьи или parsedmarc. Смотрите на новые IP с dkim=fail и spf=fail: свои — настраивайте, чужие — попытки подделки, которые отсечёт p=reject.