ARC для пересланої пошти: як зберегти SPF/DKIM при проходженні через списки розсилки
Список розсилки необоротно ламає і DKIM, і SPF — і жодне налаштування у відправника це не лікує. Розбираємо, як ARC (RFC 8617) засвідчує авторизацію на вході в посередника, і піднімаємо підписання на rspamd та OpenARC.
EvilMail Team14 липня 2026 р.13 хв читання
Користувач [email protected] пише в розсилку [email protected]. У домені origin.com стоїть p=reject, DKIM налаштований бездоганно, SPF зелений. Лист відправляється, проходить через список — і падає в Gmail з таким вироком:
text
Authentication-Results: mx.google.com;
dkim=fail reason="body hash did not verify" header.d=origin.com;
spf=pass [email protected];
dmarc=fail (p=REJECT) header.from=origin.com
dmarc=fail при p=reject означає спам-теку в кращому випадку і
ARC-підписи для пересланої пошти: врятувати DMARC на списках розсилки — EvilMail Blog
550 5.7.1
bounce — у гіршому. Головне тут — не панікувати й не кинутися правити DKIM-селектор: відправник усе зробив правильно. Лист зіпсував
посередник
. Список розсилки фізично переписав повідомлення, і жоден рядок у зоні
origin.com
цього не полагодить. Саме цю проблему й закриває ARC — не «чинячи» підпис, а засвідчуючи, що на вході в посередника автентифікація ще була валідною.
Що саме ламається, коли лист проходить через список
Тут три незалежні поломки, і важливо не зливати їх в одну.
DKIM. Mailman чи rspamd на боці списку дописує в тіло футер To unsubscribe… і майже завжди переписує тему в [list] оригінальна тема. DKIM-підпис покриває тіло через bh= (body hash) і заголовок Subject через h=. Змінилося тіло — не збігається bh. Змінився Subject — не збігається хеш заголовків. Результат: dkim=fail reason="body hash did not verify". Це не помилка конфігурації, це математика: підписаний був інший лист.
SPF. Список пересилає повідомлення зі своїх власних MX і переписує envelope MAIL FROM на [email protected] (класичний VERP для обробки bounce'ів). SPF тепер перевіряє домен lists.example, і той чесно pass — але це SPF чужого домену. Alignment із доменом у полі From: (origin.com) зникає.
DMARC. Політика DMARC вимагає, щоб пройшов хоча б один механізм — SPF або DKIM — і щоб він був у alignment із доменом у видимому From:. Після проходження через список DKIM у fail, а SPF хоч і pass, але aligned на lists.example, а не на origin.com. Обидва шляхи до dmarc=pass перекриті одночасно.
Підсумок жорсткий: p=reject, який ви поставили, щоб захистити свій бренд від фішингу, вбиває вашого ж легітимного користувача, щойно той напише в будь-яку розсилку. Це не баг DMARC — це його дизайн. І саме тому знадобився окремий механізм для проміжних вузлів.
Анатомія ARC-ланцюга: три заголовки на кожному хопі
ARC (Authenticated Received Chain, RFC 8617, статус Experimental з 2019 року) додає на кожному проміжному вузлі рівно три поля, згруповані номером інстансу i=N (від 1 до 50):
ARC-Authentication-Results (AAR) — знімок результатів автентифікації, які цей вузол побачив на вході. По суті копія Authentication-Results, зафіксована в момент прийому.
ARC-Message-Signature (AMS) — підпис, влаштований як DKIM: покриває заголовки й тіло в тому вигляді, у якому вузол передає лист далі. Ключове слово — «далі»: AMS підписує вже змінену версію з футером.
ARC-Seal (AS) — печатка. Вона підписує всі ARC-заголовки попередніх інстансів плюс AAR і AMS поточного, і несе тег cv= (chain validation): none, pass або fail.
Логіка тут така. AMS дозволяє тілу мінятися на наступних хопах, бо кожен вузол підписує свою власну версію листа — не претендуючи на незмінність оригіналу. А AS зшиває ланцюг так, що ланки не можна ні вирізати, ні переставити місцями: кожна печатка криптографічно охоплює все, що лежить під нею.
Читаємо справжній ARC-набір рядок за рядком
Ось як виглядає повний набір для першого хопу (i=1). Оскільки до списку ланцюга ще не існувало, печатка несе cv=none — «я перша ланка»:
Зверніть увагу на AAR: він фіксує dkim=pass header.d=origin.com та dmarc=pass — стан до того, як список додав футер. Це і є свідчення, яке ми несемо далі. Ключові теги решти полів:
d= — домен-запечатувач (sealer). На ньому тримається вся довіра.
s= — селектор, за яким шукається публічний ключ у DNS.
a= — алгоритм: rsa-sha256 (ключ рекомендовано 2048-bit) або ed25519-sha256 у новіших реалізаціях.
c=relaxed/relaxed — канонікалізація заголовків/тіла. Практично завжди relaxed/relaxed.
bh=, h=, t= — body hash, список підписаних заголовків, timestamp.
Семантика cv= проста, але критична: none = я перша ланка ланцюга; pass = усі попередні ланки валідні; fail = ланцюг уже зламаний, і жодної віри йому далі нема. На другому хопі (наприклад, ще один релей) буде i=2; cv=pass.
Валідація: як одержувач ухвалює рішення
Алгоритм на приймальному боці:
1.Перевірити AMS найновішого інстансу (i=N) як звичайний DKIM-підпис — сходиться bh, валідний b.
2.Перевірити всі ARC-Seal від i=1 до i=N за їхніми публічними ключами.
3.Якщо будь-який AS невалідний або десь у ланцюзі стоїть cv=fail — увесь ланцюг вважається fail.
А тепер найважливіше й найчастіше нерозуміле. Навіть при arc=pass одержувач не зобов'язаний пропустити лист. ARC дає лише підставу зробити local policy override DMARC — і тільки якщо одержувач довіряє домену запечатувача (d= в ARC-Seal). Gmail і Microsoft вирішують за репутацією sealer-домену, накопиченою роками. Ваш власний Postfix жодної такої репутації не має і мусить явно вирішити, кому вірити. У фіналі успішний лист виглядає так:
Читається це як: «Я, Gmail, довіряю lists.example; він засвідчив, що на його вході SPF/DKIM/DMARC для origin.com були pass; тому я не застосовую p=reject».
Підписуємо ARC на власному релеї через rspamd
Якщо ви і є той посередник, що пересилає й модифікує листи (список розсилки, форвардер, шлюз) — ось практична частина. ARC-ключ живе в тому самому DNS-namespace _domainkey, що й DKIM; окремого простору імен для ARC не існує. Генеруємо ключ:
Два параметри тут неочевидні й вирішальні. use_domain = "envelope" змушує rspamd брати домен зі зміненого MAIL FROM ([email protected]), тому $domain розкриється в lists.example і підтягне саме наш ключ — а не в origin.com із поля From:, до якого ключа в нас нема. А allow_hdrfrom_mismatch = true дозволяє підписати лист попри те, що From: ([email protected]) не збігається з доменом підпису — у розсилці вони й повинні різнитися. Головне правило: ARC підписуємо на виході релея, після того як усі модифікації тіла вже застосовані (футер, тег у Subject). Verify натомість робимо на вході. Якщо підписати AMS до додавання футера, наступний хоп отримає cv=fail.
Альтернатива: OpenARC як milter до Postfix
Для чистого Postfix без rspamd є OpenARC. Конфіг /etc/openarc/openarc.conf:
Спершу :8891 (OpenDKIM підписує від імені origin.com, якщо ви ще й origin), потім :8894 (OpenARC запечатує вже готовий лист). Якщо переставити місцями, ARC запечатає версію без DKIM-підпису — і ланцюг стане безглуздим. Врахуйте: OpenARC досі має ярлик experimental і вимагає уваги до того, щоб жоден мілтер не чіпав лист після того, як OpenARC поставив AMS.
Коли ланцюг рветься: діагностика cv=fail
Найпоширеніші причини cv=fail на наступному хопі:
Інший мілтер (антивірус, content-filter) модифікує лист після підписання AMS. Найчастіша причина.
Якщо dig не повертає ключ — публічний запис не з'явився або має помилку, і будь-яка валідація впаде ще до перевірки підпису.
Чеклист перед вмиканням у прод
Ви взагалі маєте підписувати? ARC ставить тільки той, хто пересилає й модифікує листи (список, форвардер). Якщо ви лише кінцевий відправник — вам потрібен виключно verify, підписувати нічого.
Ключ ≥ 2048-bit RSA (за бажанням ed25519 паралельно), окремий селектор для ARC. Не переливайте свій DKIM-ключ.
ARC-підпис ставиться на виході, після всіх модифікацій тіла — інакше cv=fail на наступному хопі.
Порядок мілтерів: DKIM → ARC. Жоден мілтер не чіпає лист після AMS.
Опублікуйте selector._domainkey TXT і перевірте dig'ом до вмикання sign_local.
Наскрізний тест: надішліть через список на Gmail, відкрийте «Показати оригінал» і знайдіть arc=pass.
ARC доречний рівно там, де ви — оператор релея чи списку розсилки, який фізично переписує чужі листи. Там він рятує DMARC-alignment, на який відправник вплинути не може. Якщо ж ви керуєте лише власним доменом і нічого не пересилаєте — вам ARC підписувати нічого; достатньо, щоб ваш вхідний MX умів його перевіряти й довіряв правильним sealer-доменам.