Звіти DMARC (RUA/RUF): як увімкнути, зібрати й прочитати XML руками
DMARC-звіти — єдине вікно, крізь яке видно, хто реально шле пошту від вашого домену. Розбираємо, як правильно налаштувати теги rua/ruf, самому зібрати агреговані звіти через IMAP і читати XML без залежності від SaaS.
EvilMail Team9 липня 2026 р.10 хв читання
Ви поставили p=none, запис відповідає стандарту — і тиша. Або навпаки: скринька dmarc@ за тиждень набила півтори тисячі однакових на вигляд .gz-файлів, і незрозуміло, що з ними робити. Жоден із цих станів не означає «DMARC не працює». Він означає, що ніхто не навчив читати самі звіти — а в них єдине об'єктивне джерело правди про те, хто у світі шле пошту з @вашдомен у полі From.
Ця стаття — не про «увімкни і чекай», а про звіти як інструмент розслідування: як налаштувати rua/ruf без помилок, як власноруч зібрати сирі XML зі скриньки і як читати конкретний `, щоб відрізнити забутий легітимний сервіс від справжньої підробки з чужого IP.
## Що показують RUA і RUF (і чому це різні речі)
DMARC генерує два різні потоки телеметрії.
**RUA (aggregate)** — добові XML-зведення. Приймаючий сервер (Gmail, Yahoo, Mail.ru) бере всю пошту, яку за добу отримав із вашим доменом у From, групує її за IP-джерелом і результатами автентифікації і надсилає одним стиснутим файлом. Тіла листів там немає — лише статистика: скільки повідомлень, з якого IP, пройшли SPF/DKIM чи ні, як спрацював alignment, який вердикт застосовано. Це 95% корисного сигналу.
**RUF (forensic/failure)** — поодинокі семпли конкретних листів, що не пройшли автентифікацію, з заголовками (а іноді й тілом). Задумувалося як «покажи мені сам підроблений лист». На практиці у 2026 році RUF майже мертвий: Google і Microsoft його не шлють у принципі, більшість великих провайдерів ЄС вимкнули forensic через GDPR (тіло листа = персональні дані третіх осіб), а ті одиниці, що ще шлють, віддають сильно відредаговані семпли. Ставити ruf варто — гірше не буде, — але будувати процес навколо нього не можна. Уся реальна робота — з
Звіти DMARC RUA/RUF: збір і читання XML руками — EvilMail Blog
rua
.
## Вмикаємо звіти: правильний DMARC-запис
Ось запис, який ми ставимо як базовий для домену, що вже пройшов інвентаризацію джерел:
dns
_dmarc.evilmail.pro. IN TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; ruf=mailto:[email protected]; fo=1; rf=afrf; ri=86400; adkim=s; aspf=s; pct=100; sp=reject"
Теги, що реально впливають на те, що вам прийде:
- **
. Це нормально й корисно — один збірник ваш, другий сторонній для контролю.
- **
fo
** — за яких умов приймач шле failure-звіт. Дефолт
fo=0
означає «шли, тільки якщо провалилось геть усе — і SPF, і DKIM». Це занадто вузько: лист, у якого DKIM pass, а SPF fail, звіту не породить, і ви пропустите проблему alignment. Ставте
fo=1
— «шли, якщо провалилось хоч що-небудь». Є ще
fo=d
(лише DKIM-фейли) і
fo=s
(лише SPF).
- **
ri=86400
** — бажаний інтервал агрегації в секундах, тобто раз на добу. Приймач має право його ігнорувати; практично всі й так шлють раз на 24 години.
- **
adkim
/
aspf
** — режим alignment.
r
(relaxed) — достатньо збігу організаційного домену (
mail.evilmail.pro
вирівнюється з
evilmail.pro
).
s
(strict) — потрібен повний збіг FQDN. Починайте з
r
, переходьте на
s
, коли впевнені у своїх підписувачах.
- **
sp
** — політика для піддоменів окремо від
p
. Часто забувають, і піддомени лишаються діркою.
rf=afrf
і
pct
тут другорядні:
rf
задає формат forensic (єдиний реальний варіант),
pct
— частку пошти під enforcement, до неї повернемось наприкінці.
## Cross-domain authorization: чому вам нічого не приходить
Найчастіша причина «поставили rua — тиша»: звіти йдуть на адресу поза вашим доменом, а ви не дали на це дозвіл.
Правило з RFC 7489 таке: якщо домен у
rua
/
ruf
**не збігається** з доменом, для якого опублікована політика, приймач **зобов'язаний перевірити**, чи цей зовнішній домен згоден приймати звіти. Робить він це запитом спеціального TXT-запису. Немає запису — сумлінний приймач звіт **не надішле**. Gmail дотримує це правило суворо.
Припустимо, ми на evilmail.pro збираємо звіти для клієнта
. Щоб звіти дійшли, **на нашому боці** має існувати:
dns
example.com._report._dmarc.evilmail.pro. IN TXT "v=DMARC1"
Читається так: «домен evilmail.pro підтверджує, що згоден приймати DMARC-звіти від example.com». Без цього рядка ланцюг рветься мовчки — жодної помилки, просто порожня скринька. Коли до нас приходять зі скаргою «DMARC не шле звіти», у восьми випадках із десяти бракує саме
_report._dmarc`.
Життєвий цикл DMARC-звіту (затримка 24–48 год)
Відправник
From: @evilmail.pro
Приймач (Gmail)
SPF+DKIM+alignment
Агрегація за добу
XML → gzip
rua: dmarc@
наша скринька
парсер / дашборд
gunzip → XML
IMAP-збір (993)
Наш бік ↓
Звіт описує пошту, що надійшла до 48 год тому — це ретроспектива, не realtime.
## Збираємо звіти самі: IMAP → gzip/zip → XML
SaaS на кшталт Dmarcian чи Postmark зручні, поки їх не треба масштабувати або поки ви готові віддавати телеметрію своїх доменів назовні. Свій збір робиться за пів години.
Виділіть окрему скриньку dmarc@ — на великому домені це сотні листів на день, і мішати їх із людською поштою не варто. Далі — простий Python на стандартному imaplib, без залежностей:
``python
import imaplib, email, gzip, zipfile, io, os
os.makedirs("reports", exist_ok=True)
def save(name, data):
with open(os.path.join("reports", os.path.basename(name)), "wb") as f:
f.write(data)
M = imaplib.IMAP4_SSL("mail.evilmail.pro", 993)
M.login("[email protected]", os.environ["DMARC_PASS"])
M.select("INBOX")
_, data = M.search(None, "UNSEEN")
for num in data[0].split():
_, msg_data = M.fetch(num, "(RFC822)")
msg = email.message_from_bytes(msg_data[0][1])
for part in msg.walk():
payload = part.get_payload(decode=True)
if not payload:
continue
ctype = part.get_content_type()
if ctype in ("application/gzip", "application/x-gzip"):
name = part.get_filename() or "report.xml.gz"
save(name[:-3] if name.endswith(".gz") else name, gzip.decompress(payload))
elif ctype in ("application/zip", "application/x-zip-compressed"):
with zipfile.ZipFile(io.BytesIO(payload)) as z:
for inner in z.namelist():
save(inner, z.read(inner))
M.logout()
Microsoft/Outlook шле .zip, решта — переважно .gz, тому обробляйте обидва. Імена файлів приходять за конвенцією receiver!policydomain!begin!end.xml.gz, де begin/end — unix-таймстампи вікна агрегації:
google.com!evilmail.pro!1719360000!1719446400.xml.gz
Це готовий ключ для сортування: за яким приймачем звіт і за яку добу. Розпакувати вручну — gunzip *.gz або unzip *.zip.
## Читаємо XML руками: анатомія одного звіту
Структура агрегованого звіту завжди однакова. Зверху — метадані, далі політика, яку приймач побачив у DNS, і потім найголовніше — набір , по одному на кожну унікальну комбінацію IP + результати.
``xml
google.com
2748163900123456
17193600001719446400
evilmail.pro
ss
quarantine
reject100
198.51.100.24
1420
quarantine
fail
fail
sendgrid.nets1pass
bounces.sendgrid.netpass
``
Ключове, повз що проходить більшість: у record є **дві паралельні гілки результатів**, і вони означають різне.
auth_results — це **сира** криптоперевірка. dkim result=pass тут значить лише «підпис математично валідний», а spf result=pass — «IP авторизований для того домену, що стоїть у SMTP-конверті (return-path)».
policy_evaluated — це результат **після alignment**, тобто після перевірки, що домен із підпису (і домен із SPF) збігається з доменом у From. Саме за policy_evaluated приймач ухвалює вердикт.
У прикладі вище обидва raw-результати зелені: підпис SendGrid валідний, а SPF для його return-path bounces.sendgrid.net проходить. Але в policy_evaluated обидва — fail: підпис зроблено доменом d=sendgrid.net, SPF авторизує bounces.sendgrid.net, і жоден не збігається з From @evilmail.pro. Alignment не виконано, DMARC провалено, і за p=quarantine ці 1420 листів щодоби летять у спам. Хто дивиться лише на raw pass/fail, доповість «пошта автентифікована» — і не зрозуміє, чому доставність просіла.
Анатомія <record>: raw pass ≠ alignment pass
<record>
policy_evaluated
(ПІСЛЯ alignment — вердикт)
disposition: quarantine
spf: fail ←
dkim: fail ←
домени з auth_results ≠ From @evilmail.pro
auth_results
(сира криптоперевірка)
dkim d=sendgrid.net
dkim: pass
spf d=bounces.sendgrid.net
spf: pass
підписи валідні, домени чужі
Пастка: обидва pass праворуч → обидва fail ліворуч (alignment)
## Три сценарії, які видно в даних
Прочитавши десяток звітів, ви навчитеся класифікувати кожен за секунди. Практично все зводиться до трьох типів.
**1. Легітимний ESP без alignment.** Raw SPF і DKIM зелені, але після alignment обидва (або хоча б DKIM) — fail; d= і return-path вказують на домен провайдера (sendgrid.net, mailgun.org, amazonses.com); count великий і стабільний. Це не атака — це ваша ж розсилка, яку забули вирівняти. Лікування: підняти DKIM CNAME на своєму піддомені (s1._domainkey.evilmail.pro CNAME ...), щоб підпис ішов вашим доменом, і/або перевести return-path на свій bounce-піддомен заради SPF-alignment. IP тут з діапазону провайдера — це нормально.
**2. Власний сервер — еталон.** source_ip — ваш MX, обидва policy_evaluated.dkim і spf = pass, disposition=none. Це зелена лінія, з якою звіряєте все інше. Свій діапазон IP треба знати напам'ять.
**3. Справжня підробка.** Незнайомий source_ip (часто з іншого континенту або з відомих botnet-діапазонів), обидва dkim і spf = fail і в raw, і після alignment, disposition=quarantine або reject. Якщо count різко стрибнув — хтось запустив кампанію від вашого імені. Тут DMARC уже робить свою роботу: за p=quarantine ці листи летять у спам одержувачів. Ваше завдання — зафіксувати IP і переконатися, що жодне легітимне джерело не ховається серед них.
count і source_ip разом дають масштаб: 1420 повідомлень з одного IP за добу — це або ваш продакшн, або серйозна атака, третього не буває.
## Від none до reject: як читати звіти по дорозі
DMARC піднімають даними, а не вірою. Наша послідовність, обкатана на тисячах доменів:
1. **p=none, мінімум 2 тижні.** Чиста інвентаризація. Збираєте всі source_ip, будуєте список легітимних джерел. Нічого не блокується — ви лише дивитеся.
2. **p=quarantine; pct=25.** Чверть невирівняної пошти йде в спам. Стежите у звітах за новими fail, яких раніше не бачили, — це джерела, які проґавили на етапі інвентаризації.
3. **pct=50 → pct=100.** Піднімаєте сходинками, коли нових сюрпризів у звітах немає.
4. **p=reject** — тільки коли **всі** легітимні source_ip стабільно зелені кілька діб поспіль.
Не ставте p=reject «бо так радять у гайдах». Reject на домені з невирівняним ESP — це ваші ж транзакційні листи в /dev/null, і дізнаєтесь ви про це від розлюченого відділу продажів, а не зі звіту.
Хто хоче генерувати ще й **вихідні** звіти зі свого Postfix — це OpenDMARC: /etc/opendmarc.conf з AuthservID і TrustedAuthenticationServices, потім opendmarc-import завантажує історію в базу, а opendmarc-reports формує добові RUA для доменів, які шлють вам пошту.
## Чек-лист впровадження
- Валідний _dmarc TXT-запис, перевірений через dig TXT _dmarc.вашдомен.
- fo=1 замість дефолтного fo=0 — інакше пропустите частину alignment-фейлів.
- Окрема скринька dmarc@, не змішана з людською поштою.
- Cross-domain authorization (policydomain._report._dmarc), якщо rua вказує на зовнішній домен, — інакше тиша.
- Автозбір по IMAP (cron кожні кілька годин), розпакування .gz і .zip.
- Парсер, що дивиться на **policy_evaluated, а не на raw auth_results** — це головне.
- Список власних легітимних source_ip під рукою як еталон.
- Тижнева ретроспектива: нові IP, нові d=-домени, стрибки count.
- Тригер-алерт на disposition≠none з невідомого IP — саме тут ловиться активна підробка.
- Не рухати політику до reject, поки звіти не покажуть чисто.
RUF залиште увімкненим для повноти, але не будуйте на ньому нічого. Сигнал — у RUA. А єдина навичка, що відрізняє того, хто «налаштував DMARC», від того, хто ним керує, — уміння за пів секунди сказати, чому pass у правій колонці записа обертається fail` у лівій.