Невідповідність хешу тіла DKIM: підступні причини, про які ніхто не розповідає

Було корисно?

Ваші панелі показників зелені. Черга пошти пуста. І все одно Gmail (або Microsoft, або шлюз партнера, який іще думає, що на дворі 2009 рік)
каже: «DKIM body hash did not verify». Повідомлення доходить. Одержувач може його прочитати. Але підпис не проходить,
вирівнювання DMARC ламається, і раптом ваш графік доставляння виглядає як гірськолижний схил.

Неймовірно дратує: «DKIM же тільки в хедерах, так?» Ні. DKIM — це ставка, що ніхто не чіпатиме повідомлення між підписуванням і перевіркою.
У реальному світі його всі чіпають — MTA, шлюзи безпеки, архіватори, додавання футерів, «допоміжні» проксі та той один вендор, який клянеться, що додає
тільки трекінг-піксель «в хедери».

Що насправді означає «невідповідність хешу тіла»

Підпис DKIM містить тег із назвою bh= (хеш тіла). Підписувач канонічно перетворює тіло (відповідно до налаштувань DKIM),
хешує його, кодує хеш у base64 і зберігає у bh=. Приймач повторює цей процес для отриманого тіла.
Якщо обчислений хеш не збігається з bh=, ви отримуєте невідповідність хешу тіла.

Це відрізняється від помилки підпису заголовків. Зміни в хедерах теж можуть зламати DKIM, але невідповідність хешу тіла вказує прямо на:
байти тіла — після канонізації — не тотожні тим, що бачив підписувач.

Канонізація — це пастка

DKIM має два режими канонізації для заголовків і тіла: simple і relaxed. Багато впроваджень використовують
relaxed/relaxed, бо він допускає деякі зміни пробілів у заголовках та деякі відмінності в пробілах у тілі.
«Дозволяє деякі» — це багато що означає в цьому контексті.

  • simple канонізація тіла: майже ніякого прощення. Будь-яка зміна байта (включно з кінцями рядків) ламає її.
  • relaxed канонізація тіла: ігнорує пробіли в кінці рядків, стискає послідовності WSP, нормалізує кінці рядків і ігнорує порожні рядки в кінці.

Але навіть relaxed не врятує вас, якщо щось вставляє футер, перекодовує quoted-printable, змінює MIME boundary або «нормалізує»
перенесення рядків всередині блоку base64. DKIM толерує неделікатні пробіли, а не творчі правки.

Практична модель: хеш тіла DKIM — це контрольна сума «того, що читає людина», плюс багато речей, яких людина не бачить — структура MIME,
transfer encoding, межі та точне розташування CRLF.

Цікаві факти й історія, що пояснюють сьогоднішні дивності

  1. DKIM не народився досконалим. Воно виникло з двох конкуруючих пропозицій (DomainKeys і Identified Internet Mail), які злилися, щоб припинити стандартну суперечку.
  2. Режими «relaxed» з’явилися, бо MTA постійно «допомогою» переформатовували пошту. Стандарти не передбачали, що бітовий потік переживе реальність незмінним.
  3. CRLF у SMTP не підлягає обговоренню. Досить багато систем досі пропускають голі LF у канали, і канонізація DKIM — місце, де цей злочин виявляється.
  4. Quoted-printable — зона ризику для DKIM. Вона призначена для перенесення рядків, і різні компоненти можуть переносити рядки під різні колонки.
  5. Деякі продукти безпеки навмисно модифікують повідомлення. Вони додають банери, переписують URL, інжектують відмовні фрази або детонують вкладення. DKIM не «розуміє» намірів.
  6. ARC існує, бо пересилка ламає DKIM. Коли пересилка змінює пошту, DKIM не проходить; ARC було введено, щоб зберегти результати автентифікації через проміжні вузли.
  7. Тег «l=» — і функція, і пастка. Він може запобігти руйнуванню, ігноруючи доданий контент, але також дозволяє зловмисникам додавати контент, що не підписаний.
  8. Розсилки (mailing lists) ускладнювали впровадження DKIM. Списки, що додають футери або переписують теми, рутинно інвалідовували підписи.
  9. DKIM застосовується для кожного повідомлення, а не для з’єднання. Будь-яка проміжна трансформація може створити невідповідності для конкретного отримувача, що виглядає випадковим.

Швидкий план діагностики (порядок триажу, що економить години)

Коли ви бачите невідповідність хешу тіла, спокуса — дивитися DNS і ключі. Не робіть цього. Ключі і DNS зазвичай руйнують перевірку повністю.
Невідповідність хешу тіла кричить «повідомлення змінено». Ось порядок, що швидко знаходить вузьке місце.

1) Підтвердіть, що це дійсно невідповідність тіла (а не заголовків)

  • Шукайте логи верифікатора, які явно згадують body hash did not verify або невідповідність bh=.
  • Якщо бачите лише «bad signature», потрібна додаткова видимість: зафіксуйте повідомлення і перевірте локально.

2) Визначте, де повідомлення підписано і де перевірено

  • Знайдіть хедер DKIM-Signature: він каже вам d= (домен), s= (селектор), c= (канонізація) і іноді l= (довжина тіла).
  • Прослідкуйте заголовки Received:: ваша модифікація ймовірно трапляється між двома суміжними хопами.

3) Порівняйте «до змін» і «після змін» тіла

  • Отримайте повідомлення таке, яке бачить підписувач (або якнайближче: на MTA, що підписує). Отримайте повідомлення таке, яке бачить отримувач (або на вашому останньому вихідному хопі).
  • Зробіть diff за допомогою інструментів, що враховують CRLF і MIME.

4) Полюйте на звичні модифікатори в ланцюжку

  • Фільтри вмісту (Amavis, Rspamd, antivirus шлюзи, DLP).
  • Вихідні футери та disclaimers для брендингу.
  • Переписування URL і проксі для трекінгу кліків.
  • SMTP-проксі, що змінюють розбивку чи перенесення рядків.
  • Архіватори / системи журналювання, що реінжектують пошту.

5) Виправляйте процес, а не симптом

  • Переконайтеся, що підписування відбувається після всіх модифікацій.
  • Або припиніть модифікувати підписану пошту й підписуйте в останньому відповідальному хопі.
  • Якщо після підпису потрібно модифікувати, прийміть, що DKIM зазнає невдач, і використайте ARC для довіри вниз по ланцюгу (де доречно).

Підступні причини, про які ніхто не розповідає (і як довести кожну)

1) Футери та відмовні фрази, додані «пізно»

Класика. Юристи хочуть дисклеймер. Маркетинг — банер. Безпека — попередження «Зовнішній лист».
Якщо це інжектується після підпису DKIM, хеш тіла не збіжиться. Якщо до підпису — все добре, поки якась інша система не додасть ще один.

Як довести: знайдіть доданий текст у фінальному тілі і підтвердіть, що його не було на хопі підписування. Видає себе рядок футера,
який з’являється після останньої MIME-границі або лише в частині text/plain.

2) Перенесення рядків у quoted-printable

Quoted-printable (QP) кодує довгі рядки м’якими розривами (= наприкінці рядка). Деякі шлюзи переносять рядки під іншу колонку,
або «нормалізують» QP, конвертуючи пробіли/табуляцію або змінюючи спосіб розриву довгих рядків.

DKIM підписує байти кодуваного тіла, а не декодований контент. Якщо змінюється QP-кодування, змінюється хеш.
Ви можете мати однакове відтворення тексту і все одно провалити DKIM.

3) Зміни меж MIME або порядку частин

MIME — це структурований документ. Якщо шлюз переставляє частини, змінює boundary або конвертує multipart/alternative у іншу структуру, DKIM ламається.
Це трапляється з фільтрами, що «очищають HTML», або з детонаторами вкладень, які перебудовують дерево MIME.

4) Зміни Content-Transfer-Encoding (8bit ↔ quoted-printable ↔ base64)

Деякі MTA знижують 8bit до quoted-printable, коли вважають, що наступний хоп не підтримує 8bit (або коли налаштовані консервативно).
Інші роблять навпаки: виявляють QP і «очищують» його. У будь-якому разі DKIM хешує те, що бачить. Зміна CTE змінює байти тіла.

5) Нормалізація кінців рядків і війна CRLF/LF

SMTP використовує CRLF. Але ви зустрінете компоненти, що зберігають повідомлення з LF і потім відтворюють їх. Якщо вони роблять це неправильно,
ви можете отримати змішані кінці рядків або відмінності канонізації, які пропускають ваш погляд.

З relaxed багато проблем із кінцями рядків толеруються. З simple ви граєтеся з відповідністю у дата-центрі.

6) «Корисне» обрізання пробілів всередині MIME-частин

Деякі фільтри обрізають пробіли в кінці, прибирають «зайві порожні рядки» або нормалізують табуляцію/пробіли. Relaxed канонізація ігнорує деякі кінцеві WSP,
але не довільне переформатування всередині тіла і не зміни в заголовках MIME частин.

7) SMTP-проксі з розбивкою і випадки dot-stuffing

Зазвичай dot-stuffing працює коректно і безпечно для DKIM. Але некоректний SMTP-проксі може неправильно обробляти рядки, що починаються з крапки,
або конвертувати провідні крапки неправильно при реінжекції. Це рідко, але коли трапляється — різниця крихітна, а біль величезний.

8) Підписування невірного представлення (порядок milter / pipeline фільтрів)

Якщо ви підписали до того, як фільтр модифікує тіло, DKIM не пройде. Якщо підписали після — пройде. Багато стеків випадково підписують не в тому місці:
milter підписує рано, потім інший milter додає хедер, або післячергова обробка перезаписує тіло.

Ця помилка проявляється як «проходить для деяких отримувачів, але не для інших», бо лише деякі маршрути проходять через модифікуючий компонент.

9) Переписування URL для трекінгу кліків

Переписування URL змінює тіло. Точка. Деякі вендори кажуть, що роблять це «не торкаючись DKIM». Вони мають на увазі:
вони можуть зберегти хедерний DKIM для повідомлень, які не переписують, або додати власний DKIM-підпис після переписування.
Але якщо ви очікуєте, що ваш DKIM переживе це, не дозволяйте нікому переписувати тіло після вашого підпису.

10) Менеджери розсилок і форвардери

Розсилки додають футери, змінюють заголовки і часом переформатовують контент. Форвардери можуть перекодовувати вміст або додавати list-unsubscribe заголовки.
DKIM ламається; DMARC ламається; і люди звинувачують DNS.

11) Тег l=: «виправлення», що створює діри в безпеці

Побачите пораду: «Просто виставте l=, щоб додавання футерів не мало значення». Це каже верифікаторам хешувати тільки перші N байтів тіла.
Так, це може зупинити невідповідності хешів тіла, коли додається контент вкінці.

Це також дозволяє зловмисникові додавати контент після підписаної частини — контент, що виглядає легітимно, бо DKIM-підпис все ще проходить.
Багато приймачів недовірливо ставляться або ігнорують l= з поважних причин. Використовуйте його лише коли ви продумали модель ризику і контролюєте весь шлях.

12) Реінжекція повідомлень системами журналювання/архівації

Деякі системи комплаєнсу захоплюють пошту, потім реінжектують її в інші системи (journaling, supervision, downstream scanning).
Якщо вони реінжектують повідомлення (навіть «без втрат»), дрібні відмінності з’являються: складання заголовків, регенерація boundary, зміни CTE.
За цим слідує невідповідність хешу тіла.

Парафразована ідея від Richard Cook (інженерія надійності): Успіх ховає ризик; відмови виявляють реальну систему, якою ви насправді керуєте.
Невідповідності хешу тіла DKIM — це відмова, що виявляє ваш реальний поштовий пайплайн.

Жарт №1: DKIM — як пломба на упаковці — відмінна, поки ваша команда продовжує «просто подивитися, що всередині».

Практичні завдання: команди, виводи та рішення (12+)

Це те, що ви робите о 02:10, коли VP каже «наші рахунки йдуть у спам», і у вас немає часу на філософські дебати.
Кожне завдання містить команду, приклад виводу, що це означає, і рішення, яке ви приймаєте.

Завдання 1: Витягніть сире повідомлення з черги Postfix (щоб не гадати)

cr0x@server:~$ sudo postcat -q 3F2A91C02E
*** ENVELOPE RECORDS ***
message_size: 48231            704         1               0
message_arrival_time: Sat Jan  3 01:12:09 2026
sender: billing@example.com
*** MESSAGE CONTENTS ***
Received: from app01 (app01.internal [10.0.12.34])
        by mx-out01.example.com (Postfix) with ESMTP id 3F2A91C02E
        for <user@recipient.tld>; Sat,  3 Jan 2026 01:12:08 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=mail;
        h=from:to:subject:date:mime-version:content-type;
        bh=VQeN9v5kG7WJQm...snip...=;
        b=Q0Vn...snip...
Content-Type: multipart/alternative; boundary="=_b4c1a7f1d1"
...snip...

Що це означає: Тепер у вас є точне тіло повідомлення, як його побачив ваш вихідний MTA. Це ваш «вхід підпису».

Рішення: Збережіть це у файл і перевірте DKIM локально. Якщо тут воно проходить, а в отримувача — ні, щось змінюється після цього хопу.

Завдання 2: Збережіть повідомлення у файл для повторюваного тестування

cr0x@server:~$ sudo postcat -q 3F2A91C02E > /tmp/msg.eml
cr0x@server:~$ ls -l /tmp/msg.eml
-rw-r--r-- 1 root root 48231 Jan  3 01:14 /tmp/msg.eml

Що це означає: Ви зафіксували докази.

Рішення: Використовуйте той самий файл для тестування OpenDKIM, парсингу MIME і обчислення канонізованих хешів тіла послідовно.

Завдання 3: Витягніть хедер DKIM-Signature і зафіксуйте канонізацію

cr0x@server:~$ grep -n '^DKIM-Signature:' -A2 /tmp/msg.eml
8:DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=mail;
9-        h=from:to:subject:date:mime-version:content-type;
10-        bh=VQeN9v5kG7WJQm...snip...=; b=Q0Vn...snip...

Що це означає: Канонізація тіла — relaxed. Це звужує ймовірні причини: не проста проблема з CRLF, а скоріше редагування тіла або перекодування.

Рішення: Зосередьте увагу на фільтрах, що модифікують контент, а не лише на пробілах.

Завдання 4: Перевірте DKIM локально за допомогою OpenDKIM (базова перевірка)

cr0x@server:~$ opendkim-testmsg -d example.com -s mail -k /etc/opendkim/keys/example.com/mail.private < /tmp/msg.eml
opendkim-testmsg: signature ok

Що це означає: Повідомлення, як зафіксовано, відповідає очікуванням підписувача. Ваш ключ і конфіг підписування в порядку.

Рішення: Перестаньте хвилюватися про DNS. Почніть шукати модифікації вниз по ланцюгу після хопу підписування.

Завдання 5: Перегляньте Received-хедери, щоб визначити вікно модифікації

cr0x@server:~$ grep -n '^Received:' -n /tmp/msg.eml
1:Received: from app01 (app01.internal [10.0.12.34])
3:Received: by mx-out01.example.com (Postfix) with ESMTP id 3F2A91C02E

Що це означає: У цьому прикладі ланцюг короткий. В реальних інцидентах ви побачите багато хопів (внутрішні реле, шлюзи безпеки, cloud MTA).

Рішення: Визначте останній хоп під вашим контролем перед отримувачем. Зафіксуйте там теж для дифу.

Завдання 6: Захопіть SMTP-трафік на вихідному інтерфейсі (якщо треба)

cr0x@server:~$ sudo tcpdump -i eth0 -s 0 -w /tmp/smtp.pcap 'tcp port 25 and host 203.0.113.55'
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C
124 packets captured

Що це означає: Ви зафіксували реальні байти на лінку до наступного хопу. Це дорога та шумна операція; використовуйте, коли логів недостатньо.

Рішення: Відтворіть DATA-частину і порівняйте її з вашим файлом .eml. Якщо вони різні — щось переписує між чергою і мережею.

Завдання 7: Перевірте післячергові фільтри в Postfix (частий підозрюваний)

cr0x@server:~$ postconf | egrep 'content_filter|smtp_tls_policy_maps|receive_override_options'
content_filter = smtp-amavis:[127.0.0.1]:10024
receive_override_options = no_address_mappings

Що це означає: Наявний content filter. Amavis (або подібний) часто перекодовує MIME, особливо навколо вкладень і QP.

Рішення: Переконайтеся, що DKIM підписування відбувається після цього фільтра, або налаштуйте фільтр на мінімальне переписування.

Завдання 8: Перевірте режими OpenDKIM і розміщення мітлера

cr0x@server:~$ grep -E '^(Mode|Socket|Canonicalization|OversignHeaders|SignHeaders)' /etc/opendkim.conf
Mode                    sv
Socket                  inet:8891@localhost
Canonicalization        relaxed/relaxed
OversignHeaders         From
SignHeaders             From,To,Subject,Date,Message-ID,MIME-Version,Content-Type

Що це означає: OpenDKIM підписує і верифікує (sv) і використовує relaxed/relaxed. OversignHeaders для From — хороша практика.

Рішення: Підтвердіть, що цей milter працює на стадії фінальної вихідної інжекції, а не на ранішому внутрішньому хопі.

Завдання 9: Визначте, чи шлюз інжектує банери/футери

cr0x@server:~$ grep -nE 'External Email|DISCLAIMER|This message originated' /tmp/msg.eml
cr0x@server:~$ echo $?
1

Що це означає: Повідомлення в черзі не містить типових інжектованих банерів.

Рішення: Якщо отримувач бачить банер, його додають після вашої фіксації черги — ймовірно на вихідному шлюзі або на боці отримувача.

Завдання 10: Перевірте м’які розриви в quoted-printable і поведінку переносу рядків

cr0x@server:~$ grep -n '^Content-Transfer-Encoding:' -n /tmp/msg.eml | head
22:Content-Transfer-Encoding: quoted-printable
cr0x@server:~$ sed -n '30,60p' /tmp/msg.eml | sed -n '1,10p'
Dear customer,=0D=0A=0D=0APlease remit payment within 30 days.=0D=0A=
If you have questions, reply to this email.=0D=0A

Що це означає: Тіло повідомлення QP-кодоване; наявність = переносів і явних =0D=0A робить його крихким.

Рішення: Підозрюйте будь-який шлюз, що декодує/перекодовує QP, або систему, яка «нормалізує» довжину рядків.

Завдання 11: Визначте CRLF vs LF (не довіряйте редактору)

cr0x@server:~$ python3 - < /tmp/msg.eml
import sys
data = sys.stdin.buffer.read()
print("CRLF count:", data.count(b"\r\n"))
print("LF count:", data.count(b"\n"))
print("Bare LF count:", data.count(b"\n") - data.count(b"\r\n"))
PY
CRLF count: 812
LF count: 812
Bare LF count: 0

Що це означає: Файл має чисті CRLF. Якщо downstream-скіди показують голі LF — ви знайшли вектор мутації тіла.

Рішення: Якщо bare LF з’являється після хопу, виправте відповідне реле/проксі або забезпечте підписування після нього.

Завдання 12: Витягніть і порівняйте MIME-boundary (зміна boundary — явний доказ)

cr0x@server:~$ grep -n '^Content-Type: multipart' -n /tmp/msg.eml
15:Content-Type: multipart/alternative; boundary="=_b4c1a7f1d1"
cr0x@server:~$ grep -n '^--=_b4c1a7f1d1' /tmp/msg.eml | head
27:--=_b4c1a7f1d1
88:--=_b4c1a7f1d1--

Що це означає: Ви знаєте очікуваний токен boundary. Якщо downstream-копія має інший boundary — щось перебудувало MIME.

Рішення: Розглядайте перебудовувачів MIME як ворожих до DKIM. Перемістіть підписування після них або налаштуйте їх, щоб не серіалізували контент заново.

Завдання 13: Перевірте, чи підписувач використовував ризиковий тег l=

cr0x@server:~$ grep -o ' l=[0-9]\+' -n /tmp/msg.eml
cr0x@server:~$ echo $?
1

Що це означає: Обмеження довжини тіла не використовується. DKIM очікує, що все тіло залишиться стабільним.

Рішення: Добре для безпеки; погано для пайплайнів з футерами. Виправляйте пайплайн, а не підпис, якщо ви не приймаєте ризики l=.

Завдання 14: Порівняйте два тіла повідомлень з двох хопів (diff, що враховує байти)

cr0x@server:~$ python3 - << 'PY'
import email, sys
from email import policy
def body_bytes(path):
    with open(path,'rb') as f:
        msg = email.message_from_binary_file(f, policy=policy.default)
    if msg.is_multipart():
        # take full serialized body (post headers) for byte diff
        raw = open(path,'rb').read()
        return raw.split(b"\r\n\r\n",1)[1]
    else:
        raw = open(path,'rb').read()
        return raw.split(b"\r\n\r\n",1)[1]
a = body_bytes("/tmp/msg.eml")
b = body_bytes("/tmp/msg-from-gateway.eml")
print("body lengths:", len(a), len(b))
print("first differing byte index:", next((i for i in range(min(len(a),len(b))) if a[i]!=b[i]), -1))
PY
body lengths: 41802 42110
first differing byte index: 21794

Що це означає: Тіло змінилося між хопами; у вас навіть є приблизна позиція для інспекції.

Рішення: Відкрийте обидва файли навколо того індексу, ідентифікуйте вставлений/переписаний контент і прив’яжіть його до конкретної функції шлюзу.

Жарт №2: Пошта — єдина система, де додавання дружнього футера може трактуватися як підробка — бо так воно і є.

Три корпоративні міні-історії з передової

Міні-історія 1: Інцидент через невірне припущення

Середня компанія мала чистий, нудний вихідний стек на Postfix з OpenDKIM. Їхня політика DMARC була quarantine, і вони цим пишалися.
Потім вони розгорнули продукт «safe links» на вихідному шляху — рекламували його як «прозорий».

Невірне припущення було просте: «Він торкається лише посилань в HTML; DKIM relaxed; все буде добре.» Перший симптом не був драматичним відключенням.
Це була повільна втрата: партнери зі строгим DMARC почали відхиляти замовлення. Всередині все виглядало доставленим. SMTP 250 OK були скрізь.

Прорив стався після порівняння чергового повідомлення з тим, що отримав тестовий поштовий ящик на іншому домені.
HTML-частина мала переписані посилання, а text/plain частина отримала параметр трекінгу на голих URL. Обидві частини змінилися, тож невідповідність хешу тіла DKIM була гарантована.
Продукт також по-іншому перекодовував quoted-printable, тому навіть повідомлення без посилань іноді ламалися.

Виправлення вимагало незручного рішення: або підписувати після переписування (тобто шлюз безпеки повинен підписувати DKIM від імені організації),
або перестати переписувати вихідну пошту взагалі. Вони вибрали підписування після переписування, перемістивши «фінальне DKIM-підписування» на останній хоп і забезпечивши шлюз правильними ключами селектора.
Також додали моніторинг несподіваних мутацій контенту, хешуючи тіло на кількох хопах.

Міні-історія 2: Оптимізація, що обернулася проти

Інша організація хотіла зменшити CPU-навантаження на MTA. Хтось помітив, що їхній фільтр контенту декодує й перекодовує все, навіть коли загроз не знайдено.
«Оптимізація» — увімкнути режим, що «нормалізує» MIME і консолідує кодування для кращої компресії.

Це виглядало чудово в мікробенчмарку: менше вихідної пропускної здатності, менші архівні повідомлення, менше дивних кодувань. Потім DKIM почав падати лише
для певних класів повідомлень: рахунки від одного додатку і відповіді служби підтримки від іншого. Оскільки ті додатки використовували різні бібліотеки, MIME іноді відрізнявся
настільки, щоб спричинити шлях переписування фільтра.

Збої були періодичні по отримувачах, бо не всі вихідні маршрути йшли через однаковий кластер фільтрів. Деякі отримувачі бачили, що DKIM проходить. Інші — невідповідність хешу тіла.
Організація тиждень звинувачувала DNS-пропагацію і «кешування отримувача», бо це історія, яку люди розповідають, коли не мають дифу.

Виправлення — відкотити налаштування «normalize» і змінити архітектуру: сканувати й переписувати контент перед DKIM-підписуванням, і вважати хоп підписування сакральним.
CPU-навантаження повернулося, але доставляння перестало бути рулеткою.

Міні-історія 3: Скупа, але правильна практика, що врятувала день

Регульований бізнес мав правило: «Жодних модифікацій пошти після підпису. Ніколи.» Це було не дуже популярно. Люди хотіли банерів, відділових футерів
і змін брендингу в останній момент. Поштова команда постійно казала «ні».

Вони дозволяли фільтрацію контенту, але лише в попередньому до підпису пайплайні. Їхній фінальний вихідний релей робив рівно три речі:
забезпечував TLS-політику, обмежував швидкість зловмисних клієнтів і DKIM-підписував. Ніяких банерів. Жодного переписування URL. Ніякої реінжекції в архів.

Під час інциденту в екосистемі партнерів — коли кілька отримувачів посилили DMARC і фільтри — ця організація майже не зазнала перебоїв.
Коли інші металися з поясненнями невідповідностей хешу тіла, вони могли швидко показати, що байти їхніх повідомлень лишалися стабільними від підпису до доставки.
Скупа практика не була ефектною; це була операційна гігієна.

Деталь, що «врятувала день»: у них був стандартний точка захоплення на фінальному реле, яка зберігала точну пост-підписну копію повідомлення протягом 48 годин для форензики.
Коли отримувач стверджував «ваш DKIM падає», вони могли довести, чи модифікував шлюз отримувача повідомлення після отримання,
чи помилка була в їхньому ланцюжку. Більшість спорів швидко завершувались. Тихі перемоги — теж перемоги.

Поширені помилки: симптом → корінь → виправлення

1) Симптом: DKIM падає тільки при відправці певним партнерам

Корінь: Маршруто-залежне переписування (інше вихідне реле, smart host, TLS-шлюз або DLP-апарат для тих доменів).

Виправлення: Порівняйте повідомлення, зафіксоване на фінальному хопі підписування, з тим, що спостерігається після маршруту-специфічного шлюзу. Стандартизуйте маршрути або перемістіть підписування після останнього модифікатора.

2) Симптом: DKIM падає лише для HTML-листів, а не plain text

Корінь: Санітизація HTML, трекінг кліків або інжекція банерів в HTML-частину модифікують одну MIME-частину.

Виправлення: Вимкніть переписування HTML для вихідних, або переконайтеся, що система, що переписує, сама DKIM-підписує від імені вашого домену після модифікації.

3) Симптом: Невідповідність хешу тіла після увімкнення нового антивірусу або DLP

Корінь: Продукт ресеріалізує MIME або змінює Content-Transfer-Encoding навіть коли «загроз не знайдено».

Виправлення: Налаштуйте режим «pass-through», що зберігає байти, або розмістіть DKIM-підписування після продукту, або оберіть продукт, який підтримує «не переписувати, якщо не потрібно».

4) Симптом: DKIM час від часу падає для одного й того ж шаблона

Корінь: Недетерміноване переписування (випадкові boundary-токени, змінне QP-обгортання або відмінності між нодами).

Виправлення: Зробіть серіалізацію детермінованою: зафіксуйте бібліотеки, вимкніть нормалізацію і підписуйте в останньому хопі. Також переконайтеся, що всі ноди мають ідентичні конфіги.

5) Симптом: DKIM проходить у внутрішніх тестах, але падає «в польоті»

Корінь: Ваш тестовий шлях обходить реальний вихідний компонент (cloud connector, journaling service, relay партнера або вихідний проксі).

Виправлення: Тестуйте тим самим вихідним маршрутом, що використовують продакшн-отримувачі. Захоплюйте на кожному хопі і робіть diff.

6) Симптом: Повідомлення з вкладеннями частіше не проходять DKIM

Корінь: Сканування вкладень/детонація перебудовують MIME або змінюють обгортку base64.

Виправлення: Перемістіть підписування після обробки вкладень або налаштуйте сканування, щоб уникати перекодування, коли можливо.

7) Симптом: DKIM ламається після увімкнення «додати дисклеймер до вихідної пошти» на Exchange або шлюзі

Корінь: Дисклеймер вставлено після підпису, або підписування відбувається на іншому хопі, ніж ви думаєте.

Виправлення: Перепорядкуйте транспортні правила так, щоб дисклеймери додавались до підписування. Якщо не можете — прийміть, що DKIM не пройде, і перебудуйте архітектуру.

8) Симптом: У повідомленні згадується «body canonicalization simple»

Корінь: Ви використовуєте c=simple/simple або simple канонізацію тіла і щось чіпає пробіли/кінці рядків.

Виправлення: Використовуйте relaxed/relaxed, якщо ви не контролюєте кожний хоп і не можете довести байтову стабільність.

Чеклісти / покроковий план

Чекліст A: Перші 30 хвилин інциденту з невідповідністю хешу тіла DKIM

  1. Отримайте одне невдале повідомлення з повними заголовками і сирим джерелом (краще від отримувача, якщо можливо).
  2. Витягніть те саме повідомлення з вашої вихідної черги або логів в той же час (postcat, копія журналу або точка фіксації).
  3. Перевірте чергову копію локально за допомогою інструментів OpenDKIM; зафіксуйте, чи проходить вона.
  4. Витягніть параметри DKIM: d=, s=, c=, і чи є l=.
  5. Порівняйте тіла побайтово і знайдіть першу відмінність.
  6. Визначте хоп, де трапилась модифікація, корелюючи Received-хедери і правила маршрутизації.
  7. Рішення: прибрати модифікатор, перемістити підписування після нього або прийняти збій і впровадити ARC/інший механізм довіри.

Чекліст B: Зробіть ваш вихідний пайплайн безпечним для DKIM (нудна архітектура)

  1. Визначте один «last-mile» вихідний релей, завдання якого — підписувати і відправляти, а не «покращувати» контент.
  2. Розмістіть фільтрацію контенту, переписування, дисклеймери і брендинг перед цим реле.
  3. Переконайтеся, що останній релей не запускає післячергові фільтри, які можуть переписати контент.
  4. Стандартизуйте канонізацію: relaxed/relaxed для більшості організацій.
  5. Підтримуйте послідовну генерацію MIME між додатками (вибір бібліотеки має значення).
  6. Розгорніть точку фіксації на last-mile реле для форензики (з коротким терміном зберігання).
  7. Моніторьте невдачі DKIM через фідбеки від отримувачів і шляхом вибіркового верифікування вихідних копій.

Чекліст C: Питання при перевірці змін вендора та внутрішніх змін

  • Чи цей компонент переписує URL, HTML або додає банери?
  • Чи декодує і перекодовує він MIME? Чи зберігає Content-Transfer-Encoding?
  • Чи перебудовує boundary MIME або змінює multipart-структуру?
  • Де в поштовому потоці відбувається DKIM-підпис стосовно цієї зміни?
  • Чи можемо ми довести побайтову стабільність після підпису через diff?
  • Чи існує перо-отримувацька маршрутизація, що може спричиняти непослідовну поведінку?

FAQ

Чому DKIM падає, якщо вміст листа «виглядає однаково»?

Тому що DKIM підписує байти, а не рендер вашого поштового клієнта. Зміни в обгортанні quoted-printable, boundary MIME або transfer encoding можуть зберегти вигляд, але змінити байти.

Чи достатньо relaxed канонізації, щоб запобігти невідповідностям хешу тіла?

Вона запобігає помилкам через тривіальні відмінності пробілів. Вона не захистить від інжекції контенту, переписування URL, переструктурування MIME або перекодування.

Чи варто використовувати c=simple/simple для кращої безпеки?

Тільки якщо ви контролюєте весь шлях і можете довести, що нічого не модифікує повідомлення. Інакше це самоосудження: ви поміняєте теоретичну чистоту на реальні відмови.

Чи можна «вирішити» це, обертаючи ключі DKIM або змінюючи DNS?

Ні, якщо помилка — невідповідність хешу тіла. Проблеми з ключами і DNS зазвичай призводять до «no key», «bad key» або помилок перевірки підпису, що не пов’язані з bh.
Ваша проблема — мутація повідомлення.

Чи допоможе підписування з l=, щоб ігнорувати додані футери?

Це може зменшити збої, коли додають футери, але це послаблює цілісність: зловмисник може додати контент поза підписаною частиною.
Багато приймачів скептично ставляться до l=. Спочатку виправляйте пайплайн.

Чому пересилка так часто ламає DKIM?

Форвардери часто модифікують повідомлення (перекодування, додавання заголовків списку, переформатування), і DKIM не виживає змін. ARC був введений, щоб допомогти зберегти автентифікацію через хопи.

Чому це відбувається тільки з повідомленнями з вкладеннями?

Вкладення призводять до сканування і обробки контенту, що може перебудувати MIME або змінити обгортку base64. Це зміни тіла, які інвалідовують bh.

Як вирішити, де підписувати DKIM у складному середовищі?

Підписуйте на останньому хопі, який ви контролюєте перед публічним інтернетом — після всіх модифікацій. Зробіть цей хоп нудним. Усе «розумне» має відбуватися перед ним.

Чи можуть отримувачі спричинити невідповідність хешу тіла на їхньому боці?

Так. Деякі вхідні шлюзи модифікують пошту (банери, переписування URL, детонація). Якщо ви можете довести стабільність вихідних байтів, помилка може бути в ланцюгу отримувача.

Чи частіше DKIM падає з UTF-8 або міжнародними символами?

Саме по собі — ні. Проблеми виникають через конверсії кодувань (8bit/QP/base64) і пониження транспорту, спричинене не ASCII-контентом.

Висновок: наступні кроки, що справді зменшують інциденти

Невідповідність хешу тіла DKIM рідко є «проблемою DKIM». Це ваш поштовий пайплайн, який повідомляє, що хтось переписує контент після підпису.
Така зміна може бути доброю наміром (банери безпеки) або тихо руйнівною (QP rewrapping, перебудова MIME). У будь-якому випадку виправлення — архітектурне.

  1. Виберіть одне last-mile реле і зробіть його освяченою коробкою для підпису й відправки. Ніяких дисклеймерів. Жодного переписування URL. Жодної нормалізації MIME.
  2. Перемістіть усі модифікації upstream від підписування, і вважайте будь-який пост-підписний компонент підозрілим, доки він не доведе байтову тотожність через diff.
  3. Інструментуйте для доказів: зберігайте короткострокову копію пост-підписного повідомлення на останньому хопі, щоб вирішувати спори доказами, а не емоціями.
  4. Стандартизуйте канонізацію (зазвичай relaxed/relaxed) і уникайте l=, якщо ви не приймаєте відповідні компроміси безпеки.
  5. Коли вендор каже «прозоро», запитайте «Чи змінюється сире тіло?» Якщо вони не можуть відповісти — вважайте, що так.

Вам не треба любити пошту, щоб гарно її адмініструвати. Потрібно просто припинити дозволяти випадковим коробкам переписувати ваші байти після того, як ви дали обіцянку їх не чіпати.

← Попередня
L2TP/IPsec підключається, але інтернет не працює: чому так відбувається і як це виправити
Наступна →
Проблеми доставки в Gmail і Outlook: ключові перевірки 2025

Залишити коментар