Главная » Почтовые серверы » SPF, DKIM и DMARC: настройка для своего домена и почтового сервера

SPF, DKIM и DMARC: настройка для своего домена и почтового сервера

Введение в термины:

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

  1. PTR для 203.0.113.10 возвращает mail.example.com.
  2. A-запись mail.example.com возвращает 85.119.149.3.
  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Адрес или сеть IPv40
ip6:2001:db8::/64Адрес или сеть IPv60
a, a:mail.example.comIP из A/AAAA-записи домена или указанного имени1
mxIP всех 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 есть во всех.

  1. Сгенерируйте ключ. Приватный ключ ляжет в файл, 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
  2. Создайте файл /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;
  3. Проверьте конфигурацию и перечитайте её:
    sudo rspamadm configtest
    sudo systemctl reload rspamd
  4. Подключите 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-сокет.

  1. Установите пакеты и сгенерируйте ключ. Каталог /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
  2. Приведите /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
  3. Создайте каталог /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
  4. Перезапустите OpenDKIM и проверьте, что он слушает порт:
    sudo systemctl restart opendkim
    sudo ss -lntp | grep 8891
    # LISTEN 0 4096 127.0.0.1:8891 ... users:(("opendkim",...)) — примерно так
  5. Подключите 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 — чаще) и сразу при подозрении на утечку. Порядок без потери подписи:

  1. Сгенерируйте ключ с новым селектором s2 той же командой, что и первый.
  2. Опубликуйте s2._domainkey.example.com. Старый s1 не трогайте.
  3. Убедитесь, что новая запись видна через публичные резолверы, и подождите TTL.
  4. Переключите подпись на s2: в rspamd — selector = "s2"; и reload; в OpenDKIM — новая строка в key.table, правка signing.table и restart.
  5. Оставьте s1 в DNS ещё на одну–две недели: письма из очередей и отложенные проверки должны успеть проверить старую подпись.
  6. Отзовите старый ключ: замените запись на v=DKIM1; p= (пустой p= по RFC 6376 означает отозванный ключ) или удалите её. Закрытый ключ удалите с сервера.

DMARC: политика и отчёты

DMARC публикуется в TXT-записи _dmarc.example.com. В мае 2026 года вышел RFC 9989, который заменил RFC 7489. Основные теги остались, часть удалена: подробности ниже.

ТегЗначениеПример
vВерсия, всегда первымv=DMARC1
pПолитика для домена: none, quarantine, rejectp=none
spПолитика для поддоменов. Нет тега — действует psp=reject
npПолитика для несуществующих поддоменов (RFC 9989)np=reject
adkim, aspfВыравнивание: r — relaxed (по умолчанию), s — strictadkim=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 9989pct=25
tРежим тестирования (RFC 9989): y — применять политику на ступень мягчеt=y

Поэтапное внедрение: none → quarantine → reject

  1. Мониторинг, 2–4 недели. Политика ничего не меняет в доставке, но получатели начинают присылать отчёты.
    _dmarc.example.com.  3600  IN  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
  2. Разбор отчётов. Находите все IP, которые шлют от имени домена. Легитимные источники (сайт, CRM, бухгалтерия, сервис рассылок) доводите до DKIM pass с выравниванием. Источники, которых вы не знаете, — кандидаты в подделку.
  3. Карантин. Когда в отчётах легитимная почта проходит, а остаётся только подделка:
    _dmarc.example.com.  3600  IN  TXT  "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
  4. Отказ. Ещё через несколько недель без сюрпризов:
    _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.

Как подключить сервис

  1. Выделите сервису поддомен: news.example.com для рассылок, crm.example.com для CRM. У каждого поддомена свой бюджет в 10 DNS-запросов. Репутация рассылок не смешивается с деловой перепиской.
  2. В настройках сервиса укажите «свой домен отправки». Сервис выдаст DKIM-запись: TXT с ключом или CNAME на свой ключ, например crm1._domainkey.crm.example.com CNAME crm1.dkim.provider.example. CNAME удобнее: ротацию ключей сервис делает сам.
  3. Если сервис поддерживает собственный bounce-домен (Return-Path), настройте и его: SPF тогда выровнен тоже.
  4. Добавьте 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"
  5. Посчитайте запросы функцией 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=spf1spf=permerror в Authentication-ResultsСлить в одну запись с нужными include
Больше 10 DNS-запросов в SPFspf=permerror, началось после подключения нового сервисаspfcount, вынести сервисы на поддомены, заменить a/mx на ip4
+all в SPFСпам от имени домена проходит SPF, домен теряет репутациюЗаменить на ~all или -all
Нет IPv6 в SPF, сервер шлёт по IPv6spf=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.