ARC для пересылки и списков рассылки: как сохранить аутентификацию, когда SPF и DKIM ломаются
Легитимное письмо уходит в Mailman, список переписывает Subject и клеит футер — DKIM рассыпается, SPF не выравнивается, DMARC у подписчиков даёт reject. ARC (RFC 8617) не чинит SPF и DKIM: он передаёт результат аутентификации через хоп, который их сломал. Разбираем цепочку заверений по заголовкам, настраиваем sealing в rspamd и OpenARC и честно говорим про пределы доверия.
EvilMail Team18 июля 2026 г.14 мин чтения
Автор пишет с корпоративного домена под DMARC p=reject, отправляет в рассылку на Mailman, и половина подписчиков это письмо не получает. В логе приёмника строка вида:
text
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 и его 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:
allow_hdrfrom_mismatch = true нужен именно для списков: домен в From: (автора) не совпадает с доменом запечатывающего (списка), и без этого флага rspamd откажется подписывать. Для стека evilmail мы рекомендуем rspamd: он уже стоит в тракте, переиспользует те же DKIM-ключи и логику sign_networks, а входящую валидацию цепочки делает автоматически. Проверка конфигурации:
bash
rspamadm configdump arc
В Authentication-Results, которые rspamd дописывает, вы увидите arc=pass либо arc=fail с причиной — это первое, что нужно грепать при отладке.
DNS и ключи: ARC переиспользует DKIM
Частая путаница: ARC не заводит отдельный тип DNS-записи. AMS и AS ссылаются на теги d= и s=, которые резолвятся ровно как DKIM — обычный TXT в _domainkey:
Для 2048-битного RSA публичная часть не влезает в один TXT-string (лимит 255 символов), поэтому запись бьётся на несколько строк в скобках — резолвер склеит их обратно. Проверка резолва:
Практический совет: заводите отдельный селектор под 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.
Грепать логи на arc=fail и разбирать причину: chain-error, body-hash-mismatch, bad-seal.
Типовые грабли: двойное запечатывание (два milter'а подписывают один инстанс); подпись AMS после того, как список уже поменял тело, вместо снимка на входе; отсутствие sign_networks/PeerList — узел молча ничего не подписывает; обрезанный split-TXT для 2048-бит ключа — резолвер отдаёт битый p=; неверный порядок milter'ов, когда ARC подписывает раньше, чем DKIM успел проверить.