개발
home
✉️

Cloudflare Email Routing 원리 - MX/SPF/DKIM과 SRS 봉투 재작성

Created
2026/08/17
Tags
Domain
2026-08-17

개요

leedo.mecontact@ 수신 주소를 만들면서 알게 된 것을 정리합니다.
메일함을 하나 만든 게 아닙니다. contact@leedo.me에는 메일함도 비밀번호도 없고, DNS 레코드 3종(MX, SPF, DKIM)과 전달 규칙 하나만 있습니다. Cloudflare 수신 서버가 메일을 받아서 저장 없이 Gmail로 재전송할 뿐인 순수 릴레이입니다.
구성해놓고 테스트 메일을 보냈더니 Gmail 상세 보기에 발송 도메인과 인증기관이 서로 다른 도메인으로 찍혀 있었습니다. 왜 이렇게 나뉘는지 파보다가 SPF와 SRS(Sender Rewriting Scheme)까지 내려갔습니다.
SPF(Sender Policy Framework)는 도메인 주인이 "이 도메인 이름으로 메일을 보낼 수 있는 서버는 이 목록뿐"이라고 DNS에 공표해두고, 받는 서버가 지금 접속한 IP를 그 목록과 대조하는 검사입니다. 남의 도메인을 사칭하는 스팸을 걸러내는 장치입니다.
그런데 포워딩은 원발신자 이름을 달고 엉뚱한 서버가 대신 보내는 행위입니다. 사칭과 구분이 안 되니 그대로는 검사를 통과하지 못합니다. 이 충돌을 SRS로 풀어냅니다.
SMTP의 봉투(envelope)와 편지지(헤더)가 분리돼 있다는 점만 잡으면 전체 구조가 보입니다.
flowchart LR
    A["발신자<br>leedo@naver.com"] -->|"1홉, SPF 검사 대상 naver.com"| B["Cloudflare MX<br>route1.mx.cloudflare.net"]
    B -->|"2홉, 봉투를 SRS로 재작성<br>SPF 검사 대상 leedo.me"| C["Gmail<br>leedo@gmail.com"]
Mermaid
복사

DNS 레코드 3종

같은 도메인이라도 웹과 메일은 별개 시스템입니다. 웹은 A/CNAME 레코드로 HTTP를 타고, 메일은 MX 레코드로 SMTP를 탑니다. 서로를 전혀 모릅니다.
Cloudflare Email Routing을 활성화하면(API 호출 한 번) 아래 3종이 자동으로 심어집니다.

MX, 수신 서버 지정

"이 도메인 앞으로 온 메일은 이 서버가 받는다"는 선언입니다. 발신 서버는 SMTP 접속 전에 반드시 MX부터 조회합니다.
leedo.me. MX 9 route1.mx.cloudflare.net. <- 우선순위는 낮은 숫자부터 leedo.me. MX 41 route3.mx.cloudflare.net. <- 장애 시 예비 leedo.me. MX 71 route2.mx.cloudflare.net.
Plain Text
복사
MX가 없으면 배달 자체가 불가능합니다. 도메인을 사도 메일이 "안 만들어져 있는" 이유입니다.

SPF, 발신 자격의 공표 (TXT)

방향이 반대입니다. 수신이 아니라 발신 자격입니다. "leedo.me 이름으로 메일을 보낼 수 있는 서버는 이 목록뿐"이라고 DNS에 공표합니다.
leedo.me. TXT "v=spf1 include:_spf.mx.cloudflare.net ~all"
Plain Text
복사
검사 주체는 수신 서버입니다. Gmail이 메일을 받으면 "발신자 도메인의 SPF 목록에 지금 접속한 이 IP가 있나"를 대조합니다. 끝의 ~all은 softfail로, 목록 밖 IP라도 거부하지 말고 의심 대상으로 표시하라는 뜻입니다.
헷갈렸던 지점이 하나 있습니다. 이 레코드를 leedo.me 한 곳만 갖는 게 아닙니다. 메일을 보내는 도메인은 저마다 자기 목록을 공표해둡니다. 네이버도 당연히 공표해두고 있습니다.
naver.com. TXT "v=spf1 ip4:111.91.135.0/27 ip4:125.209.208.0/20 ip4:125.209.224.0/19 ... ~all"
Plain Text
복사
수신 서버가 조회하는 목록은 봉투 발신자(MAIL FROM)의 도메인 것입니다. 네이버에서 온 메일이면 naver.com의 목록을 보고, 봉투가 leedo.me로 적힌 메일이면 leedo.me의 목록을 봅니다. 바로 아래 1홉에서 Cloudflare가 naver.com SPF를 뒤지는 이유가 이겁니다. 위에 심은 leedo.me 레코드는 leedo.me 이름으로 나가는 메일에만 쓰이고, 그게 2홉에서 SRS와 맞물립니다.

DKIM, 공개키 게시판 (TXT)

발신 서버가 헤더와 본문의 해시를 개인키로 서명해 보내면, 수신자가 DNS에 게시된 공개키로 검증합니다. 그럼 수신자는 공개키가 어느 이름에 있는지 어떻게 알까요. 서명한 쪽이 메일 헤더에 자기가 쓴 열쇠의 주소를 적어 보내기 때문입니다.
DKIM-Signature: v=1; a=rsa-sha256; d=leedo.me; s=cf2024-1; h=from:to:subject:date; bh=<본문 해시>; b=<서명값>
Plain Text
복사
여기서 d=는 서명한 도메인, s=는 셀렉터(selector), 역할상 키 이름입니다. 수신 서버는 이 둘을 셀렉터._domainkey.도메인 형식으로 조립해 TXT를 조회합니다. 그래서 cf2024-1._domainkey.leedo.me입니다. 지정된 자리가 아니라 헤더에서 계산된 자리입니다.
_domainkey는 RFC 6376이 정해둔 고정 문자열. 밑줄은 도메인 주인이 직접 만드는 이름과 부딪힐 여지를 없앤다는 장치
cf2024-1은 Cloudflare가 자기 키에 붙인 이름. 이름 자체는 아무 의미가 없어서 서버가 s=에 적어 보내는 것과 맞기만 하면 됩니다
셀렉터를 나누는 이유는 키 교체와 공존입니다. cf2025-1을 새로 게시해도 이미 발송된 메일은 이전 셀렉터를 보며 그대로 검증되고, 발신 경로가 여럿이면 경로마다 다른 셀렉터로 각자의 키를 씁니다
검증을 통과하면 두 가지가 증명됩니다. 본문이 중간에 바뀌지 않았고, 서명한 도메인이 진짜라는 것.

재전송과 SRS

전제, 메일 주소는 두 겹이다

여기서 막혔다가 풀린 지점입니다. 메일 주소는 한 겹이 아닙니다.
층위
무엇
누가 봄
SMTP 봉투 (envelope)
MAIL FROM: / RCPT TO:, 서버 간 배달용
서버만
메시지 헤더
From: / To:, 본문 맨 위 텍스트
사용자 화면
💡
종이 우편과 같습니다. 봉투 겉면 주소(배달, 반송용)와 편지지의 "보내는 사람"(내용물)은 별개입니다. 우체국은 봉투만 봅니다.

1홉, 네이버에서 Cloudflare로

첫 구간은 평범합니다.
MAIL FROM: <leedo@naver.com> <- 봉투 RCPT TO: <contact@leedo.me> 접속 IP: 네이버 발신 서버
Plain Text
복사
Cloudflare가 SPF를 검사합니다. 봉투 발신자가 naver.com이니 naver.com의 SPF 목록을 조회하고, 거기에 네이버 발신 서버 IP가 있으니 통과, 수신합니다.

2홉, Cloudflare에서 Gmail로 (문제 지점)

봉투를 그대로 복사해 재전송하면 이렇게 됩니다.
MAIL FROM: <leedo@naver.com> <- 발신 도메인은 naver.com인데 접속 IP: Cloudflare 서버 <- IP는 Cloudflare
Plain Text
복사
Gmail의 SPF 검사에서 naver.com SPF 목록에 Cloudflare IP가 없으니 실패, 스팸함으로 갑니다. 포워딩이라는 행위 자체가 "발신 도메인과 발신 서버는 일치한다"는 SPF 모델을 구조적으로 깨뜨립니다.

해법, SRS로 봉투만 재작성

Cloudflare가 봉투 발신자를 자기가 SPF를 보장할 수 있는 도메인으로 바꿔 씁니다.
MAIL FROM: <SRS0=a1b2=xy=naver.com=leedo@leedo.me> 접속 IP: Cloudflare 서버
Plain Text
복사
Gmail의 SPF 검사 대상이 leedo.me로 바뀌고, 그 목록에는 Cloudflare가 있으니 통과
주소 형식이 괴상한 이유는 원주소(naver.com=leedo)를 인코딩해 품고 있기 때문. 반송(bounce) 경로를 보존하는 장치입니다. Gmail이 반송하면 SRS 주소로 돌아오고, Cloudflare가 디코딩해 원발신자에게 전달합니다
앞쪽 해시는 SRS 주소를 위조해 반송 릴레이를 악용하는 것을 막는 서명

편지지(헤더)는 왜 안 바꾸나

From: 이영훈 <leedo@naver.com>은 그대로 둡니다. 사용자에게는 원발신자가 보여야 하고, DKIM 서명이 헤더와 본문에 걸려 있어 손대면 서명이 깨집니다. 그대로 뒀기에 Gmail이 naver.com 공개키로 검증에 성공합니다.

최종 결과

처음에 이상하게 봤던 화면이 이 구조의 흔적이었습니다.
SPF 검사 대상: 봉투(MAIL FROM) -> leedo.me (SRS 재작성) -> 통과 DKIM 검사 대상: 편지지(헤더+본문) -> naver.com (원본 서명 유지) -> 통과 화면 표시: From 헤더 -> 이영훈 <leedo@naver.com>
Plain Text
복사
Gmail 상세 보기에 "발송 도메인 leedo.me / 인증기관 naver.com"으로 나뉘어 찍힌 게 정상입니다. 두 인증이 서로 다른 도메인으로 통과하는 게 정석적인 메일 포워딩의 모습입니다.

제약과 주의사항

⚠️
도메인당 SPF TXT 레코드는 하나만 유효합니다. leedo.me로 이미 다른 경로(Google Workspace, 뉴스레터 발송 서버 등)에서 메일을 보내고 있다면, Cloudflare가 심는 레코드로 덮어쓰지 말고 include:를 한 줄로 합쳐야 합니다. TXT를 둘로 나누면 SPF 검사가 permerror로 떨어지며 기존 발신까지 같이 망가집니다.
이 구조는 수신 전용입니다. Cloudflare는 MX(수신)만 제공하고 발신용 submission(587) 서버가 없습니다. Gmail에서 답장하면 상대에게 gmail 주소가 노출되니, 필요하면 Gmail send-as를 따로 구성해야 합니다
전달 목적지(Gmail 주소)는 사전 검증이 필수입니다. 아무나 남의 주소를 목적지로 걸어 스팸 릴레이로 악용하는 것을 막는 장치입니다. Cloudflare 계정 메일과 같으면 자동 검증됩니다
로컬 맥에서 SMTP 직접 발송 테스트는 실패했습니다. 가정용 ISP가 스팸 방지를 위해 outbound 25번 포트를 차단하기 때문입니다. 서버 간 메일 전송은 데이터센터 IP에서만 현실적으로 가능합니다

정리

❄️
메일 포워딩은 MX로 받아서, 봉투(SPF 검사 대상)만 SRS로 갈아끼우고, 편지지(DKIM 검사 대상)는 그대로 넘기는 것.
설정 실체는 Cloudflare API 두 번입니다. email/routing/enable로 DNS 3종을 심고, email/routing/rulescontact@leedo.me에서 leedo@gmail.com으로 가는 전달 규칙을 만듭니다. 클릭 몇 번으로 끝나는 작업이지만, 그 뒤에서 봉투가 한 번 갈아끼워진다는 걸 알고 나니 스팸함에 빠진 포워딩 메일을 디버깅할 자신이 생겼습니다.