SES 공부인데 mail 프로토콜 공부가 되어버림
마이그레이션 작업하면서 SES를 활성화하다가 SPF, DKIM, DMARC를 몰라서 시작한 메일 프로토콜 공부
SES 설정은 그렇게 어렵지 않다. 단순히 SES 활성화를 진행하고 이메일 인증(단일 유저 발송), 도메인 인증은 발송자의 도메인만 지정하는 거라고 생각하면 된다.
마이그레이션 프로젝트 중 기존 sendmail을 이용해서 IDC 같은 환경에서 메일을 보내고 있었는데 이번 기회에 AWS로 마이그레이션 하면서 SES를 사용하게 되었다.
기존에도 SES를 활용해서 회사 내부용 메일 서비스를 구축했지만 그 당시에는 단순히 구현이 목표여서 구현만 하고 종료했다. 이번 마이그레이션할 때 좀 더 알아보고 앞으로 아는 척을 해야겠다.
실제 애플리케이션에서 SES를 사용하는 방법은 아래와 같이 간단하다.
1
2
3
4
1. SES에서 도메인 검증한다
2. SMTP 자격증명 만든다
3. 애플리케이션 SMTP 설정에 SES 엔드포인트 넣는다
4. 끝
- Identity 검증할 때 DNS에 TXT 레코드를 넣으라는데, 이게 SPF인데 뭐하는거지?
- DKIM이라고 CNAME 레코드 3개를 넣으라는데 이건 또 뭔가? 이거 하면 도메인 우리가 관리한다 인증정도인가?
- “Custom MAIL FROM domain”은 왜 굳이 따로 설정하는 항목으로 있나? 왜 다 따로 설정한거야?
- DMARC는 아예 SES 화면에 나오지도 않는데, 그럼 안 해도 되는 건가?
메일을 그냥 “보내는 기능”으로만 써왔지, 왜 이 레코드들이 필요한지는 생각해본 적이 없었다. 그래서 SMTP부터 다시 짚어봐야겠다고 생각했다. 공부할 게 왜 이리 많아
메일 한 통에는 주소가 두 개 있다
From 주소 하나만 있는 줄 알았는데 두 개가 있음. 하나는 누가 보냈는가(설정해서 보냄), 두 번째는 반송지 주소
1
2
3
4
5
6
7
편지봉투(Envelope) 편지지(Header)
MAIL FROM: bounce@mydomain.com From: no-reply@mydomain.com
RCPT TO: user@gmail.com To: user@gmail.com
│ │
│ SMTP 프로토콜이 실제로 │ 메일 클라이언트가 화면에
│ 배달에 쓰는 주소 │ 보여주는 주소
│ (수신자는 안 봄) │ (수신자가 실제로 보는 주소)
- MAIL FROM (Envelope From): SMTP 대화에서 오가는 주소다. 배달 실패했을 때 반송(bounce)이 이 주소로 간다. 그래서 Return-Path, bounce address라고도 부른다. 수신자는 메일 원본을 열어보지 않는 이상 이 주소를 볼 일이 없다.
- From (Header From): 메일 헤더 안에 있는 주소다. 우리가 메일함에서 보는 “보낸사람”이 이거다.
공식문서 · Using a custom MAIL FROM domain — “The MAIL FROM address is sometimes called the envelope sender, envelope from, bounce address, or Return Path address… The MAIL FROM address is usually only viewable by recipients if they view the source code for the message.”
그렇다면 주소는 왜 2개로 분리되어 있을까?
From (Header From)는 그냥 메일의 내용이라고 생각하면 된다. 메일에 포함되는 거라서 엔드유저에게만 의미가 있음. 다만 MAIL FROM은 반송 시 사용되는 주소이다. 즉, 메일 배달을 할 때 여러 홉을 거칠 수 있는데 홉마다 이 값은 바뀔 수 있어야 하기 때문에 의도적으로 값을 2개로 나눈 거다.
여기서 홉이란 메일이 발신자에서 최종 수신함까지 가는 동안 거쳐가는 SMTP 서버 하나하나를 말한다. 실제 메일 헤더를 열어보면 Received: 줄이 서버 개수만큼 쌓여 있는데, 그게 홉의 흔적이다.
여기서 Received를 보면 geopod-ismtpd-126 -> recvd-5bfcddc674-nfzbw -> o4239.e.email.meetup.com 등등 여러 번 간다. 하지만 자세히 보면 from이 있는 줄만 실제 조직 간 이동(=진짜 홉)이고, 여기선 ③번 한 번뿐이다.
나머지는 전부 “같은 조직 안에서 역할만 바뀌는” 처리 단계라 from이 비어있거나 API 특성상 unknown으로 찍힌 것이다. 조직 경계는 하나(SendGrid→Google)인데, 그 앞뒤로 SendGrid와 Google이 각자 자기 내부 파이프라인 로그까지 Received로 남겨서 전체 줄 수가 많아 보이는 것이다.
그래 그래 그렇다면 그 외 SPF, DKIM, DMARC는 무엇인가?
먼저 SPF는 뭐지?
SPF (Sender Policy Framework) — 도메인 소유자가 “내 도메인을 대신해서 메일을 보낼 수 있는 서버(IP) 목록”을 DNS에 공표해두고, 수신 서버가 그 목록과 대조해서 발신 서버가 정당한지 검증하는 이메일 인증 표준이다. RFC 7208로 정의돼 있고, 스푸핑(발신자 위조) 방지를 목적으로 만들어졌다.
여기서 TXT로 해당 서버 목록을 넣는다.
1
2
3
4
5
6
7
8
① 도메인 소유자가 DNS에 TXT 레코드로 "이 IP들만 나 대신 메일 보낼 수 있음" 이라고 공표
mydomain.com. TXT "v=spf1 include:amazonses.com ~all"
② 메일이 도착하면, 수신 서버는:
- 이 메일의 MAIL FROM 도메인이 뭐지? → mydomain.com
- mydomain.com의 SPF 레코드를 DNS에서 조회
- 지금 이 메일을 보낸 IP가 그 목록에 있나? → 있으면 Pass, 없으면 Fail
SPF가 검사하는 건 Header From이 아니라 MAIL FROM 도메인이다.
SES 기본 상태에서는 이미 SPF가 통과한다
커스텀 MAIL FROM을 설정 안 해도 SES로 보낸 메일의 SPF는 통과한다. 애초에 MAIL FROM이 AWS의 SES이기 때문이다!
1
2
3
4
5
6
설정 안 함:
MAIL FROM = bounce.us-east-1.amazonses.com ← SES 소유, SPF 이미 통과
From = no-reply@mydomain.com ← 내 도메인
SPF는 통과한다. 그런데 두 도메인이 서로 다르다.
→ 뒤에서 볼 DMARC 정렬(alignment)이 여기서 깨진다.
여기서 SPF는 통과하지만 2개의 도메인이 달라 DMARC(이건 아마도 2개가 일치해야 성립되는 거)가 깨진다는 문제점이 있긴 하지만 큰 문제는 아님(마지막에 다룰 거임)
Custom MAIL FROM domain을 쓰는 경우
1
2
3
4
목표: MAIL FROM 도메인을 내 도메인(mydomain.com)의 서브도메인으로 바꾼다
bounce.mydomain.com. MX 10 feedback-smtp.<region>.amazonses.com
bounce.mydomain.com. TXT "v=spf1 include:amazonses.com ~all"
- MX 레코드가 필요한 이유: 이 도메인으로 반송·컴플레인 메일이 실제로 돌아올 수 있어야 하기 때문이다. SES가 그 수신 처리까지 대신 해준다.
- SPF TXT 레코드가 필요한 이유: 이제 MAIL FROM이
amazonses.com이 아니라mydomain.com의 서브도메인이 됐으니, 이 도메인 몫의 SPF는 내가 새로 공표해야 한다.
공식문서 · Using a custom MAIL FROM domain — “By using a custom MAIL FROM domain, you have the flexibility to use SPF, DKIM, or both to achieve DMARC validation.” / MAIL FROM 도메인 요구조건: 검증된 identity의 상위 도메인의 서브도메인이어야 하고, 발신·수신용으로 겹쳐 쓰면 안 됨. · MX 레코드가 없거나 실패 시 동작(Behavior on MX failure)을 “기본 MAIL FROM으로 폴백” 또는 “메시지 거부”로 선택 가능.
이제는 두 번째 DKIM 넌 뭐여
SPF의 구조적 약점이 존재한다.
위 내용을 보면 SPF는 경로(어느 IP가 보냈나)만 본다. 메일이 중간에 포워딩되면 문제 발생~
1
2
3
4
5
A가 나에게 메일을 보냄 → 내가 그걸 B에게 포워딩
포워딩된 메일의 MAIL FROM은 이제 "나"의 서버가 된다.
A의 SPF 레코드에는 "나"의 IP가 없다.
→ SPF Fail. 그런데 이 메일은 사실 위조된 게 아니라 정상적인 포워딩이다.
경로가 바뀌면 SPF는 쉽게 깨진다. 그래서 내용 자체에 서명을 붙이는 방식을 고안해서 DKIM을 사용한다.
DKIM 정의
DKIM (DomainKeys Identified Mail) — 메일이 정말 그 도메인 소유자가 승인한 게 맞는지를, 공개키 암호화로 메일 내용에 서명해서 증명하는 이메일 인증 표준이다. RFC 6376으로 정의돼 있다.
우리가 SES에 DKIM을 위해서 3개의 CNAME 레코드를 등록하는데 여기서 서명할 때 이 값을 사용한다.
1
2
3
4
5
6
7
8
9
10
① 도메인 소유자가 개인키/공개키 쌍을 만든다
② 공개키는 DNS TXT 레코드로 공표한다 (selector._domainkey.mydomain.com)
③ 메일을 보낼 때, 정해진 헤더값 + 본문 해시를 개인키로 서명
④ 서명값을 DKIM-Signature 헤더에 실어서 같이 보낸다
DKIM-Signature: v=1; a=rsa-sha256; d=mydomain.com; s=selector1;
h=from:to:subject; bh=...; b=<서명값>
⑤ 수신 서버는 d= 태그로 어느 도메인인지 알고, 그 도메인의 DNS에서
s= 셀렉터에 해당하는 공개키를 조회해서 서명을 검증한다
메일 내용의 일부다. 그래서 포워딩으로 경로가 바뀌어도, 헤더/본문만 변형되지 않았다면 서명은 그대로 유효하다. SPF가 “누가 보냈나”를 보는 반면 DKIM은 “내용이 그 도메인이 진짜로 승인한 게 맞나”를 본다.
SES의 DKIM 서명 방식 네 가지
콘솔에서 봤던 선택지들이 이거였다.
| 방식 | 키는 누가 관리 | 서명은 누가 |
|---|---|---|
| Easy DKIM | SES가 키 쌍 생성·관리 | SES가 자동 서명 |
| DEED | Easy DKIM 설정을 여러 리전에 복제 | SES가 자동 서명 |
| BYODKIM | 내가 직접 키 쌍 생성 | SES가 (내 키로) 자동 서명 |
| 수동 서명 | 내가 직접 관리 | 내가 SendRawEmail에 직접 서명해서 보냄(여기까지 하나??) |
공식문서 · Authenticating Email with DKIM in Amazon SES — 네 가지 방식 목록 / “Amazon SES also supports DKIM 2048 to allow more secure authentication of emails and therefore uses it as the default key length” / “You can’t switch to the same key length as is already configured” · “You can’t switch to a different key length more than once in a 24 hour period”
콘솔에서 CNAME 레코드 3개가 나왔던 건 Easy DKIM이라서다. SES가 키를 관리하니까, 내 도메인의 selector._domainkey 자리에는 실제 공개키 대신 AWS가 관리하는 키로 가리키는 CNAME을 심는 방식이다. 셀렉터가 3개인 건 키 로테이션을 무중단으로 하기 위한 여유라고 한다.
DKIM은 도메인 단위로 상속된다
1
2
3
4
5
6
7
8
도메인에 DKIM 설정 → 그 도메인 + 모든 서브도메인 + 그 도메인 쓰는 모든 이메일 주소에 자동 적용
mydomain.com에 DKIM 설정
→ no-reply@mydomain.com 자동 서명됨
→ alert@mydomain.com 자동 서명됨 (따로 설정 안 해도)
개별 이메일 주소 identity에서 끌 수는 있어도(override), 켤 수는 없다.
(도메인에서 이미 켜져 있으면 상속이 기본값이라서)
공식문서 · Authenticating Email with DKIM in Amazon SES — “You only need to set up DKIM for the domain that you use in your ‘From’ address… Because DKIM properties are inherited from the parent domain…” / “You cannot enable DKIM signing on the email address identity” (도메인에서 켜져 있을 때 개별 주소에서는 끄는 것만 가능)
세번째 이제 DMARC은?
1
2
3
4
5
6
7
8
9
10
11
SPF만 있을 때:
MAIL FROM: bounce@spammer.com ← 여기 기준으로 SPF 검사, Pass
From: ceo@우리회사.com ← 사람은 이것만 본다
spammer.com이 자기 SPF를 제대로 설정해놨다면 SPF는 "정상 통과"한다.
그런데 사람이 보는 From은 여전히 위조된 우리회사.com이다.
→ SPF Pass ≠ Header From이 신뢰할 만하다
DKIM만 있을 때도 마찬가지다.
DKIM-Signature의 d= 가 spammer.com이면, 그 서명은 완벽하게 유효하다.
그런데 그것도 Header From(우리회사.com)과는 다른 도메인 얘기다.
SPF와 DKIM은 각자 자기 영역(MAIL FROM / DKIM 서명 도메인)을 모두 검증하지만 그게 사람이 보는 From 주소는 검증하지 않는다. 이 틈을 메우는 게 DMARC다.
정렬(Alignment) — DMARC는 사람이 보는 도메인의 루트 도메인을 확인함
1
2
SPF 정렬(aspf): Header From 도메인 ==(또는 서브도메인)== MAIL FROM 도메인
DKIM 정렬(adkim): Header From 도메인 ==(또는 서브도메인)== DKIM 서명(d=) 도메인
각각 relaxed(느슨)/strict(엄격) 두 모드가 있다.
1
2
3
4
5
6
7
relaxed (기본값): 서브도메인까지 허용
From: no-reply@mail.mydomain.com
MAIL FROM: bounce.mydomain.com ← 상위 도메인만 같으면 정렬 통과
strict: 완전히 같은 도메인이어야 함
From: no-reply@mydomain.com
MAIL FROM: bounce.mydomain.com ← 서브도메인이라 strict에서는 불일치
공식문서 · Complying with DMARC through SPF — “the domain’s DMARC policy must not specify a strict SPF policy (aspf=s)” /
dig TXT _dmarc.example.com으로aspf=r(relaxed, 기본값) /aspf=s(strict) 확인 방법 · Complying with DMARC through DKIM — “If the domain’s DMARC policy specifies strict alignment for DKIM, these domains must match exactly (SES uses a strict DKIM policy by default)”
최종 판정은 OR
1
2
DMARC Pass = (SPF Pass AND SPF 정렬) OR (DKIM Pass AND DKIM 정렬)
DMARC Fail = 둘 다 위 조건을 못 채웠을 때
하나만 제대로 맞춰도 DMARC는 통과한다. 그래서 SPF/DKIM 중 하나만 있어도 되긴 하지만, 포워딩 상황(SPF에 약함)과 내용 변형 상황(DKIM에 약함)을 서로 다르게 방어하기 때문에 둘 다 정렬시켜두는 게 정석이다.
공식문서 · Complying with DMARC authentication protocol in Amazon SES — “앞서 설명한 SPF 검사나 DKIM 검사 중 하나(또는 둘 다) 통과하면 그 메시지는 DMARC를 통과한 것이다. 앞서 설명한 SPF와 DKIM 검사가 둘 다 실패하면 그 메시지는 DMARC를 실패한 것이다.”
그래 그러면 DMARC 레코드는 필요 없는 거 아님?
아니지, 위 내용이 SES에서 기본값이 아니라는 뜻이다. 해당 TXT 레코드를 직접 설정해줘야 위 로직대로 움직인다.
1
_dmarc.mydomain.com. TXT "v=DMARC1; p=quarantine; rua=mailto:report@mydomain.com"
p=none— 아무 조치도 안 함. 리포트만 받는다.p=quarantine— 실패한 메일을 스팸함으로.p=reject— 실패한 메일을 아예 거부.rua=— 결과 리포트 메일을 받을 주소다.
공식문서 · Setting up the DMARC policy on your domain — 레코드 이름은
_dmarc.,p=값 세 가지 설명 · Best practices for implementing DMARC — “It’s best to implement your DMARC policy enforcement in a gradual and phased approach” → none으로 리포트 보며 관찰 → quarantine → reject 순으로 단계적으로 올리라는 권고
p=reject부터 바로 걸면 안 되는 이유가 여기 있다. 아직 SPF/DKIM 정렬이 안 맞는 정상 발신 채널(제3자 발송 서비스, 포워딩 등)이 있으면 그 메일들까지 몽땅 거부당한다. 그래서 순서가 none(관찰) → quarantine → reject다.
정리정리
처음 봤던 화면 항목들을 다시 정리하면 이렇다.
1
2
3
4
5
Identity 검증 + DKIM (Easy DKIM) → CNAME 3개
DKIM용 CNAME을 DNS에 넣는 것 자체가 "도메인 소유권 증명"과 "DKIM 서명 활성화"를 동시에 해결
Custom MAIL FROM domain → MAIL FROM을 amazonses.com에서 내 도메인으로
→ SPF를 내 도메인 몫으로 가져오는 전제 조건
DMARC → SES 콘솔엔 없다. 별도로 _dmarc TXT를 직접 등록해야 한다
콘솔에서 “이거 설정 안 해도 메일은 잘 가던데?”였던 이유와, 그런데도 왜 굳이 Custom MAIL FROM을 켜야 하는지가 이제 같은 문장으로 설명된다. SPF 단독 통과와 DMARC 정렬은 다른 얘기이기 때문이다.
결론은 DKIM만 등록하고 도메인 인증 or 인증된 이메일로 보내면 보내지긴 한다. 다만 수신 메일함에서 메일 신뢰도가 떨어질 수 있긴 함
근데 확인해보니까 대량 발신자 하루 5000통 이상이면 DMARC 권장하네… 이것도 추가로 설정해야겠다. https://support.google.com/mail/answer/81126?hl=en#zippy=%2Crequirements-for-sending-or-more-messages-per-day
여기까지가 “이 메일이 진짜 그 도메인 게 맞나”였다면, 이제 “그래서 SES를 거쳐 어떻게 나가나”다
인증 3형제(SPF/DKIM/DMARC)는 메일의 신원 증명이었다. 여기서부터는 릴레이 얘기다 — 애플리케이션은 SES의 무엇에 연결해서, 무엇으로 인증하고, 보낸 뒤엔 뭘 돌려받는가.
SMTP 릴레이 — 애플리케이션은 SES의 SMTP 엔드포인트에 붙는다
1
2
[애플리케이션] --SMTP(TLS)--> [SES SMTP 엔드포인트] --(SPF/DKIM 서명 등 처리)--> [수신자 메일서버]
email-smtp.<region>.amazonaws.com
SES는 반드시 TLS로 암호화된 연결만 받는다. 그 안에서도 두 가지 방식이 있다.
1
2
3
4
5
6
7
8
9
STARTTLS (포트 25 / 587 / 2587)
1) 일단 평문으로 연결 → EHLO
2) 서버가 STARTTLS 확장 지원한다고 응답
3) 클라이언트가 STARTTLS 명령 → 그 위에서 TLS 협상
4) 협상 끝나면 암호화된 채널 위에서 다시 EHLO부터 정상 진행
TLS Wrapper (포트 465 / 2465, 일명 SMTPS)
1) 처음부터 TLS로 연결 (평문 구간이 아예 없음)
2) 서버가 인증서 제시 → EHLO → 정상 SMTP 진행
공식문서 · Connecting to an Amazon SES SMTP endpoint — “The Amazon SES SMTP endpoint requires that all connections be encrypted… Amazon SES supports two mechanisms for establishing a TLS-encrypted connection: STARTTLS and TLS Wrapper.” / STARTTLS는 25·587·2587번 포트, TLS Wrapper는 465·2465번 포트
SMTP 자격증명
장기 키를 발행해야 하는데 임시 자격증명으로는 안 되나 궁금했다. 하지만 임시 자격증명(ASIA... + SessionToken)으로는 이 변환 자체가 안 된다. SMTP 인터페이스가 SessionToken이 들어갈 자리를 아예 마련해두지 않았기 때문이다. 그래서 SMTP 릴레이 전용으로는 장기 자격증명을 가진 별도 IAM 사용자를 만들어 쓰는 게 정석이라고 한다. (물론 이 사용자는 ses:SendRawEmail 하나만 가진, 권한이 최소화된 사용자여야 한다.)
보낸 뒤엔 어떻게 되나 — Bounce, Complaint
메일을 SES에 넘겼다고 끝이 아니다. SES가 수신 서버에 넘긴 뒤 벌어지는 일은 SNS 알림으로 돌아온다.
1
2
3
4
5
6
[SES가 발송] → 수신 서버가 처리
│
┌──────────┼──────────┐
▼ ▼ ▼
Delivery Bounce Complaint
(정상 전달) (반송됨) (수신자가 스팸 신고)
Bounce는 두 종류로 갈린다.
1
2
3
4
5
6
7
Permanent (hard bounce)
존재하지 않는 주소, 도메인 자체가 없음 등
→ 재시도해도 답 없음. 즉시 발송 리스트에서 빼야 함.
Transient (soft bounce)
메일함 꽉 참, 메시지 너무 큼, 일시적 서버 문제 등
→ SES가 일정 기간 재시도하다가 포기하면 그때 알림
Complaint는 반송과는 다른 문제다. 메일 자체는 잘 도착했는데, 수신자가 “이거 스팸이다”라고 신고(feedback loop)한 것이다.
1
"메일이 실패했다" (Bounce) ≠ "메일은 갔는데 수신자가 싫어했다" (Complaint)
공식문서 · Amazon SNS notification contents for Amazon SES — bounceType은
Permanent(hard) /Transient(soft) /Undetermined. “When you receive a bounce notification with a bounce type of Permanent (hard)… you should immediately remove the recipient whose address produced the bounce from your mailing lists.” / complaint 객체의complaintFeedbackType(abuse, fraud, not-spam 등)
둘 다 평판(reputation)에 직결된다. hard bounce를 계속 시도하거나 complaint를 방치하면, SES는 계정의 발송 자체를 제한한다. 그래서 애플리케이션 쪽에서 이 SNS 알림을 구독해서 “다음부터 이 주소로는 보내지 않기” 처리를 해줘야 한다 — SES가 자동으로 알아서 걸러주는 게 아니라, 알림을 받는 건 내 몫이라는 뜻이다.
지금 작업에서 처음에 마케팅 용도로 ses를 활성화 했지만 프로덕션 요청시 수많은 검증을 요청 받았다. 이메일 관리 어떻게 하나 구독 여부 어떻게 처리하나 예시 보여달라 등등 여기에 답해야할 예정이었지만 중간에 확인해보니 트랜잭션(주문, pw 변경등)에 처리하는 경우만 있다고 해서 변경해서 aws 서포트에 회신했다.
결과 마케팅용도가 아니니까 바로 프로덕션 허용됨!
위 내용 말고 추가적으로 메일 프로토콜은 더 많은 내용이 있겠지만 AWS 환경에서 사용하기에는 위 지식으로 충분할 것으로 예상되어 여기까지 작성했다.
더 필요하면 공부해야지…

