DMARC로 이메일 발송 변조를 예방하기

안녕하세요?
오늘은 DMARC, SPF를 사용하여
이메일 발송 관련해서 변조하는 공격 패턴을 방지하는 방법을 알아보도록 하겠습니다.

DNS 기반 이메일 보안 메커니즘을 실무 관점에서 정리한 가이드입니다.
대상 독자: 클라우드 인프라 엔지니어, DevOps, 도메인/메일 운영 담당자
01. 배경
- 이메일은 기본적으로 "신뢰를 전제하지 않는" 프로토콜이다
SMTP(Simple Mail Transfer Protocol)는 1982년에 설계되었습니다.
당시에는 보안보다 연결성이 우선이었기 때문에, 발신자를 검증하는 메커니즘이 프로토콜에 내장되지 않았습니다.
즉, 누구든지 기술적으로는 아래처럼 From: 헤더를 위조할 수 있습니다.
From: no-reply@trusted-bank.com
To: victim@example.com
Subject: 긴급 계정 확인 요청
이 구조적 취약점을 악용한 것이 피싱(Phishing) 과 스푸핑(Spoofing) 입니다.
| 위협 유형 | 설명 | 예시 |
| 도메인 스푸핑 | 신뢰 도메인 위조 | @samsung.com 사칭 발신 |
| 피싱 | 위조 메일로 자격증명 탈취 | 가짜 로그인 링크 유도 |
| BEC (Business Email Compromise) | 임원 사칭 송금 지시 | CEO/CFO 사칭 |
| 이메일 중간자 변조 | 전송 중 내용 수정 | 계좌번호 교체 |
이 네 가지 위협을 각각 대응하기 위해 등장한 것이 SPF, DKIM, DMARC 입니다.
02. TXT 레코드
- TXT 레코드는 DNS의 범용 텍스트 저장소입니다.
- 원래는 사람이 읽는 메모 용도로 설계되었지만, 현재는 다음 용도로 광범위하게 사용됩니다.
| 용도 | 예시 값 |
| SPF 발신 서버 선언 | v=spf1 include:... |
| DKIM 공개키 등록 | v=DKIM1; k=rsa; p=MIGf... |
| 도메인 소유권 인증 | google-site-verification=... |
| DMARC 정책 선언 | v=DMARC1; p=reject; rua=... |
중요한 특성
- 하나의 도메인에 TXT 레코드를 여러 개 등록할 수 있습니다.
- 단, SPF는 도메인당 하나만 유효합니다(여러 개 있으면 PermError 발생).
- TXT 레코드 자체는 단순한 문자열 컨테이너이며, 보안 기능은 그 값을 해석하는 수신 서버 측에서 처리합니다.
2-1. SPF 란?
"우리 도메인을 대신해서 메일을 보낼 수 있는 서버(IP)가 어디인지" DNS에 공개 선언하는 메커니즘
RFC 7208로 표준화되어 있으며, 수신 서버가 발신 IP를 DNS에서 조회해 허가 여부를 판단합니다.
📌 동작 원리
발신 흐름:
성환12 서버 (IP: 203.0.x.x) → From: notice@ssunghwan.tistory.com 로 메일 발송
수신 서버 검증 흐름:
1. From 헤더의 도메인 추출: ssunghwan.tistory.com
2. DNS 조회: ssunghwan.tistory.com TXT 레코드
3. "v=spf1 include:spf1.성환12.com ~all" 발견
4. spf1.성환12.com 내부에 203.0.x.x 포함 여부 확인
5. 포함되면 → SPF PASS
포함 안되면 → SPF SOFTFAIL (~all) 또는 HARDFAIL (-all)
📌 SPF 레코드 문법
v=spf1 [메커니즘...] [all]
📌 주요 메커니즘:
| 메커니즘 | 설명 | 예시 |
| include: | 외부 SPF 레코드 참조 | include:spf1.성환12.com |
| ip4: | 특정 IPv4 허가 | ip4:203.0.113.0/24 |
| ip6: | 특정 IPv6 허가 | ip6:2001:db8::/32 |
| a: | 해당 도메인 A 레코드 IP 허가 | a:mail.example.com |
| mx: | MX 레코드 IP 허가 | mx |
Qualifier(판정):
| 기호 | 의미 | 결과 |
| + (기본값) | Pass | 정상 발신 |
| ~ | SoftFail | 의심, 스팸 마킹 |
| - | Fail | 명시적 거부 |
| ? | Neutral | 판정 보류 |
실제 예시 (all 수식어 비교):
# 느슨한 설정 (초기 단계 권장)
v=spf1 include:spf1.성환12.com ~all
# 엄격한 설정 (정착 후 권장)
v=spf1 include:spf1.성환12.com -all
📌 SPF의 한계
- SPF는 봉투 발신자(Envelope From, Return-Path) 의 IP만 검증합니다.
- 사용자가 메일 클라이언트에서 보는 From: 헤더(Header From)와는 별개입니다.
이 간극을 이용한 공격(Display Name Spoofing)은 SPF만으로 막을 수 없으며,
DMARC의 Alignment 검사 가 이를 보완합니다.
📌 DNS Lookup 제한
SPF 평가 시 include:, a:, mx: 등의 DNS 조회는 최대 10회 로 제한됩니다(RFC 7208 §4.6.4).
이를 초과하면 PermError 처리되어 SPF 검증이 실패합니다.
# 위험 예시 - lookup이 10회를 초과할 수 있음
v=spf1 include:A include:B include:C include:D include:E
include:F include:G include:H include:I include:J ~all
2-2. DKIM 란?
"이 메일이 우리 도메인에서 서명되었고, 전송 중 내용이 변조되지 않았다" 는 것을 공개키 암호화로 증명!
RFC 6376으로 표준화되어 있으며, 비대칭 암호화(공개키/개인키 쌍)를 사용합니다.
📌 동작 원리
발신 측 (성환12 서버):
1. 메일 헤더 + 본문을 해시
2. 성환12가 보유한 개인키(private key)로 서명
3. 서명값을 DKIM-Signature 헤더에 첨부해 발송
DKIM-Signature: v=1; a=rsa-sha256; d=ssunghwan.tistory.com;
s=성환12; h=from:to:subject:date;
bh=base64(body hash); b=base64(signature)
수신 측 (Gmail 등):
1. DKIM-Signature 헤더에서 d=도메인, s=셀렉터 추출
2. DNS 조회: 성환12._domainkey.ssunghwan.tistory.com
3. 공개키(public key) 획득
4. 공개키로 서명 검증
5. 검증 성공 → DKIM PASS / 실패 → DKIM FAIL
📌 DKIM DNS 레코드 구조
레코드 이름 형식: {셀렉터}._domainkey.{도메인}
; 레코드 이름
성환12._domainkey.ssunghwan.tistory.com
; 레코드 값
v=DKIM1; k=rsa; p=ABCDEFGHIJKLMNOP...
| 필드 | 의미 |
| v=DKIM1 | 버전 |
| k=rsa | 키 알고리즘 |
| p=... | Base64 인코딩된 공개키 |
셀렉터(Selector)의 의미
셀렉터는 하나의 도메인이 여러 개의 DKIM 키를 동시에 운영할 수 있게 해주는 식별자입니다.
wiseu._domainkey.purinapetcare.co.kr → mnwise/WiseuCloud용 키
ds._domainkey.purinapetcare.co.kr → DirectSend용 키
google._domainkey.purinapetcare.co.kr → Google Workspace용 키
이 구조 덕분에 서비스별로 DKIM 키를 독립적으로 관리하고 교체할 수 있습니다.
📌 SPF와 DKIM의 결정적 차이
| 구분 | SPF | DKIM |
| 무엇을 검증 | 발신 서버 IP | 메일 헤더 + 본문 무결성 |
| 변조 감지 | 불가능 | 가능 |
| 포워딩 시 | 중계 서버 IP 변경으로 FAIL | 서명은 유지되므로 PASS |
| 메일 본문 보호 | 없음 | 있음 |
2-3. DMARC 란?
"SPF와 DKIM 검증 결과를 종합해, 실패 시 수신 서버가 어떻게 처리할지 정책을 선언하고 리포트를 받는" 메커니즘
RFC 7489로 표준화되어 있으며, SPF와 DKIM 위에 얹히는 정책 레이어 입니다.
DMARC가 추가하는 핵심 개념: Alignment
- SPF와 DKIM은 각각 독립적으로 검증됩니다.
- 그런데 이 둘이 PASS여도 Header From 도메인과 일치하지 않으면 스푸핑일 수 있습니다.
하지만, DMARC의 Alignment 검사가 이를 잡아냅니다.
SPF Alignment: Return-Path 도메인 == Header From 도메인?
DKIM Alignment: DKIM-Signature의 d= 도메인 == Header From 도메인?
둘 중 하나라도 Alignment 통과 + 해당 인증 PASS
→ DMARC PASS
둘 다 실패
→ DMARC FAIL → p= 정책 적용
📌 DMARC 레코드 문법
레코드 이름: _dmarc.{도메인} (항상 이 형식)
_dmarc.ssunghwan.tistory.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@ssunghwan.heo@gmail.com"
주요 태그:
| 태그 | 의미 | 값 예시 |
| v= | 버전 (필수) | DMARC1 |
| p= | 정책 (필수) | none, quarantine, reject |
| sp= | 서브도메인 정책 | none, quarantine, reject |
| rua= | 집계 리포트 수신 주소 | mailto:dmarc@example.com |
| ruf= | 포렌식 리포트 수신 주소 | mailto:forensic@example.com |
| pct= | 정책 적용 비율 (%) | 100 |
| adkim= | DKIM Alignment 강도 | r(relaxed), s(strict) |
| aspf= | SPF Alignment 강도 | r(relaxed), s(strict) |
정책 단계별 의미:
p=none → 아무것도 하지 않음. 리포트만 받음 (모니터링 단계)
p=quarantine → 실패 메일을 스팸함으로 격리
p=reject → 실패 메일을 완전 거부 (가장 강력)
📌 DMARC 도입 권장 단계
1단계: p=none; rua=mailto:dmarc@your-domain.com
→ 2~4주 리포트 수집, 정상 발신 경로 파악
2단계: p=quarantine; pct=10
→ 10%만 격리 적용, 점진적 강화
3단계: p=quarantine; pct=100
→ 전체 격리 적용
4단계: p=reject; pct=100
→ 완전 차단 (최종 목표)
03. 세 가지 레코드의 관계
📌 정리
- SPF : 발신 IP 확인 및 허가 여부 판정
- DKIM : 서명 검증 및 무결성 확인
- DMARC : SPF + DKIM 종합하여 Alignment 검사, 정책에 따라 집행
📌 DMARC 판정 매트릭스
| PASS (Aligned) | PASS (Aligned) | ✅ PASS | 이상적 상태 |
| PASS (Aligned) | FAIL | ✅ PASS | SPF 하나로 충분 |
| FAIL | PASS (Aligned) | ✅ PASS | DKIM 하나로 충분 |
| FAIL | FAIL | ❌ FAIL | p= 정책 실행 |
| PASS (Not Aligned) | FAIL | ❌ FAIL | Alignment 실패 |
각각이 막는 위협
| 위협 | SPF | DKIM | DMARC |
| 외부 IP 스푸핑 | ✅ 차단 | ❌ | ✅ (종합 판정) |
| 메일 내용 변조 | ❌ | ✅ 감지 | ❌ |
| Header From 위조 | ❌ | ❌ | ✅ Alignment |
| 포워딩 메일 오탐 | ❌ (오탐 발생) | ✅ 견고 | 부분적 |
04. 실무 설정 예시
📌 다중 발송 서비스 SPF 통합
여러 이메일 서비스를 쓸 때 SPF는 반드시 하나의 레코드에 통합해야 합니다.
; ❌ 잘못된 설정 - 두 개의 SPF가 있으면 PermError
ssunghwan.tistory.com TXT "v=spf1 include:spf1.성환12.com ~all"
ssunghwan.tistory.com TXT "v=spf1 include:domain.com ~all"
; ✅ 올바른 설정 - 하나의 레코드에 모두 통합
ssunghwan.tistory.com TXT "v=spf1 include:spf1.성환12.com include:domain.com ~all"
📌 서비스별 DKIM 레코드 구성
; mnwise/WiseuCloud용 DKIM
wiseu._domainkey.ssunghwan.tistory.com TXT "v=DKIM1; k=rsa; p=ABCDEFG..."
; DirectSend용 DKIM
ds._domainkey.ssunghwan.tistory.com CNAME dkim.directsend.co.kr
; Google Workspace용 DKIM (설정 시)
google._domainkey.ssunghwan.tistory.com TXT "v=DKIM1; k=rsa; p=GFEDCBA..."
📌 DMARC 설정
; 초기 모니터링 단계
_dmarc.ssunghwan.tistory.com TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@ssunghwan.heo@gmail.com"
; 강화 단계
_dmarc.ssunghwan.tistory.com TXT "v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc-reports@ssunghwan.heo@gmail.com; adkim=r; aspf=r"
05. 추가 권장 항목
SPF/DKIM/DMARC 외에 이메일 및 도메인 보안을 강화하는 추가 메커니즘들입니다.
MTA-STS (Mail Transfer Agent Strict Transport Security)
"우리 메일 서버로의 연결은 반드시 TLS로만 허용한다"
SMTP는 기본적으로 평문 통신이 가능합니다. MTA-STS는 발신 서버에게 TLS 강제를 선언하여 중간자 공격(MITM)을 방어합니다.
; MTA-STS 정책 위치 선언
_mta-sts.ssunghwan.tistory.com TXT "v=STSv1; id=20240101"
; 실제 정책은 HTTPS로 제공
; https://mta-sts.ssunghwan.tistory.com/.well-known/mta-sts.txt
version: STSv1
mode: enforce
mx: mail.ssunghwan.tistory.com
max_age: 86400
TLS-RPT (TLS Reporting)
"TLS 연결 실패를 리포트로 받는다"
MTA-STS와 함께 사용하며, TLS 협상 실패 시 알림을 받을 수 있습니다.
_smtp._tls.ssunghwan.tistory.com TXT "v=TLSRPTv1; rua=mailto:tls-reports@ssunghwan.heo@gmail.com"
BIMI (Brand Indicators for Message Identification)
"DMARC p=reject 도달 후, 수신함에 브랜드 로고를 표시한다"
Gmail, Yahoo 등 주요 메일 클라이언트에서 발신자 아이콘 자리에 공식 로고를 표시합니다. DMARC p=reject 설정이 선행 조건입니다.
default._bimi.ssunghwan.tistory.com TXT "v=BIMI1; l=https://ssunghwan.tistory.com/logo.svg; a=https://ssunghwan.tistory.com/vmc.pem"
- l=: SVG 형식 로고 URL
- a=: VMC(Verified Mark Certificate) - CA로부터 발급받는 브랜드 인증서 (유료)
CAA (Certification Authority Authorization)
"우리 도메인의 SSL/TLS 인증서는 특정 CA만 발급할 수 있다"
이메일과 직접 관련은 없지만, 도메인 보안의 일환으로 도메인 하이재킹 및 인증서 오발급을 방어합니다.
ssunghwan.tistory.com CAA 0 issue "amazon.com"
ssunghwan.tistory.com CAA 0 issue "letsencrypt.org"
ssunghwan.tistory.com CAA 0 issuewild ";" -- 와일드카드 발급 금지
SPF Macro (vali.email 방식)
일부 글로벌 표준에서 사용하는 동적 SPF 평가 방식입니다.
발신 IP, 도메인 등을 변수로 활용해 더 정밀한 허가 목록을 구성할 수 있습니다.
; 매크로 방식 예시
v=SPF1 include:%{i}._ip.%{h}._ehlo.%{D}._spf.vali.email ~All
; %{i} = 발신 IP
; %{h} = EHLO 도메인
; %{D} = 헤더 From 도메인
긴 글 읽어주셔서 감사합니다.
참고 RFC: RFC 7208 (SPF), RFC 6376 (DKIM), RFC 7489 (DMARC), RFC 8461 (MTA-STS), RFC 8460 (TLS-RPT)