Автор пишет с корпоративного домена под DMARC p=reject, отправляет в рассылку на Mailman, и половина подписчиков это письмо не получает. В логе приёмника строка вида:
Authentication-Results: mx.google.com;
dkim=fail (body hash did not verify) header.d=corp.com;
spf=pass (google.com: domain of [email protected] designates 203.0.113.9 as permitted sender) [email protected];
dmarc=fail (p=REJECT sp=REJECT dis=REJECT) header.from=corp.comДва независимых слома, один симптом. DKIM провалился по body hash — список поменял тело. SPF формально pass, но по домену списка, а не по corp.com из From: — значит не выровнен. DMARC требует хотя бы одну выровненную и валидную проверку, обеих нет, политика p=reject срабатывает, живые люди перестают получать почту. ARC (RFC 8617) не чинит ни SPF, ни DKIM. Он делает другое: позволяет узлу, который эти проверки сломал, криптографически засвидетельствовать «когда письмо пришло ко мне, оно проходило DMARC», а конечный приёмник по своей политике доверия может это заверение принять и не применять reject.
Что именно ломается при пересылке
SPF проверяет MAIL FROM (конверт, он же Return-Path) против IP отправляющего MTA. При пересылке письмо уходит уже с IP форвардера, а Return-Path список переписывает на свой bounce-адрес ([email protected]), чтобы отлупы шли ему, а не автору. В результате SPF может быть технически pass — но по домену списка. DMARC смотрит на выравнивание (alignment): домен, проверенный SPF, должен совпадать с доменом в заголовке From:. list.example.org не равно corp.com, значит для DMARC этот pass бесполезен.
DKIM подписывает тело (тег bh=, хеш body) и фиксированный набор заголовков (h=). Список делает ровно то, что ломает body hash: добавляет к Subject префикс [list], вклеивает MIME-часть с футером «отписаться тут», иногда пере-кодирует Content-Transfer-Encoding из 7bit в quoted-printable. Любое из этих изменений сдвигает body hash, bh= не сходится, подпись fail.
Итог арифметический: forwarding через список валит как минимум одну из двух проверок, обычно обе. DMARC устроен так, что ему хватило бы одной выровненной и валидной — но её нет. Отсюда reject. Это не баг конкретного списка, это следствие того, что SPF и DKIM проектировались до эпохи модифицирующих ретрансляторов.
Как устроен ARC: три заголовка и цепочка инстансов
ARC не добавляет третий метод аутентификации. Он добавляет историю. Каждый ARC-совместимый хоп дописывает свой «инстанс», пронумерованный тегом i= (i=1, i=2, …). Инстанс — это набор из трёх заголовков (ARC-Set):
- ARC-Authentication-Results (AAR) — снимок результатов SPF/DKIM/DMARC, какими их увидел этот хоп в момент приёма, до своих модификаций. По сути замороженная строка
Authentication-Results. - ARC-Message-Signature (AMS) — DKIM-подобная подпись всего сообщения (тело + заголовки) на момент прохождения. Важно: AMS исключает ARC-Seal.
- ARC-Seal (AS) — подпись только над ARC-заголовками: всеми предыдущими инстансами плюс AAR и AMS текущего. Именно AS сцепляет цепочку.
Ключевой тег в ARC-Seal — cv= (chain validation): cv=none у самого первого хопа (входящей цепочки ещё нет), cv=pass если входящая цепочка валидна, cv=fail если сломана. Инстанс с cv=fail закрывает цепочку — дальше запечатывать бессмысленно, доверие уже утеряно.
Зачем две разные подписи? AMS фиксирует контент — что письмо выглядело так-то. AS защищает саму историю заверений от подмены: нельзя вырезать средний инстанс или переписать чужой AAR, потому что AS следующего хопа подписал их целиком. Это и есть смысл слова «chain».
Читаем настоящую ARC-цепочку
Вот как выглядят заголовки письма, прошедшего Google → список → приёмник. Порядок обратный: свежий инстанс (i=2) сверху.
ARC-Seal: i=2; a=rsa-sha256; t=1720000200; cv=pass;
d=list.example.org; s=arc2026;
b=Q7f...
ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed;
d=list.example.org; s=arc2026; t=1720000200;
h=from:subject:date:to:mime-version; bh=Zt9...; b=Rk2...
ARC-Authentication-Results: i=2; list.example.org;
dkim=pass header.d=corp.com;
spf=pass [email protected];
dmarc=pass (p=REJECT) header.from=corp.com
ARC-Seal: i=1; a=rsa-sha256; t=1720000100; cv=none;
d=google.com; s=arc-20230601; b=A1c...
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed;
d=google.com; s=arc-20230601; bh=8xN...; b=Lp0...
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.d=corp.com; spf=pass [email protected];
dmarc=pass (p=REJECT) header.from=corp.comЧитаем при разборе инцидента так. Сначала — верхний ARC-Seal и его cv=: здесь i=2 cv=pass, значит входящая цепочка на входе в список была цела. Спускаемся к i=1 cv=none — это точка, где Google начал цепочку; в его AAR зафиксировано dmarc=pass, снятое с ещё нетронутого письма. В AAR инстанса i=2 та же картина: список проверил аутентификацию на входе, до своих правок, и тоже увидел валидный DKIM и dmarc=pass. Обратите внимание на bh=: у i=1 он снят с оригинального тела, у i=2 — уже другой, потому что AMS списка подписывает исходящее, модифицированное тело. Именно зафиксированный на входе dmarc=pass и переживает поломку подписи на выходе — ради него вся конструкция и существует.
Валидация и запечатывание: OpenARC и rspamd
Два рабочих варианта под Postfix.
Вариант A — OpenARC (Trusted Domain Project) как отдельный milter. Конфиг /etc/openarc.conf:
Domain list.example.org
Selector arc2026
KeyFile /etc/openarc/keys/arc2026.private
Mode sv
Socket inet:8894@localhost
PeerList 127.0.0.1
AuthservID list.example.orgMode sv значит sign + verify: узел и проверяет входящую цепочку, и запечатывает новый инстанс. Подключение в main.cf:
smtpd_milters = inet:localhost:8891, inet:localhost:8894
non_smtpd_milters = inet:localhost:8891, inet:localhost:8894Порядок важен: DKIM-верификация (8891, OpenDKIM) должна отработать раньше ARC-sealing (8894), иначе AAR запишет пустой результат.
Вариант B — rspamd, модуль arc. local.d/arc.conf:
sign_networks = "127.0.0.1, 203.0.113.0/24";
use_domain = "header";
selector = "arc2026";
path = "/var/lib/rspamd/dkim/$domain.$selector.arc.key";
allow_hdrfrom_mismatch = true;allow_hdrfrom_mismatch = true нужен именно для списков: домен в From: (автора) не совпадает с доменом запечатывающего (списка), и без этого флага rspamd откажется подписывать. Для стека evilmail мы рекомендуем rspamd: он уже стоит в тракте, переиспользует те же DKIM-ключи и логику sign_networks, а входящую валидацию цепочки делает автоматически. Проверка конфигурации:
rspamadm configdump arcВ Authentication-Results, которые rspamd дописывает, вы увидите arc=pass либо arc=fail с причиной — это первое, что нужно грепать при отладке.
DNS и ключи: ARC переиспользует DKIM
Частая путаница: ARC не заводит отдельный тип DNS-записи. AMS и AS ссылаются на теги d= и s=, которые резолвятся ровно как DKIM — обычный TXT в _domainkey:
arc2026._domainkey.list.example.org. IN TXT (
"v=DKIM1; k=rsa; "
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArn... "
"...QIDAQAB" )Для 2048-битного RSA публичная часть не влезает в один TXT-string (лимит 255 символов), поэтому запись бьётся на несколько строк в скобках — резолвер склеит их обратно. Проверка резолва:
dig +short TXT arc2026._domainkey.list.example.orgПрактический совет: заводите отдельный селектор под ARC (arc2026, а не общий DKIM-селектор), чтобы ротировать ключи независимо и чтобы компрометация или ротация DKIM не задевала ARC-цепочки, которые уже ушли в мир.
ARC не отменяет DMARC — решает приёмник
Это раздел, отделяющий понимание от карго-культа. Валидная цепочка cv=pass не делает DMARC pass автоматически. Механика такая: приёмник берёт из i=1 AAR исходный результат (dmarc=pass), смотрит, кто запечатал цепочку (d= в ARC-Seal), и спрашивает себя — доверяю ли я этому домену? Только если да, приёмник по своей локальной политике может переопределить провал DMARC и не применять p=reject. Нет доверия к запечатавшему — ARC просто игнорируется, и письмо режется как обычно.
Отсюда следствие, которое надо принять честно: ARC работает только между сторонами с устоявшейся репутацией. Google, Microsoft 365, крупные списки — их ARC-заверения приёмники учитывают, потому что накоплена история. Свежий домен, который начал запечатывать ARC на прошлой неделе, никто переопределять не станет — его d= ничего не значит для Gmail. ARC не создаёт доверие, он лишь переносит уже существующее через хоп. Если у вашего списка нет репутации, ARC вам не поможет, и это не недостаток протокола, а его дизайн.
Настройка списка рассылки, чтобы не ломать почту
Для оператора списка на Mailman 3 есть два рычага. dmarc_moderation_action со значениями Munge From или Wrap Message — это обходной путь для приёмников, которые ARC не поддерживают. Munge From переписывает From: на адрес списка (author via list <list@...>), из-за чего DMARC проходит по домену списка — но ценой identity автора: Reply-To ломается, «ответить автору» больше не работает интуитивно.
ARC-sealing — путь для приёмников, которые ARC поддерживают: он сохраняет оригинальный From: и Reply-To. Правильная последовательность для списка:
- Валидировать входящий DMARC до любых модификаций сообщения.
- Запечатать ARC с
cv=, отражающим реальный результат входящей цепочки. - Минимизировать модификации: не трогать Subject и тело, если это не критично для работы списка.
На практике многие крупные списки комбинируют оба: запечатывают ARC И делают Munge From как fallback для тех, кто ARC не учитывает.
Чеклист внедрения и отладка
- Выделить отдельный ARC-селектор (
arc2026) и опубликоватьTXTв_domainkey; проверитьdig +short TXT arc2026._domainkey.<домен>. - Включить sealing в rspamd (
sign_networks) или OpenARC (PeerList) — только для доверенных сетей, иначе вы запечатаете чужой спам. - Убедиться, что валидация входящей цепочки включена до подписи, иначе
cv=будет врать. - Проверить порядок milter'ов: DKIM-верификация → потом ARC-sealing. В Postfix это порядок в
smtpd_milters. - Отправить тест через список на Gmail, открыть «Показать оригинал», найти
arc=pass (i=2 ...)рядом сdmarc=fail.


