24분 분량

내 도메인으로 메일 보내기 — SPF·DKIM·DMARC는 실제로 무슨 일을 하는가

내 도메인으로 메일을 보낸다는 건 메일 서버를 돌리는 게 아니라, DNS로 "이 도메인에서 보낸 메일이 맞다"를 증명하는 일이다. SPF·DKIM·DMARC가 각각 무슨 일을 하고, Resend 같은 서비스로 어떻게 설정하는지 정리한다.

서비스를 만들다 보면 반드시 no-reply@내도메인.com으로 메일을 보내야 하는 순간이 온다. 회원가입 인증, 비밀번호 재설정, 결제 영수증 같은 것들이다. 그런데 막상 보내 보면 스팸함으로 직행하거나, 심하면 아예 거부당한다. "내 도메인인데 왜 내 메일을 못 믿지?"라는 의문이 여기서 시작된다.

이 글에서는 내 도메인으로 메일을 보낸다는 게 실제로 어떻게 동작하는지(SPF·DKIM·DMARC가 각각 무슨 일을 하는지)와, Resend 같은 발송 서비스로 그걸 어떻게 설정하는지, 그리고 가비아·GCP·AWS·로컬 환경마다 세팅이 어떻게 갈리는지를 정리한다. 결론부터 말하면, "내 도메인으로 메일 보내기"의 본질은 메일 서버를 돌리는 게 아니라 DNS로 '이 도메인에서 보낸 메일이 맞다'를 증명하는 일이다.

왜 그냥 보내면 안 닿는가 — SMTP의 원죄

이메일의 전송 규약인 SMTP는 1980년대에 설계됐다. 당시엔 서로 믿을 수 있는 소수의 기관만 메일을 주고받았기 때문에, 발신자를 검증하는 장치가 아예 없었다. 지금도 SMTP 수준에서는 "From: 아무개@어디든.com"을 그냥 적어서 보낼 수 있다. 편지 봉투에 아무 발신인이나 손으로 써넣는 것과 같다.

그래서 받는 서버 입장에서는 난감하다. no-reply@mybank.com에서 왔다고 주장하는 메일이 정말 그 은행에서 온 건지, 아니면 피싱범이 위조한 건지 메일 내용만으로는 알 수 없다. 검증 수단이 없으니 받는 쪽은 인증되지 않은 메일을 일단 의심할 수밖에 없고, 그 결과가 스팸함이다.

여기에 결정적인 변화가 더해졌다. 2024년부터 Gmail과 Yahoo가 대량 발송자(하루 5,000통 이상)에게 SPF·DKIM·DMARC 인증을 의무화했다. 셋 중 하나라도 빠지면 메일이 거부된다. 즉 "내 도메인으로 메일 보내기"는 이제 잘 보내기 위한 선택이 아니라, 안 하면 아예 안 닿는 필수 조건이 됐다 (Gmail·Yahoo 2024 발송자 요건).

그럼 이 세 가지가 각각 무슨 일을 하는지부터 보자.

두 개의 발신자 — 위조는 여기서 일어난다

인증을 이해하려면 먼저 이메일에 발신자가 두 개 있다는 걸 알아야 한다. 이게 모든 혼란의 출발점이다.

  • 봉투 발신자(envelope sender, MAIL FROM) — SMTP 전송 과정에서 쓰이고, 반송(bounce)이 돌아가는 주소다. 사용자에게는 보이지 않는다. Return-Path라고도 부른다.
  • 헤더 발신자(From:) — 메일 클라이언트에서 사용자가 실제로 보는 "보낸 사람"이다.

이 둘은 서로 다를 수 있다. 피싱이 노리는 지점이 바로 여기다. 봉투는 자기 도메인으로 두고 인증을 통과시키면서, 사용자에게 보이는 From:만 mybank.com으로 위조하는 식이다. 이 구분을 기억해 두면 뒤에 나오는 DMARC의 "정렬"이 왜 필요한지 자연스럽게 이해된다.

SPF — "이 IP들만 내 도메인 메일을 보낸다"

SPF(Sender Policy Framework)는 발신 IP를 검증한다. 도메인 소유자가 DNS의 TXT 레코드에 "내 도메인 메일을 보낼 수 있는 IP(또는 서버)는 이것들이다"를 선언해 둔다.

text
1v=spf1 include:amazonses.com ~all

받는 서버는 봉투 발신자(Return-Path) 도메인의 SPF 레코드를 조회하고, 실제로 메일을 보낸 IP가 그 목록에 있는지 대조한다. 있으면 통과, 없으면 실패다. 끝의 ~all은 "목록에 없으면 soft-fail(의심하되 거부까진 말라)"라는 뜻이고, -all은 hard-fail(거부)이다. 처음에는 ~all로 시작해 안정화되면 조이는 게 보통이다.

여기서 중요한 함정 하나. SPF는 From:이 아니라 봉투 발신자(Return-Path)를 본다. 이 사실이 뒤에서 정렬 문제의 핵심이 된다.

DKIM — 메일에 서명을 찍는다

SPF가 "어디서 보냈는가(IP)"를 본다면, DKIM(DomainKeys Identified Mail)은 **"내용이 위변조되지 않았고 이 도메인이 서명했는가"**를 본다. 공개키 암호를 쓴다.

  1. 발신 서버가 메일 헤더와 본문을 개인키로 서명하고, 그 서명을 메일 헤더(DKIM-Signature)에 넣는다.
  2. 서명에는 d=(서명한 도메인)과 s=(셀렉터)가 들어간다. 예를 들어 Resend는 셀렉터로 resend를 쓴다.
  3. 공개키는 DNS의 <셀렉터>._domainkey.<도메인> 위치에 TXT로 공개해 둔다.

받는 서버는 DKIM-Signature의 d=와 s=로 DNS에서 공개키를 찾아 서명을 검증한다. 검증되면 중간에 내용이 바뀌지 않았고, 그 도메인이 실제로 서명했다는 뜻이다. DKIM 레코드는 긴 공개키 문자열이라 대소문자까지 정확히 복사해 넣어야 하고, 한 글자만 틀려도 검증이 깨진다.

DMARC — "통과했다"로는 부족하다

여기서 반전이 있다. 메일이 SPF도 통과하고 DKIM도 통과했는데 여전히 가짜일 수 있다. 앞에서 본 "두 개의 발신자" 때문이다. 공격자가 자기 도메인(attacker.com)으로 SPF·DKIM을 멀쩡히 통과시키면서, 사용자에게 보이는 From:만 mybank.com으로 적으면, 두 인증은 "통과"로 나오지만 실제로는 완벽한 위조다.

DMARC(Domain-based Message Authentication, Reporting & Conformance)는 이 빈틈을 **정렬(alignment)**로 막는다. DMARC는 그 자체로 새로운 인증이 아니라, SPF·DKIM 위에 한 겹을 더 얹는 규칙이다. 핵심은 이것이다.

사용자에게 보이는 From: 도메인이, SPF가 검증한 Return-Path 도메인 또는 DKIM이 서명한 d= 도메인과 **일치(정렬)**해야 한다.

즉 SPF나 DKIM이 통과하는 것만으로는 안 되고, 그 통과한 도메인이 From:과 같아야 DMARC를 통과한다 (DMARC 정렬 설명). 공격자가 attacker.com으로 인증을 통과시켜도 From:이 mybank.com이면 정렬이 깨지므로 걸러진다. 이게 DMARC가 스푸핑을 실제로 막는 원리다.

DMARC 레코드는 _dmarc.<도메인>에 TXT로 둔다. 정책(p)과 리포트 주소(rua)가 핵심이다.

text
1v=DMARC1; p=none; rua=mailto:dmarc@내도메인.com; adkim=r; aspf=r

왜 발송 서비스를 쓰고, 왜 하필 서브도메인인가

이론은 그렇다 치고, 실제로는 메일 서버를 직접 돌리지 않는다. 발신용 IP의 평판(reputation)을 쌓고 관리하는 일, 역방향 DNS·blocklist 대응, 바운스·불만 처리 인프라를 직접 운영하는 건 비현실적이기 때문이다. 그래서 Resend·Amazon SES·Postmark 같은 서비스를 쓴다. 이들이 평판 좋은 IP로 대신 보내 주고, 나는 DNS 레코드만 설정해서 "이 서비스가 내 도메인으로 보내는 걸 허가한다"고 선언한다.

그런데 여기서 많이들 걸리는 지점이 있다. 서비스가 기본으로 쓰는 Return-Path는 서비스의 도메인이다. 그러면 앞의 규칙 때문에 문제가 생긴다. SPF는 Return-Path 도메인을 보는데, 그게 서비스 도메인이면 내 From: 도메인과 정렬되지 않는다. DKIM 정렬만으로 버틸 수는 있지만, 양쪽을 다 맞춰 두는 게 안전하다.

그래서 Resend는 **내 도메인 아래의 서브도메인(기본 send.내도메인)**에 Return-Path를 걸게 한다 (Resend 도메인 추가 문서). 서브도메인도 내 도메인의 일부이므로 SPF가 From:과 정렬된다. "왜 굳이 send. 같은 서브도메인을 만들라고 하지?"의 답이 바로 이 SPF 정렬이다. 단순한 관례가 아니라 정렬을 성립시키기 위한 구조다. (이 "서브도메인으로 정렬을 맞춘다"는 패턴은 뒤의 AWS SES에서 똑같이 반복된다.)

실제 DNS 레코드 — Resend 기준

Resend에 도메인을 추가하면 아래 레코드들을 만들라고 안내한다. DNS 제공업체(가비아·Cloudflare 등)에 그대로 넣으면 된다 (Resend SPF/DKIM 설정).

text
1# 1) 바운스 처리용 MX — send 서브도메인에
2Type: MX Name: send.내도메인 Value: feedback-smtp.(리전).amazonses.com Priority: 10
3
4# 2) SPF — send 서브도메인에 (발신 IP 허가)
5Type: TXT Name: send.내도메인 Value: v=spf1 include:amazonses.com ~all
6
7# 3) DKIM — 공개키 (대소문자까지 정확히)
8Type: TXT Name: resend._domainkey.send.내도메인 Value: p=MIGfMA0GCS...(긴 공개키)
9
10# 4) DMARC — 권장, 처음엔 관찰 모드로
11Type: TXT Name: _dmarc.내도메인 Value: v=DMARC1; p=none; rua=mailto:dmarc@내도메인

몇 가지 주의할 점이 있다.

  • DKIM은 대소문자 민감이다. 콘솔에서 생성된 값을 손으로 치지 말고 복사해 넣는다.
  • DNS 전파에는 시간이 걸린다. 넣자마자 검증이 안 돼도 정상이고, 보통 수분~수십 분 뒤 다시 확인한다.
  • DMARC 정책은 반드시 p=none부터 시작한다. 처음부터 p=reject를 걸면, 아직 정렬이 안 맞는 정상 메일(예: 다른 경로로 나가는 알림)까지 전부 거부돼 사고가 난다. rua 리포트로 어떤 메일이 정렬에 실패하는지 며칠~몇 주 관찰한 뒤, quarantine을 거쳐 reject로 점진적으로 조인다.

환경별로 어떻게 다른가 — 가비아 / GCP / AWS / 로컬

여기서 한 가지 축을 명확히 해야 한다. DNS를 어디서 관리하느냐와 메일을 실제로 누가 발송하느냐는 별개다. SPF·DKIM·DMARC 레코드는 "DNS 관리처"(가비아·Route53·Cloud DNS)에 넣고, 실제 발송은 "발송 주체"(Resend·SES·자체 MTA)가 한다. 환경마다 이 두 축이 어떻게 갈리는지가 세팅 차이의 전부다.

환경DNS 관리발송 주체핵심 함정
가비아가비아 DNSResend·SES 등 외부호스트는 상대 이름만, SPF TXT는 도메인당 1줄
GCPCloud DNS외부 릴레이(SendGrid 등)포트 25·465·587 아웃바운드 차단 → VM 직접 발송 불가
AWSRoute53SES기본 MAIL FROM이 amazonses.com → Custom MAIL FROM 필요 + 샌드박스
로컬·자체아무 DNS자체 MTA(Postfix)PTR·포트25·IP 평판 전부 직접 — 난이도 최상

가비아 — DNS만 관리, 발송은 외부

가비아는 발송 서비스가 아니라 도메인·DNS 등록기관이다. 따라서 가비아의 역할은 앞서 만든 SPF·DKIM·DMARC 레코드를 DNS에 등록하는 것까지고, 발송은 Resend·SES가 한다.

  • 먼저 My가비아 → 도메인 관리 → DNS 관리에서 네임서버가 가비아 기본 네임서버로 되어 있어야 레코드를 직접 관리할 수 있다. Cloudflare 등으로 위임했다면 거기서 넣는다.
  • DNS 레코드 관리 → 레코드 추가에서 타입(TXT·MX·CNAME)·호스트·값을 입력한다.
  • 가장 헷갈리는 함정: 호스트 칸에는 도메인 전체가 아니라 상대 이름만 넣는다. 루트면 @, send.내도메인이면 호스트에 send, resend._domainkey.send.내도메인이면 resend._domainkey.send만 적는다. 뒤의 도메인은 자동으로 붙는다.
  • SPF는 도메인당 TXT 한 줄만 둔다. 구글 워크스페이스와 Resend를 함께 쓴다면 두 줄로 나누지 말고 include를 한 레코드에 합친다 (가비아 DNS 레코드 설정 가이드).

GCP — Cloud DNS + 외부 릴레이 (포트 25가 막혀 있다)

GCP에서 가장 먼저 부딪히는 벽은 DNS가 아니라 발송 포트다. Compute Engine은 스팸 남용 때문에 아웃바운드 25·465·587 포트를 기본 차단한다. 즉 GCP VM에 Postfix를 깔아도 바깥으로 직접 SMTP를 못 보낸다 (GCP sending mail 문서).

  • 해결책은 외부 릴레이다. SendGrid·Mailgun·Mailjet 같은 서비스를 대체 포트(SendGrid는 2525)나 API로 호출해 발송한다. 이들이 평판 좋은 IP로 대신 보내 주므로 포트 차단과 IP 평판 문제를 동시에 피한다.
  • DNS는 Cloud DNS에 SPF·DKIM·DMARC TXT를 넣는다. 레코드 내용·정렬 규칙은 앞과 동일하고, 발송 주체(SendGrid 등)가 알려주는 값을 그대로 등록한다.
  • 수신은 막히지 않는다. 막히는 건 아웃바운드 발송뿐이다.

AWS — Route53 + SES (MAIL FROM과 샌드박스)

AWS는 발송까지 자체 제공한다. SES(Simple Email Service)가 발송 주체고, DNS가 Route53이면 둘이 자동으로 엮인다.

  • SES에 도메인 아이덴티티를 등록하고 Easy DKIM을 켜면 SES가 2048비트 DKIM 키를 자동 생성한다. DNS가 Route53이면 필요한 레코드를 SES가 자동으로 게시해 검증이 몇 분 안에 끝난다 (SES Easy DKIM 문서).
  • 여기서 앞의 서브도메인 논리가 그대로 반복된다. SES의 기본 MAIL FROM(Return-Path)은 amazonses.com이다. 이건 SPF를 통과하지만 내 도메인과 정렬되지 않는다. DMARC를 SPF로도 정렬시키려면 Custom MAIL FROM 도메인(내 서브도메인, 예: mail.내도메인)을 설정해 MX·TXT를 추가해야 한다. Resend가 send. 서브도메인을 요구한 것과 정확히 같은 이유다.
  • 샌드박스 주의: 신규 SES 계정은 샌드박스 상태라 검증된 주소로만 발송되고 일일 한도가 낮다. 실제 사용자에게 보내려면 프로덕션 액세스를 신청해야 한다.

로컬 / 자체 구축 — 전부 직접 (난이도 최상)

Postfix + OpenDKIM으로 메일 서버를 직접 세우는 길이다. 가능은 하지만, 앞의 서비스들이 대신 해주던 걸 전부 직접 떠안는다.

  • PTR(역방향 DNS): 발송 IP가 내 메일 도메인으로 역해석돼야 한다. PTR이 없으면 사실상 스팸 처리된다. 이제 Google·Yahoo·Microsoft 모두 PTR을 필수 baseline으로 요구한다 (PTR 설정 가이드).
  • 포트 25 아웃바운드: 뚫려 있어야 한다. 대부분의 클라우드·가정용 IP는 기본 차단이라, 포트 25가 열린 VPS가 필요하다.
  • IP 평판(warm-up): 신규 IP는 몇 주간 의심받는다. 처음부터 대량 발송하면 블록리스트에 오른다. 조금씩 양을 늘리며 평판을 쌓아야 한다.
  • OpenDKIM으로 서명하고(기본이 tempfail이라 OpenDKIM이 죽으면 메일을 보류한다), TLS·MTA-STS까지 챙긴다.
  • 결론: 학습이나 완전한 통제가 목적이 아니라면 권하지 않는다. SPF·DKIM·DMARC 네 줄만 넣으면 끝나는 일을, 자체 구축은 평판·포트·PTR·모니터링이라는 상시 운영 부담으로 바꾼다.

보내는 쪽 — 앱에서 메일이 나가기까지

설정이 끝나면 앱에서는 서비스 API를 호출해 메일을 보낸다. 전체 흐름을 한 장으로 보면 이렇다.

앱 코드 자체는 평범한 API 호출이다. 신뢰의 근거는 코드가 아니라 DNS에 심어 둔 SPF·DKIM·DMARC에 있다는 점이 핵심이다.

ts
1await resend.emails.send({
2 from: "no-reply@내도메인.com", // From 도메인 = 정렬 대상
3 to: user.email,
4 subject: "가입을 환영합니다",
5 html: renderWelcome(user),
6});

대량 발송이라면 추가로 — 2024년 요건

알림·마케팅처럼 하루 5,000통 이상을 Gmail·Yahoo로 보낸다면, 인증만으로는 부족하고 아래까지 맞춰야 거부되지 않는다 (Gmail·Yahoo 발송자 요건).

  • 원클릭 구독취소: 마케팅 메일에는 List-Unsubscribe와 List-Unsubscribe-Post 헤더(RFC 8058)를 넣어 한 번에 구독을 끊을 수 있어야 한다. Yahoo는 2일 내 처리를 요구한다.
  • 스팸 불만율 0.3% 미만 유지. 넘으면 전체 발송 평판이 망가진다.
  • SPF·DKIM·DMARC는 당연히 기본이고, From: 정렬이 맞아야 한다.

트랜잭션 메일(인증·영수증)만 보낸다면 여기까지는 아니어도 되지만, 인증 3종(SPF·DKIM·DMARC)은 규모와 무관하게 갖춰 두는 게 좋다.

정리 — 결국 전부 DNS에 적는 신뢰

  • SPF: Return-Path 도메인의 TXT에 발신 IP를 허가한다. (v=spf1 include:... ~all)
  • DKIM: 메일에 개인키로 서명하고, 공개키를 셀렉터._domainkey에 둔다. 내용 위변조·서명 도메인을 보증.
  • DMARC: SPF/DKIM의 통과 도메인이 From:과 정렬되는지 보고, 안 맞으면 정책(none→quarantine→reject)으로 처리. 스푸핑을 실제로 막는 건 이 정렬이다.
  • 환경별: 가비아는 DNS 등록만(호스트는 상대 이름), GCP는 포트 25 차단이라 외부 릴레이 필수, AWS는 SES Custom MAIL FROM + 샌드박스 해제, 로컬은 PTR·포트·평판까지 직접.
  • 운영: DKIM은 정확히 복붙, DNS 전파 대기, DMARC는 p=none부터 리포트 보며 점진 강화.

처음엔 레코드 네 줄 넣는 단순 작업처럼 보이지만, 그 네 줄이 하는 일은 분명하다. "이 도메인에서 보낸 메일이 맞다"를 받는 세상 모든 메일 서버에게 증명하는 것이다. 어느 환경을 쓰든 DNS 관리처와 발송 주체만 갈릴 뿐, 신뢰의 근거는 결국 내 DNS에 적어 둔 이 선언뿐이다. 그래서 p=reject에 도달하는 순간은, 설정이 끝났다는 신호가 아니라 내 도메인을 아무도 위조할 수 없게 됐다는 신호로 읽는 게 맞다.