DMARC 집계 보고서로 내 발송 IP 지켜보기

아프로·2026년 10월 30일

DMARC 집계 보고서로 내 발송 IP 지켜보기 표지

고객센터로 "청구서 메일을 받았는데 진짜 귀사가 보낸 것이냐"는 문의가 옵니다. 보낸 사람 주소는 우리 도메인입니다. 우리는 그런 메일을 보낸 적이 없습니다. DMARC 레코드는 1년 전에 p=quarantine으로 걸어 두었고, 보고서 받는 주소(rua)도 적어 두었지만 그 우편함은 아무도 열어 보지 않았습니다. 열어 보니 XML 첨부가 수백 개 쌓여 있습니다. 이번 글은 그 XML에서 "우리 이름으로 메일을 보낸 IP 목록"을 꺼내, 모르는 발송처를 찾는 일을 직접 해 봅니다.

집계 보고서가 하는 일

받는 쪽 수신자 A·B·C가 XML 보고서를 DMARC 레코드의 rua 우편함으로 보낸다 → 파서(gzip 풀기, 이름공간 제거, 크기 제한) → IP별 표(통과·실패·정렬, 발송처 대장과 맞대기) → 모르는 발송처 경보 → 13편이 찾아보는 JSON

왼쪽에서 오른쪽으로 흐릅니다. 맨 왼쪽은 남이 보내 주는 것이고, 가운데부터는 내가 만드는 것입니다. 아래 노란 띠는 이 흐름이 DMARC 보고서 분석 서비스가 하는 일의 뼈대라는 뜻입니다.

DMARC(Domain-based Message Authentication, Reporting, and Conformance)는 세 가지를 합니다. 내 도메인으로 온 메일을 어떻게 확인할지(SPF·DKIM 정렬), 실패하면 어떻게 할지(p=), 그리고 결과를 알려 달라(rua=). 이 글은 세 번째입니다.

rua를 적어 두면 메일을 받는 쪽(Mail Receiver)이 정해진 기간(보통 UTC 하루)의 결과를 모아 XML로 보내 줍니다. 그 안에는 메일 본문이 없고, 보낸 IP와 개수와 인증 결과만 있습니다. 10편에서 본 이메일 IP 평판이 "남이 내 IP를 어떻게 보나"였다면, 집계 보고서는 "누가 내 이름을 쓰고 있나"를 받는 쪽이 대신 세어 준 것입니다.

DMARCeye 용어집은 자사 서비스가 DMARC 집계 보고서의 발송 IP를 전 세계 평판 데이터·차단 목록과 엮어 의심스럽거나 등재된 IP를 표시한다고 설명합니다. 이 글의 실습은 그 뼈대를 로컬에서 작게 재현한 것입니다.

기준 문서가 바뀌었다: RFC 7489 → RFC 9989·9990·9991

2015년 정보성 문서 RFC 7489 하나가 2026-05 표준 트랙 문서 셋으로 나뉨. RFC 9989 본체(정책·정렬·트리 워크, np·psd·t 추가, pct·rf·ri 제거), RFC 9990 집계 보고서(이름공간 dmarc-2.0, DKIM selector 필수, extension, 최소 하루 한 번), RFC 9991 실패 보고서

왼쪽이 옛 문서, 오른쪽 셋이 새 문서입니다. 레코드의 버전 표시는 여전히 v=DMARC1입니다.

기획서는 "RFC 7489 기준"으로 적었지만, 원문을 확인해 보니 2026년 5월에 DMARC가 새 문서로 바뀌었습니다(RFC Editor 확인).

문서제목상태
RFC 9989DMARC 본체Proposed Standard, 2026-05. RFC 7489와 RFC 9091을 대체
RFC 9990DMARC Aggregate Reporting(집계 보고서)Proposed Standard, 2026-05. RFC 7489를 대체
RFC 9991DMARC Failure Reporting(실패 보고서)Standards Track, 2026-05. RFC 7489를 대체, RFC 6591 갱신(이 시리즈에서는 다루지 않음)

RFC 7489(2015)는 "정보성(Informational)" 문서였고, 새 문서들은 표준 트랙입니다. 보고서를 읽는 사람에게 중요한 변화는 이렇습니다.

  • 태그: np(없는 하위 도메인 정책), psd, t(시험 모드)가 생기고, pct·rf·ri(보고 간격)가 빠졌습니다. 10편 실습이 pct를 경고한 이유입니다.
  • 보고서 XML: 이름공간 urn:ietf:params:xml:ns:dmarc-2.0이 생겼고, DKIM 결과에 selector가 필수가 됐고, extension 자리가 생겼습니다.
  • 보고 주기: "하루 한 번 또는 더 자주"의 보고서. 보고 기간은 보통 0시 UTC에 시작하는 하루입니다.

현실의 우편함에는 한동안 두 형식이 섞여 들어올 것입니다. 이 글의 파서는 둘 다 읽게 만들었습니다.

보고서 XML의 뼈대

feedback 아래 version, report_metadata(org_name·email·report_id·date_range), policy_published(domain·adkim·aspf·p·sp, 새로 np·testing·discovery_method), extension(새로), record 여러 개. record는 row(source_ip·count·policy_evaluated), identifiers(header_from·envelope_from), auth_results(dkim selector 필수·spf). 노란 칠은 RFC 9990에서 새로 생긴 것

들여쓰기가 XML의 포함 관계입니다. 노란 칠은 RFC 7489에 없고 RFC 9990에서 생긴 것입니다. 아래 두 상자는 자주 헷갈리는 값입니다.

보고서 하나는 정책 도메인 하나, 기간 하나입니다. 그 안의 record 한 줄은 "이 IP에서, 이런 결과 조합으로, 몇 통"입니다. 같은 IP라도 결과가 다르면 줄이 나뉩니다. 실습의 수신자 B 보고서(RFC 9990 형식, gzip)를 풀어 보면 이렇습니다.

실제 출력(2026-10-31, gzip -dc 앞 20줄 중 일부):

<feedback xmlns="urn:ietf:params:xml:ns:dmarc-2.0">
  <version>1.0</version>
  <report_metadata>
    <org_name>Receiver B</org_name>
    <email>dmarc-reports@receiver-b.example</email>
    <report_id>b-7f3a9c2e</report_id>
    <date_range><begin>1793232000</begin><end>1793318399</end></date_range>
  </report_metadata>
  <policy_published>
    <domain>good.mail.lab.example</domain><adkim>r</adkim><aspf>r</aspf><p>quarantine</p><sp>quarantine</sp><np>none</np><testing>n</testing><discovery_method>treewalk</discovery_method>
  </policy_published>
  <record>
    <row>
      <source_ip>192.0.2.10</source_ip>
      <count>640</count>
      <policy_evaluated><disposition>pass</disposition><dkim>pass</dkim><spf>pass</spf></policy_evaluated>

date_range는 유닉스 시각(UTC)입니다. 1793232000은 2026-10-29 0시 UTC입니다. 첨부 파일 이름도 규칙이 있습니다. receiver-b.example!good.mail.lab.example!1793232000!1793318399.xml.gz처럼 수신자!정책 도메인!시작!끝입니다(RFC 9990 3.5.2절).

disposition은 받는 쪽이 실제로 한 처리입니다. RFC 9990은 pass(강제 정책 아래에서 통과)를 새로 두었고, RFC 7489 보고서는 통과한 메일도 none으로 적습니다. 그래서 통과 여부는 disposition이 아니라 policy_evaluated의 dkim·spf로 셉니다.

통과와 정렬은 다르다

identifiers의 header_from(good.mail.lab.example)과 auth_results의 spf(bounce.esp-mailer.example = pass, From과 조직 도메인이 달라 정렬 안 됨), dkim(d=good.mail.lab.example s1 = pass, 정렬) → policy_evaluated(dkim=pass, spf=fail, 둘 중 하나만 pass면 DMARC 통과)

왼쪽이 받는 사람이 보는 보낸 사람, 가운데가 날것의 인증 결과, 오른쪽이 수신자가 정렬까지 따진 결과입니다. 아래 띠는 실습 데이터의 실제 경우입니다.

보고서에는 인증 결과가 두 군데 있습니다.

  • auth_results: SPF·DKIM이 각자 확인한 날것의 결과. SPF는 봉투 발신자(envelope_from, 반송 주소) 도메인, DKIM은 서명한 도메인(d=)을 봅니다.
  • policy_evaluated: 그 결과가 From 도메인과 정렬됐는지까지 본 결과. DMARC 판정은 이쪽입니다.

뉴스레터 업체(ESP)를 쓰면 흔히 그림 같은 모양이 나옵니다. 반송 주소가 업체 도메인이라 SPF는 통과해도 정렬이 안 됩니다. 업체가 우리 도메인 키로 DKIM 서명을 해 주면 DKIM이 정렬되고, 둘 중 하나가 통과했으니 DMARC 통과입니다. 조직 도메인 판단(옛 방식은 공개 접미사 목록, RFC 9989는 DNS 트리 워크)은 수신자가 합니다. 그래서 판정은 policy_evaluated를 믿고, auth_results는 실패 원인을 찾을 때 씁니다.

실습: 가짜 보고서 네 개 → IP별 표

lab/email/dmarc/에 재료가 있습니다. 모든 IP는 문서용, 도메인은 *.example입니다.

  • make_fake_reports.py: 2026-10-28~30의 가짜 보고서 네 개. 수신자 A·C는 RFC 7489 형식(이름공간 없음), B는 RFC 9990 형식에 gzip
  • known_senders.json: 발송처 대장. 우리 도메인으로 보내도 되는 곳의 목록입니다. 사내 메일 서버 두 대, 옛 알림 서버, 뉴스레터 업체의 대역
  • parse_dmarc.py: 보고서를 모두 읽어 IP별로 합치고, 대장과 맞대고, 경보를 내고, JSON을 씁니다

파서는 받은 XML을 남이 만든 입력으로 다룹니다. 압축을 풀고, 크기를 제한하고, 이름공간을 떼어 두 형식을 같은 방식으로 읽습니다. 실습은 표준 라이브러리 파서를 썼지만, 운영에서는 외부 엔터티와 압축 폭탄을 막는 파서(defusedxml 등)와 압축 해제 크기 제한을 둡니다. RFC 9990도 8.1절에서 보고서 자체가 공격 수단이 될 수 있다고 적습니다.

cd ip-reputation/lab/email
../.venv/bin/python fake_dns.py --port 18630 &        # 10편의 로컬 DNSBL(교차 확인용)
cd dmarc
../../.venv/bin/python make_fake_reports.py
../../.venv/bin/python parse_dmarc.py --dnsbl-zone bl.lab.example
kill %1                                               # 로컬 DNS 서버 끄기

실제 출력(2026-10-31, macOS, Python 3.12.12):

보고서 4개
  Receiver A Mail  RFC 7489  2026-10-28  rows=4  p=quarantine  id=a-20261028-001
  Receiver A Mail  RFC 7489  2026-10-29  rows=4  p=quarantine  id=a-20261029-001
  Receiver B       RFC 9990  2026-10-29  rows=4  p=quarantine  id=b-7f3a9c2e
  Receiver C       RFC 7489  2026-10-30  rows=4  p=quarantine  id=c-2026-10-30-17

source_ip       known_as          msgs   pass   fail   rate  dkim_al  spf_al  status
203.0.113.66    -                 2762      0   2762     0%        0       0  unknown_fail
203.0.113.67    -                   88      0     88     0%        0       0  unknown_fail
198.51.100.200  -                   35      0     35     0%        0       0  unknown_fail_forwarded
198.51.100.77   -                  210    210      0   100%      210       0  unknown_pass
192.0.2.10      own-mta-1         3038   3035      3   100%     3035    3035  authorized_with_failures
192.0.2.11      own-mta-2          779    779      0   100%      779     779  authorized_pass
192.0.2.12      legacy-app          60     60      0   100%        0      60  authorized_pass
198.51.100.5    newsletter-esp    9710   9710      0   100%     9710       0  authorized_pass

경보
  [HIGH  ] 203.0.113.66    unknown_fail             2762통, 수신자 3곳, 2026-10-29~2026-10-30  reasons=['unknown_sender', 'dmarc_fail_rate=100%', 'seen_by_3_receivers', 'unknown_neighbors_in_/24=1', 'dnsbl_listed:bl.lab.example']
  [HIGH  ] 203.0.113.67    unknown_fail             88통, 수신자 1곳, 2026-10-30~2026-10-30  reasons=['unknown_sender', 'dmarc_fail_rate=100%', 'unknown_neighbors_in_/24=1']
  [MEDIUM] 198.51.100.77   unknown_pass             210통, 수신자 1곳, 2026-10-29~2026-10-29  reasons=['unknown_sender', 'passes_dmarc_check_who_holds_key', 'unknown_neighbors_in_/24=1']
  [MEDIUM] 192.0.2.10      authorized_with_failures 3038통, 수신자 2곳, 2026-10-28~2026-10-29  reasons=['dmarc_fail=3', 'seen_by_2_receivers']

wrote out/dmarc-senders.json

실습 결과 표: 203.0.113.66(대장에 없음, 2762통 모두 실패, 수신자 3곳, DNSBL 등재), 203.0.113.67(88통 실패, 같은 /24), 198.51.100.200(35통 실패, 전달), 198.51.100.77(210통 통과, 대장에 없음), 192.0.2.10(own-mta-1, 3통 실패), 192.0.2.11·192.0.2.12·198.51.100.5(대장에 있고 모두 통과)

위 출력을 표로 옮긴 그림입니다. 위 네 줄(레드·노랑)이 경보 대상, 아래 그린 줄은 대장에 있고 통과한 발송처입니다.

표를 읽어 봅니다.

  • 203.0.113.66: 대장에 없고, 이틀 동안 2,762통이 모두 실패했고, 수신자 세 곳이 모두 봤습니다. 10편의 로컬 DNSBL에도 올라 있습니다. 장면의 청구서 메일은 여기서 나갔을 가능성이 큽니다. p=quarantine 덕에 받는 쪽이 격리했습니다(dispositions: quarantine 2762).
  • 203.0.113.67: 같은 /24에서 나온 또 하나의 모르는 IP입니다. 사칭 발송자가 이웃 IP를 함께 쓰는 흔한 모양입니다.
  • 198.51.100.200: 실패지만 수신자가 mailing_list로 표시했습니다. 누군가 우리 메일을 메일링 리스트로 다시 보낸 것으로 보입니다. 경보 단계가 낮습니다.
  • 198.51.100.77: 대장에 없는데 210통이 모두 통과했습니다. 선택자 crm1로 우리 도메인 DKIM 서명이 붙었습니다. 마케팅팀이 새 CRM 서비스를 붙였을 수 있습니다. 정상일 수도 있지만, 대장에 없는 곳이 우리 키를 쓰고 있다는 사실은 반드시 확인합니다.
  • 192.0.2.10: 우리 서버인데 하루 3통이 실패했습니다. 서명 없이 나간 메일이 있다는 뜻입니다.
  • 192.0.2.12, 198.51.100.5: 통과했지만 한쪽 정렬이 0입니다. 옛 알림 서버는 DKIM 서명을 아직 안 하고(대장에 적힌 알려진 문제), ESP는 SPF 정렬이 안 됩니다. 지금은 통과지만, 한쪽만 믿고 있다는 기록입니다.

모르는 발송처 가리기

IP 하나를 다섯 칸 중 하나에 넣는 규칙. 발송처 대장에 있으면 authorized_pass(모두 통과) 또는 authorized_with_failures(실패 있음, 설정 확인). 대장에 없으면 unknown_pass(90% 이상 통과, 누가 키를 쓰나), unknown_fail_forwarded(실패, 전달 표시), unknown_fail(실패, 사칭 의심, 경보). 덧붙이는 이유: 수신자 여러 곳, 같은 /24, 로컬 DNSBL 등재, DKIM 정렬 0건

위 검은 띠 두 개가 첫 질문(대장에 있나)과 두 번째 질문(통과율은)입니다. 그 아래 다섯 상자가 결과 칸입니다. 90%와 대장은 글쓴이가 정한 실습 값입니다.

규칙의 핵심은 두 가지입니다.

  1. 기준은 "대장"이다. 보고서는 무엇이 정상인지 모릅니다. 우리 도메인으로 보내도 되는 IP·대역 목록이 있어야 "모르는 발송처"를 말할 수 있습니다. 대장이 없으면 이 실습의 절반이 사라집니다.
  2. 통과를 정상으로 넘기지 않는다. 통과는 "우리 이름의 키나 SPF가 쓰였다"는 뜻이지 "우리가 보냈다"는 뜻이 아닙니다. 대장에 없는 통과(unknown_pass)는 사칭보다 더 조용한 사고일 수 있습니다. 예를 들어 퇴사한 담당자가 만든 SaaS 계정이 아직 우리 키로 서명하고 있는 경우입니다.

reasons에는 점수 대신 근거를 문자열로 남깁니다. 수신자 몇 곳이 봤는지, 같은 /24에 모르는 IP가 더 있는지, 로컬 DNSBL에 올라 있는지. 이 근거 목록을 13편이 다른 평판 소스와 합칩니다.

13편으로 넘기기

글쓴이가 추정한 부분이 들어간 그림. out/dmarc-senders.json의 senders["203.0.113.66"](status·severity·reasons·first/last_seen) → 13편 어댑터가 내 도메인 기준 신호로 읽어 Talos·Criminal IP·DNSBL과 같은 IP로 합침(제안) → 근거 묶음(unknown_fail, dnsbl_listed, seen_by_3_receivers). 할 일은 IP 차단이 아니라 p=reject 준비, 사칭 신고, 모르는 SaaS 정리

왼쪽 흰 상자가 이 편의 실제 결과, 가운데와 오른쪽 노란 점선 상자가 13편과의 연동 방식에 대한 글쓴이의 제안입니다.

파서가 쓴 JSON은 IP로 바로 찾을 수 있게 senders를 IP 키의 사전으로 둡니다. 13편 평판 모으기 서비스가 이 파일을 읽습니다.

실제 출력(2026-10-31, out/dmarc-senders.json의 203.0.113.66 항목):

"203.0.113.66": {
  "status": "unknown_fail",
  "severity": "high",
  "known_as": null,
  "messages": 2762,
  "dmarc_pass": 0,
  "dmarc_fail": 2762,
  "pass_rate": 0.0,
  "first_seen": "2026-10-29",
  "last_seen": "2026-10-30",
  "receivers": ["Receiver A Mail", "Receiver B", "Receiver C"],
  "dispositions": {"quarantine": 2762},
  "unknown_neighbors_24": ["203.0.113.67"],
  "reasons": ["unknown_sender", "dmarc_fail_rate=100%", "seen_by_3_receivers", "unknown_neighbors_in_/24=1", "dnsbl_listed:bl.lab.example"],
  "dnsbl": {"zone": "bl.lab.example", "zone_healthy": true, "verdict": "LISTED", "codes": ["127.0.0.4", "127.0.0.2"]}
}

(가독성을 위해 줄을 합치고 dkim_aligned 등 몇 칸을 뺐습니다. 원문은 lab/results/11-dmarc-parse.txt)

여기서 오해하기 쉬운 점이 있습니다. 이 IP를 내 방화벽에서 막아도 소용없습니다. 사칭 메일은 내 서버를 지나지 않고 공격자 IP에서 고객의 메일함으로 바로 갑니다. 할 일은 다른 곳에 있습니다.

  • 정상 발송처가 모두 통과하는지 확인한 뒤 p=quarantine을 p=reject로 올릴 준비를 합니다.
  • 고객에게 사칭 주의를 알리고, 필요하면 해당 IP의 호스팅 업체 abuse 창구에 신고합니다. 신고는 내 도메인 사칭에 대한 것만, 손으로 합니다.
  • unknown_pass(198.51.100.77)의 주인을 찾아 대장에 넣거나 키를 회수합니다.

평판 모으기 서비스에서 이 결과가 쓰이는 곳은 "이 IP가 우리 고객 로그인에도 나타나는가" 같은 교차 확인입니다. 사칭 메일을 보낸 대역이 로그인 시도에도 보이면, 피싱으로 얻은 계정을 시험하는 중일 수 있습니다.

보고서가 보여 주지 않는 것

시간선: 10-29 09:00 사칭 메일 시작, 10-29 24:00 UTC 수신자 하루 집계 끝, 그다음 날 보고서 도착과 경보. 아래 세 상자: 늦다(UTC 하루 단위, 실시간 신호 아님), 일부만(rua를 지원하는 수신자만), 내용 없음(제목·본문 없음)

위 시간선은 하루 단위 보고가 얼마나 늦는지를 보여 주는 예입니다(시각은 설명용). 아래 세 상자가 이 편의 한계입니다.

오탐과 한계

  • 늦다. 보고 기간이 보통 UTC 하루이고, 보고서는 그 뒤에 옵니다. 사칭이 시작된 지 하루가 지나서야 알게 됩니다. 실시간 차단 신호가 아닙니다.
  • 보내 주는 곳만 보인다. rua를 지원하는 수신자만 보고서를 보냅니다. 보고서를 안 보내는 메일함으로 간 사칭은 이 표에 없습니다.
  • 내용이 없다. 제목·본문·링크가 없습니다. 무엇을 사칭했는지는 고객 신고나 실패 보고서(RFC 9991)로 봅니다.
  • 전달이 실패를 만든다. 메일링 리스트와 전달은 정상 메일도 실패로 만듭니다. reason의 mailing_list·trusted_forwarder를 따로 다룹니다.
  • 수신자마다 다르다. 조직 도메인 판단, 보고서를 쪼개는 방식, disposition 표기가 수신자와 형식(RFC 7489/9990)마다 다릅니다. 같은 날 같은 IP가 수신자마다 다른 줄로 나옵니다.
  • 대장이 낡으면 오탐이 된다. 새 서비스를 붙였는데 대장을 안 고치면 정상 발송처가 매일 경보로 뜹니다. 대장 변경을 발송 시스템 변경 절차에 넣습니다.
  • 보고서도 개인정보를 담을 수 있다. 보고서에는 발송 IP와 도메인이 들어 있습니다. RFC 9990 7절은 보고서 수신자와 데이터의 개인정보 문제를 따로 다룹니다. 외부 업체에 rua를 맡길 때는 처리 위탁 여부를 확인합니다(법률 자문 아님).

실무 체크

  • rua 우편함을 파서에 연결하고, 두 형식(RFC 7489·9990)과 gzip·zip 첨부를 모두 받게 한다. 파서는 크기 제한과 안전한 XML 파서를 쓴다.
  • 발송처 대장(IP·대역·DKIM 선택자)을 만들고, 대장에 없는 IP는 통과든 실패든 경보로 본다.
  • 모든 정상 발송처가 통과하는 것을 몇 주 확인한 뒤 p=reject로 올린다. 사칭 IP는 막는 대상이 아니라 정책과 신고의 근거다.

정리

  • DMARC 집계 보고서는 받는 쪽이 "당신 도메인 이름으로 어떤 IP가 몇 통을 보냈고 결과는 어땠나"를 하루 단위로 알려 주는 XML이다.
  • 2026년 5월 RFC 9989·9990·9991이 RFC 7489를 대신했다. 보고서에는 이름공간이 생겼고 pct·ri 같은 태그가 빠졌다. 한동안 두 형식이 섞여 온다.
  • 판정은 policy_evaluated로 세고, 발송처 대장과 맞대 모르는 발송처를 가린다. 대장에 없는 통과도 경보다.
  • 결과는 IP를 키로 한 근거 묶음 JSON으로 남겨 13편 평판 모으기 서비스가 다른 소스와 합친다.

출처와 확인 날짜

모든 링크는 2026-10-31에 확인했습니다.


이전 글 이메일 발송 IP 평판: 무엇을 보고 점수를 매기나
다음 글 도입 체크리스트: 고를 때 물어야 할 여덟 가지
시리즈 전체 목록 IP 평판은 어떻게 만들어지고, 어떻게 써야 하나 전체 목록

profile
고인물 개발자(OG)

0개의 댓글