메일 통합 서비스 OAuth 토큰 보안 완전가이드
TL;DR
- 메일 통합 서비스는 OAuth 위임, refresh token 저장, 메일 본문 처리까지 한 번에 다루므로 일반 API 연동보다 보안 경계가 넓다.
- REST API와 IMAP XOAUTH2는 접근 방식은 다르지만 둘 다 장기 토큰 보호, TLS, 키 관리, 재인증 설계가 핵심이다.
- 실무 기준선은 AES-256-GCM, KMS 기반 envelope encryption, 짧은 수명 토큰, 로그 마스킹, invalid_grant 감지다.
1. 개념
메일 통합 서비스의 OAuth 토큰 보안은 사용자가 위임한 메일 접근 권한을 서버가 장기간 안전하게 보관·갱신·사용하는 전체 설계다. 핵심 범위는 Gmail API·Microsoft Graph·IMAP XOAUTH2 같은 접근 방식, refresh token 저장 보안, TLS 기반 통신 보호, 메일 본문 처리 시의 데이터 보호까지 포함한다.
2. 배경
메일 통합 서비스는 메일 서버를 직접 운영하지 않으면서도 사용자 메일을 대신 읽고 보내야 한다. 이 구조에서는 access token보다 refresh token이 더 오래 살아남고, 한 번 유출되면 사용자의 메일함 전체 권한으로 이어질 수 있어 일반적인 웹 세션 보안보다 더 강한 저장·회전·감사 전략이 필요하다.
3. 이유
이 주제를 정확히 이해하지 못하면 IMAP과 REST API의 차이를 혼동하거나, 토큰을 평문 저장하거나, TLS만 켜고 저장소 보안은 비워 두는 식의 설계 오류가 반복된다. 메일 통합 서비스는 인증, 암호화, 키 관리, 네트워크 보안, 운영 모니터링이 연결된 문제라서 한 문서 단위의 통합 이해가 필요하다.
4. 특징
- IMAP XOAUTH2, Gmail API, Microsoft Graph를 같은 위협 모델 관점에서 비교
- access token과 refresh token의 수명, 폐기 조건, 재인증 플로우를 함께 설명
- AES-256-GCM, envelope encryption, KMS, AAD 바인딩 같은 저장 보안 패턴 정리
- TLS, MTA-STS, STARTTLS, S/MIME, SPF·DKIM·DMARC까지 메일 통신 계층 전반 연결
- 운영 체크리스트, 안티패턴, 사고 사례까지 포함한 실무형 가이드
5. 상세 내용
메일 통합 서비스의 OAuth · 토큰 저장 · 통신 보안 완전 가이드
작성일: 2026-05-13 대상: 시니어 백엔드 엔지니어 / 메일 통합 서비스 운영자 범위: Gmail API / Microsoft Graph / IMAP 프로토콜 / OAuth 2.0 / 토큰 저장 보안 / 이메일 통신 보안 전반
0. 이 문서가 다루는 것
“내가 직접 메일 서버를 운영하지는 않지만, 사용자의 메일을 대신 읽고/보내고/태깅하는 서비스”를 만들 때 알아야 할 것들을 한 문서에 모았다.
크게 세 흐름으로 읽으면 된다:
- 메일에 어떻게 접근할 것인가? — IMAP/POP3 vs Gmail API vs Microsoft Graph
- 사용자 권한을 어떻게 위임받고 보관할 것인가? — OAuth 2.0과 refresh token
- 그 자격증명과 메일 내용을 어떻게 안전하게 다룰 것인가? — 암호학 / 통신 보안
목차
- 메일 통합 서비스란 무엇인가
- 메일 접근 방식: IMAP/POP3 vs REST API
- 이 메일 통합 서비스는 어떤 방식인가
- OAuth 2.0과 토큰의 메커니즘
- 암호학 기초 — 대칭키와 AES
- Refresh Token 저장 — 베스트 프랙티스
- 이메일 통신 보안 전반
- 한 페이지 정리
- 참고 자료
1. 메일 통합 서비스란 무엇인가
1.1 용어
“사용자 대신 메일 서버와 통신하는 백엔드 서비스”를 부르는 표준 용어는 맥락마다 다르다:
| 용어 | 사용 맥락 |
|---|---|
| MUA (Mail User Agent) | RFC 표준. Thunderbird/Outlook 같은 클라이언트가 전통적이고, 서버형 MUA도 포함 |
| Server-side MUA / Cloud MUA | 백엔드 서비스 형태의 MUA |
| Email Integration Service / Email Connector | 업계 일반 호칭 |
| AI Email Productivity Platform | 제품 카테고리 (Shortwave, Notion Mail, Superhuman 같은 형태) |
| OAuth Client (Confidential Client) | OAuth 사양 관점. client_secret을 보관하는 서버형 클라이언트 |
| Third-party Application | Google/MS 보안 정책 문서 표현. CASA 심사 대상 |
| Universal Email API / Mailbox Integration Platform | Nylas/Aurinko가 자칭하는 표현 |
1.2 이 카테고리의 공통점
- 메일 서버를 직접 운영하지 않음 — Gmail / Outlook / Naver 등의 메일을 사용자 위임받아 사용
- OAuth 2.0으로 권한 위임받음
- refresh token을 영구 보관할 수밖에 없음 (이게 모든 보안 설계의 출발점)
- 사용자 메일 본문을 가공·분류·검색·요약·발송 보조
→ Shortwave, Superhuman, Notion Mail, Front, Hey, Sanebox 같은 서비스들이 모두 같은 카테고리다.
2. 메일 접근 방식: IMAP/POP3 vs REST API
2.1 두 방식의 본질적 차이
| 차원 | IMAP/POP3 (프로토콜) | Gmail API / MS Graph (REST API) |
|---|---|---|
| 전송 | TCP 직접 (993/995 포트) | HTTPS (443 포트) |
| 데이터 표현 | 명령어 + MIME 텍스트 스트림 | JSON 객체 |
| 상태 | Stateful (LOGIN → SELECT → FETCH 순서, 연결 유지) | Stateless (요청마다 토큰만 있으면 독립) |
| 작업 단위 | IMAP 커맨드 (FETCH, STORE, SEARCH) | HTTP 메서드 + URI |
| 식별자 | UID (폴더 내 정수, 폴더 이동 시 변경) | opaque ID (글로벌 unique) |
| 인증 | SASL (XOAUTH2 메커니즘) | OAuth 2.0 Bearer 토큰 헤더 |
같은 작업의 비교: “INBOX의 미읽음 메시지 가져오기”
# IMAP
C: a001 LOGIN ... (또는 AUTHENTICATE XOAUTH2 ...)
C: a002 SELECT INBOX
C: a003 UID SEARCH UNSEEN
S: * SEARCH 1234 1235 1236
C: a004 UID FETCH 1234 (BODY[])
# REST API
GET /gmail/v1/users/me/messages?q=is:unread&labelIds=INBOX
Authorization: Bearer ya29.xxx
GET /gmail/v1/users/me/messages/{id}
2.2 REST API가 더 좋다는 평가의 7가지 근거
| 이유 | 설명 |
|---|---|
| ① 방화벽 친화 | 443 하나만 열면 됨 |
| ② Stateless | 워커 수평 확장 자유. Gmail IMAP은 계정당 동시 연결 15개 한도 |
| ③ 풍부한 서버 측 메타데이터 | threadId, labelIds[], conversationId, categories, importance 등 IMAP에 없는 정보 제공 |
| ④ 강력한 서버 측 검색 | q=has:attachment from:boss after:2024/1/1 / Graph KQL — Google·MS 인덱스 활용 |
| ⑤ 효율적 증분 동기화 | Gmail History API, Graph Delta Query는 표준 기능 |
| ⑥ 진짜 푸시 | Gmail watch+Pub/Sub, Graph webhooks. IMAP IDLE은 폴더당 TCP 연결 점유 |
| ⑦ JSON + 표준 HTTP | 에러 코드·구조화·로깅·트레이싱 도구 호환 |
2.3 “IMAP 사형선고”라는 표현은 부정확하다
자주 들리는 말이지만 정확히는 IMAP에 비밀번호로 로그인하는 방식이 죽은 것. IMAP + OAuth 2.0(XOAUTH2 SASL) 조합은 Gmail/Outlook 모두 공식 지원하고 살아있다.
| 항목 | Gmail | Microsoft 365 (EXO) |
|---|---|---|
| IMAP + Basic Auth (비밀번호 직접) | 🪦 2025-03 사망 | 🪦 2022-10 사망 |
| IMAP + App Password (2FA 계정) | ⚠️ 개인 계정 한정, Workspace 관리자 차단 가능 | 🪦 사망 |
| IMAP + XOAUTH2 (OAuth 토큰) | ✅ 공식 지원, 살아있음 | ✅ 공식 지원, 살아있음 |
| SMTP + XOAUTH2 | ✅ 살아있음 | ✅ 살아있음 (신규 테넌트는 2026-12부터 기본 OFF) |
| EWS (SOAP API) | (해당없음) | ⏰ 2026-10 비활성화 → 2027-04 영구 종료 |
| POP3 + XOAUTH2 | ⚠️ 살아있음 | ⚠️ 살아있음 |
2.4 알아둬야 할 데드라인 (2026 기준)
| 시점 | 이벤트 | 영향 |
|---|---|---|
| 이미 종료 | EXO Basic Auth (IMAP/POP/EWS) | M365는 OAuth XOAUTH2 전용. 복구 불가 |
| 이미 종료 | Google Workspace Less Secure Apps (2025-03) | Gmail IMAP/SMTP는 OAuth만 |
| 2026-04-30 | EXO SMTP AUTH Basic Auth 100% 차단 | 레거시 SMTP 발송 마이그레이션 |
| 2026-10-01 | EWS 비활성화 개시 | EWS 기반 시스템 강제 폐기 |
| 2026-12 | 신규 M365 테넌트 SMTP AUTH 기본 OFF | 신규 배포 시 명시 활성화 |
| 2027-04 | EWS 영구 종료 | EWS Managed API 등 모두 폐기 |
2.5 그래서 어떤 방식을 선택할 것인가?
| 시나리오 | 권장 |
|---|---|
| Gmail 전용 자동화/CRM/에이전트 | Gmail API + Pub/Sub + History API |
| M365 전용 엔터프라이즈 | Microsoft Graph + Webhooks + Delta Query |
| 멀티 프로바이더 (Gmail + M365 + Naver 등) | IMAP XOAUTH2 + 어댑터 또는 Nylas/Aurinko 통합 API |
| EWS 레거시 운영 중 | 🚨 즉시 Graph 마이그레이션 (2027.04 종료) |
| 온프레미스 메일 서버 통합 | IMAP (REST 옵션 없음) |
| 대량 마케팅 발송 | 별도 ESP (SendGrid, Mailgun) |
3. 이 메일 통합 서비스는 어떤 방식인가
3.1 결론
이 메일 통합 서비스는 IMAP + XOAUTH2 + SMTP 방식이다. Gmail API / Microsoft Graph REST API는 사용하지 않는다.
3.2 코드 증거
Bazel 의존성 (raven/backend/BUILD.bazel)
# Maven Dependencies - Mail
"@cck_maven//:jakarta_mail_jakarta_mail_api",
"@cck_maven//:org_eclipse_angus_angus_mail", # IMAP/SMTP 클라이언트
"@cck_maven//:software_amazon_awssdk_ses", # AWS SES (발송 보조)
→ Gmail API SDK(google-api-services-gmail), Microsoft Graph SDK 없음.
IMAP 프로토콜 직접 사용 (ImapSyncService.kt)
setProperty("mail.store.protocol", "imaps")
setProperty("mail.imaps.host", account.imapHost)
setProperty("mail.imaps.port", account.imapPort.toString()) // 993
setProperty("mail.imaps.ssl.enable", "true")
setProperty("mail.imaps.ssl.protocols", "TLSv1.2")
props.setProperty("mail.imaps.auth.mechanisms", "XOAUTH2") // OAuth는 SASL 인증에만
props.setProperty("mail.imaps.sasl.enable", "true")
IMAP IDLE 실시간 알림
ImapIdleManager.kt,ImapIdleSession.ktorg.eclipse.angus.mail.imap.IMAPFolder사용
멀티 프로바이더 호스트 매핑 (OAuthService.kt)
GOOGLE → imap.gmail.com:993 / smtp.gmail.com:587
MICROSOFT → outlook.office365.com:993 / smtp.office365.com:587
NAVER → imap.naver.com:993 / smtp.naver.com:587
OAuth는 인증·프로필 조회에만 사용
googleapis.com/oauth2/v2/userinfo— 사용자 프로필graph.microsoft.com/v1.0/me— 사용자 프로필- → 메일 데이터는 REST가 아니라 IMAP으로 가져옴. OAuth access_token을 IMAP XOAUTH2 SASL에 주입.
3.3 합리적 선택이었던 이유
| 요인 | 효과 |
|---|---|
| 3개 프로바이더 (Google + Microsoft + Naver) 동시 지원 | 단일 IMAP 코드. Naver는 REST API 자체가 없음 |
| CASA / Restricted Scope 심사 회피 | Gmail mail.google.com 스코프 안 쓰면 보안 감사 면제 |
| 빠른 MVP | Jakarta Mail은 20년 검증된 라이브러리 |
| Naver 제약 | 한국 시장 필수인데 REST 옵션 없음 |
3.4 동시에 짊어진 트레이드오프
- Gmail 계정당 동시 IMAP 연결 15개 한도
- 스레딩 클라이언트 측 구현 필요 (
ThreadAssemblyService—References헤더 파싱) - History API / Delta Query 같은 효율적 증분 동기화 불가 — 폴링 + IDLE 의존
- MIME 직접 파싱 (
MimeDecoder.kt) - 첨부 처리도 BODYSTRUCTURE 직접 다룸
4. OAuth 2.0과 토큰의 메커니즘
4.1 두 종류의 토큰
| 토큰 | 수명 | 용도 |
|---|---|---|
| access_token | ~1시간 (3,600초) | 실제 API 호출 시 인증 자격증명 |
| refresh_token | 사실상 무기한 (조건부 무효화) | access_token 갱신용 |
| id_token (OIDC) | ~1시간 | 사용자 식별, 메일 호출엔 불필요 |
4.2 토큰 발급 흐름
[초기 1회 — 사용자 OAuth 동의]
사용자 → Google 동의 화면 → ?code=AUTH_CODE
서버 → POST oauth2.googleapis.com/token
grant_type=authorization_code
access_type=offline ← 이게 있어야 refresh_token 발급
← { access_token, refresh_token, expires_in: 3600 }
💾 refresh_token을 DB에 암호화 저장
💾 access_token은 캐시/메모리
[매 1시간 — 자동 갱신, 사용자 모름]
서버 → POST oauth2.googleapis.com/token
grant_type=refresh_token
refresh_token=...
client_id=..., client_secret=...
← { access_token, expires_in: 3600 }
주요 OAuth 파라미터:
access_type=offline— 안 넣으면 refresh_token이 안 옴 (가장 흔한 함정)prompt=consent— 이미 동의한 사용자에 강제로 refresh_token 재발급- 같은 client_id로 두 번째 OAuth는 기본적으로 refresh_token을 안 보냄
4.3 refresh_token이 무효화되는 6가지 조건 (Gmail 기준)
| 조건 | 결과 |
|---|---|
| 6개월간 사용 안 함 | 자동 만료 |
| 비밀번호 변경 (Gmail scope 포함 시) | 즉시 무효화 |
| 사용자가 myaccount.google.com에서 앱 해제 | 즉시 무효화 |
| 같은 (client_id, user) 토큰 50개 초과 | LRU로 가장 오래된 것 무효화 |
| OAuth 앱이 “Testing” 상태 (미공개) | 7일 후 만료 |
| OAuth scope 변경 | 기존 토큰 무효화 |
감지 방법: refresh 호출 시 HTTP 400 + error: "invalid_grant". 재시도 금지, 사용자 재인증 요청 플로우로 전환.
4.4 IMAP에서 OAuth는 어떻게 작동하나 (SASL XOAUTH2)
“Bearer 토큰을 어떻게 IMAP에?” — TCP 위의 SASL
IMAP은 평문 텍스트 명령어 프로토콜이지만 OAuth 토큰을 운반하는 방법이 따로 있다. SASL AUTHENTICATE 명령어의 인자로 넣는다.
[TLS 핸드셰이크 후 TCP 안에서 평문 IMAP 대화]
C: a002 AUTHENTICATE XOAUTH2 dXNlcj11c2VyQGdtYWlsLmNvbQFhdXRoPUJlYXJlciB5YTI5...AQE=
└────── base64로 인코딩된 SASL 페이로드 ──────┘
S: a002 OK Authenticated.
C: a003 SELECT INBOX
C: a004 UID FETCH 1500:* (BODY[HEADER])
base64 페이로드를 풀면:
user=user@gmail.com\x01auth=Bearer ya29.AB...\x01\x01
└─────────────┘ │ └──────────────┘ │ │
사용자명 SOH OAuth 토큰 SOH SOH
→ access_token이 Bearer ... 형식 그대로 SASL 페이로드 안에 들어감. HTTP Authorization 헤더와 개념적으로는 같지만 운반 매체가 다름.
SASL ≠ XOAUTH2 (혼동 주의)
SASL = 인증 프레임워크, XOAUTH2 = 그 위의 한 가지 메커니즘. “XOAUTH2 ⊂ SASL” 부분집합 관계.
Application Protocol (IMAP, SMTP, XMPP, LDAP...)
↓ uses
SASL framework (RFC 4422) ← 인터페이스
↓ implements
Mechanisms: ← 구현체들
PLAIN, LOGIN, CRAM-MD5, DIGEST-MD5,
SCRAM-SHA-256, GSSAPI, EXTERNAL,
★ XOAUTH2 (Google spec) ★
★ OAUTHBEARER (RFC 7628 정식 표준) ★
mail.imaps.sasl.enable=true→ SASL 프레임워크 켜기mail.imaps.auth.mechanisms=XOAUTH2→ 그 중 OAuth 메커니즘 선택
만약 auth.mechanisms=PLAIN이면 같은 SASL을 쓰지만 비밀번호 평문(TLS 안)을 보내는 메커니즘이 됨.
“처음 인증 후엔 그냥 IMAP” 맞다, 다만…
IMAP은 stateful이라 AUTHENTICATE 한 번 성공하면 그 TCP 연결 동안은 재인증 불필요. FETCH/STORE/IDLE은 토큰 없이 진행.
문제는 그 연결이 영원하지 않다는 점:
| 상황 | 결과 |
|---|---|
| TCP 연결 정상 (IDLE 유지) | 토큰 재사용 |
| NAT/방화벽 타임아웃 (5~15분) | 연결 끊김 → 재연결 → 새 AUTHENTICATE → 토큰 필요 |
| IDLE 29분 갱신 (RFC 권장) | 같은 연결이라 토큰 재사용 |
| access_token 만료 (1시간) | 다음 명령에서 NO 응답 → 재인증 |
| 멀티 폴더 모니터링 | 폴더당 별도 TCP 연결 → 각각 토큰 |
| 워커 재시작/배포 | 모든 연결 끊김 → 전부 재인증 |
→ refresh_token이 살아있어야 다음 연결 만들 때 access_token을 갱신할 수 있음. REST API든 IMAP+XOAUTH2든 refresh_token 저장 필요성은 동일.
5. 암호학 기초 — 대칭키와 AES
5.1 대칭 vs 비대칭
| 항목 | 대칭키 (AES, ChaCha20) | 비대칭키 (RSA, ECC, EdDSA) |
|---|---|---|
| 키 개수 | 1개 (공유 비밀) | 2개 (공개키 + 개인키) |
| 암/복호화 키 | 같음 | 다름 (공개키로 암호화 → 개인키로 복호화) |
| 속도 | 매우 빠름 (수 GB/s) | 느림 (수 MB/s, 1000배 차이) |
| 용도 | 대용량 데이터 암호화 | 키 교환, 디지털 서명 |
| 키 길이 | 128/192/256 bit | RSA 2048~4096 bit, ECC 256 bit |
| 키 공유 문제 | 미리 안전하게 공유 필요 | 공개키는 마음대로 배포 |
실무는 둘 다 결합 (하이브리드 암호화): TLS, KMS Envelope Encryption, PGP, JWE 모두 비대칭으로 대칭키를 교환하고, 그 후로는 대칭키로 데이터를 암호화한다. 비대칭이 느리니까 키 교환에만 한 번 쓰고 본 작업은 빠른 대칭이 맡는 패턴.
5.2 AES-256-GCM 분해
AES - 256 - GCM
│ │ │
│ │ └── 운용 모드 (Mode of Operation)
│ │ GCM = Galois/Counter Mode
│ │ AEAD (인증 암호화), 무결성 태그 포함
│ │
│ └── 키 길이 (bits)
│ 128 / 192 / 256 중 256
│ = 32 bytes
│
└── 알고리즘 (Advanced Encryption Standard)
= 대칭키 블록 암호 (블록 크기 128-bit 고정)
5.3 “대칭키는 약하다”는 오해
이 인상은 옛 대칭키(DES, RC4) 사망 사례에서 옴. AES는 그 교훈으로 만든 후속작.
| 알고리즘 | 등장 | 키 길이 | 운명 |
|---|---|---|---|
| DES | 1977 | 56-bit | 1998년 22시간 만에 brute force, 사망 |
| 3DES | 1995 | 112-bit | 2023년 NIST 폐기 |
| RC4 | 1987 | 가변 | 2015년 TLS 금지 |
| MD5 (해시) | 1991 | - | 충돌 공격으로 사망 |
→ “대칭키가 약한 게” 아니라 “56-bit 키가 약한 것”. 키 공간이 짧으면 컴퓨터 발전이 따라잡음.
AES-256은 왜 안 깨지나:
- 키 조합 = 2²⁵⁶ ≈ 10⁷⁷ (우주 원자 수 ≈ 10⁸⁰)
- 지구 모든 컴퓨터를 우주 나이 동안 돌려도 극히 일부만 탐색
- Landauer 한계로 계산해도 brute force 에너지는 태양 출력의 수십억 배
- 20년 공개 분석에 노출되었지만 알고리즘 자체 미해독
- Biclique 공격(2011): 2²⁵⁴.⁴로 줄임 — 여전히 비실용
- 양자컴퓨터(Grover)도 실효 128-bit로만 약화 — 여전히 안전
- NSA가 TOP SECRET 데이터 보호에 인증
5.4 그럼에도 보안 사고가 일어나는 이유 — 알고리즘이 아니라 운용
| 무엇이 깨졌나 | 사례 |
|---|---|
| 키 관리 | 환경변수에 마스터 키 → printenv 노출 |
| 잘못된 모드 | AES-ECB → 같은 평문이 같은 ciphertext로 패턴 노출 |
| IV/Nonce 재사용 | GCM에서 같은 nonce + 같은 key → 키 복구 가능 (치명) |
| 인증 검증 누락 | AES-CBC(인증 없음) → padding oracle 공격 |
| 사이드 채널 | 캐시 타이밍, 전력 분석 → 키 추출 |
| 약한 RNG | 키 생성에 약한 난수 → 키 공간 축소 |
| 소셜 엔지니어링 | 키 가진 사람 협박/매수 (XKCD #538 $5 렌치 공격) |
한 줄: 알고리즘은 정답. 게임은 그 키를 어떻게 다루느냐다.
6. Refresh Token 저장 — 베스트 프랙티스
6.1 왜 저장이 필수인가
- access_token이 1시간마다 만료
- 백그라운드 cron/scheduler/webhook은 사용자 부재에서 토큰 갱신 못 함
- Gmail watch는 7일 유지인데 갱신할 토큰이 없으면 알림 끊김
- OAuth 2.0 자체가 “한 번 동의받고 그 후엔 서버끼리 갱신” 모델
6.2 위협 모델
refresh_token은 bearer credential — 소지자가 곧 권한자 (RFC 6750). 패스워드보다 위험한 이유:
| 항목 | 패스워드 유출 | Refresh Token 유출 |
|---|---|---|
| MFA 우회 | MFA로 차단 가능 | 이미 MFA 통과 상태 |
| 로그인 실패 흔적 | 실패 시도 로그 남음 | 정상 API 호출로 보임 |
| 지리적 이상 탐지 | 가능 | 어려움 |
| 패스워드 변경으로 차단 | 즉시 무효화 | Google: Gmail scope 포함 시만, MS: 부분적 |
알려진 사고 사례:
- GitHub OAuth 토큰 유출 (2022.04) — Heroku 내부 DB에서 고객 OAuth token 탈취 → 수십 개 조직 private repo 유출
- CircleCI (2023.01) — 직원 노트북 악성코드 → SSO 토큰 탈취 → 고객 환경변수/OAuth/SSH 키 전부 노출
- Vercel/Context.ai (2026.04) — Lumma Stealer → Google Workspace OAuth 탈취 → MFA 우회 → 고객 환경변수 노출
6.3 저장 위치 비교
| 방식 | 보안 | 성능 | 키 회전 | 비용 | 운영 |
|---|---|---|---|---|---|
| HSM / Cloud HSM | ★★★★★ | 낮음 | 수동 | 높음 | 매우 높음 |
| Cloud KMS + 앱 암호화 | ★★★★☆ | 중간 | 자동 | 중간 | 중간 |
| Secrets Manager | ★★★★☆ | 낮음 (API 호출) | 자동 | 사용자 많으면 비쌈 | 낮음 |
| DB 컬럼 + Envelope Encryption | ★★★★☆ | 높음 | 가능 | 낮음 | 중간 |
| DB 컬럼 + 앱 레벨 대칭키 | ★★★☆☆ | 높음 | 어려움 | 최저 | 낮음 |
| TDE만 | ★★☆☆☆ | 높음 | 제한적 | 낮음 | 최저 |
| 평문 DB 컬럼 | ★☆☆☆☆ | 최고 | N/A | 없음 | 없음 |
메일 통합 SaaS 표준: DB 컬럼 + KMS Envelope Encryption (수만 사용자 이상이면 Secrets Manager는 비쌈).
6.4 Envelope Encryption — 표준 패턴
비유: 금고 안의 금고
🔐 KMS 마스터 키 (CMK) ─ KMS HSM 안에만 있음. 절대 안 빠져나옴
│
│ KMS가 만들어준 일회용 열쇠
▼
🔑 DEK (Data Encryption Key) ─ 우리가 토큰 암호화에 쓰는 진짜 열쇠
│
│ 이 열쇠로 잠금
▼
🎫 refresh_token (평문) ─ 보호해야 할 것
DB에 저장되는 4개 컬럼 의미
oauth_tokens 테이블의 한 행:
┌─────────────────┬──────────────────────────────────────────────┐
│ account_id │ 12345 │ ← 누구 토큰인지
├─────────────────┼──────────────────────────────────────────────┤
│ encrypted_token │ Base64(IV ‖ AES_GCM(DEK, refresh_token)) │ ← 잠긴 토큰
├─────────────────┼──────────────────────────────────────────────┤
│ encrypted_dek │ Base64(KMS_Encrypt(CMK, DEK)) │ ← 잠긴 열쇠
├─────────────────┼──────────────────────────────────────────────┤
│ kms_key_id │ arn:aws:kms:ap-northeast-2:...:key/abc-123 │ ← 어느 금고
└─────────────────┴──────────────────────────────────────────────┘
| 컬럼 | 의미 | 없으면 |
|---|---|---|
encrypted_token |
DEK로 잠근 토큰 | 토큰 어디 저장? 평문이면 끝장 |
encrypted_dek |
CMK로 또 잠근 DEK | 평문 DEK를 DB에 두면 envelope 무의미 |
kms_key_id |
어느 KMS 키로 잠갔는지 | 키 회전·멀티 리전에서 어느 키로 풀지 모름 |
왜 DEK 중간 단계가 필요한가?
직접 KMS로 토큰 암호화 안 하는 이유:
- KMS Encrypt API는 평문 최대 4KB 제한
- 성능 — 토큰 사용할 때마다 KMS API 왕복 레이턴시
- 비용 — KMS 호출당 과금
- 가용성 — KMS 장애 = 모든 토큰 사용 불가
- 레이트 리밋 — KMS 리전당 호출 한도
Envelope Encryption은 KMS 호출은 작은 DEK에만 하고, 실제 토큰 암호화는 빠른 로컬 AES로 처리.
KMS 내부 동작 (GenerateDataKey)
앱: KMS.GenerateDataKey(KeyId=CMK_ARN, KeySpec=AES_256)
↓
KMS HSM 내부:
1. SecureRandom으로 32 bytes 랜덤 생성 → 평문 DEK
2. CMK로 그 DEK를 AES-256-GCM으로 암호화
3. 두 개 다 응답
↓
응답: {
Plaintext: <32 bytes 평문 DEK> ← 앱이 한 번 받고 폐기
CiphertextBlob: <CMK로 잠긴 DEK + 메타데이터> ← 앱이 DB에 보관
}
CMK도 대칭키, 같은 AES
┌──────────────────────────────────────────────────────────┐
│ Layer 3: refresh_token (보호 대상) │
│ 알고리즘: AES-256-GCM │
│ 키: DEK (32 bytes, 일회용) │
│ 위치: 암호화된 채로 DB │
└────────────────────┬─────────────────────────────────────┘
│ DEK로 잠금
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 2: DEK │
│ 알고리즘: AES-256-GCM (KMS 내부 구현) │
│ 키: CMK (32 bytes, 영구) │
│ 위치: 평문은 메모리 잠시 / 잠긴 DEK는 DB │
└────────────────────┬─────────────────────────────────────┘
│ CMK로 잠금
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 1: CMK (Customer Master Key) │
│ 알고리즘: AES-256 │
│ 키 보호: HSM 하드웨어 (FIPS 140-2 Lv3) │
│ 위치: AWS KMS HSM 안. 절대 외부 노출 X │
└──────────────────────────────────────────────────────────┘
→ 알고리즘은 다 AES다. 차이는 키의 신뢰 경계 — CMK는 HSM 하드웨어 안에 영원히 갇혀 있어 코드/DB 유출되어도 안전.
6.5 공격 시나리오로 보는 envelope encryption 가치
DB만 유출 (가장 흔한 사고)
공격자가 DB 덤프 획득:
✅ encrypted_token ← 깡통 (DEK 없이 못 품)
✅ encrypted_dek ← 깡통 (CMK 없이 못 품)
✅ kms_key_id ← 주소만
→ KMS API 권한 없으면 무용지물. 토큰 보호 ✓
DB + IAM credentials 같이 유출 (드물지만)
공격자가 둘 다 획득:
encrypted_dek + kms_key_id → KMS.Decrypt() → 평문 DEK
평문 DEK + encrypted_token → AES_GCM_Decrypt → 토큰 노출
→ 그래서 KMS 접근 권한 분리(Bastion 패턴), CloudTrail 감사 추가
6.6 AAD 바인딩 (크로스 테넌트 공격 차단)
GCM의 AAD(Additional Authenticated Data)에 accountId를 바인딩하면, 공격자가 사용자 A의 ciphertext를 사용자 B 레코드로 복사해도 AAD 불일치로 복호화 실패.
cipher.init(ENCRYPT_MODE, secretKey, GCMParameterSpec(...))
cipher.updateAAD(accountId.toByteArray()) // ← 어카운트 바인딩
val ciphertext = cipher.doFinal(token.toByteArray())
복호화 시 동일한 AAD 필요 → 다른 계정에 옮겨심으면 실패.
6.7 키 회전
| 케이스 | 방법 |
|---|---|
| 정기 회전 | AWS KMS CMK 자동 회전 (기본 연 1회). 이전 키 버전 보존, 새 암호화는 새 버전 사용 |
| 침해 의심 시 수동 회전 | KMS.ReEncrypt API로 평문 DEK 노출 없이 새 CMK로 재포장 |
| OAuth refresh token 자체 회전 | Google: 회전 안 함(사용 시 수명 연장). MS: 매 사용 시 새 토큰 발급 + 구 토큰 무효 — 원자적 DB 업데이트 필수 |
6.8 운영/모니터링
| 항목 | 설명 |
|---|---|
invalid_grant 메트릭 |
월 1% 초과면 비정상. 시간당 10배 급증 = 침해 또는 배포 오류 |
| 토큰 폐기 (revoke) | 계정 해지 시 oauth2.googleapis.com/revoke 또는 MS revoke endpoint 호출 |
| 휴면 정리 | 180일 미사용 토큰 자동 폐기. Google은 6개월 미사용 자동 만료 |
| 이상 사용 탐지 | 새벽 시간 다량 발송, 지리적 불일치, 분당 호출량 급증 |
| 사용자 재인증 UX | invalid_grant 감지 → 계정 상태 REAUTH_REQUIRED → 사용자에게 재인증 링크 (토큰 값 자체는 절대 알림에 포함 X) |
| CloudTrail 감사 | 모든 KMS 복호화 요청 자동 로깅 |
6.9 안티패턴 (절대 금지)
| 안티패턴 | 위험도 | 설명 |
|---|---|---|
| 평문 DB 컬럼 저장 | 🔴 치명 | SQL injection 한 번 = 전체 유출 |
| 환경변수에 마스터 키 | 🔴 치명 | printenv, K8s etcd 평문 노출 |
Git에 .env 커밋 |
🔴 치명 | 히스토리 영구 기록 |
| 로그에 토큰 출력 | 🔴 치명 | ELK 같은 집계 시스템 노출 |
| Query String에 토큰 | 🔴 치명 | 브라우저 히스토리, Referrer, 액세스 로그 기록 |
| localStorage에 refresh token | 🔴 치명 | XSS 한 번 = 즉시 탈취 |
| 클라이언트(앱/브라우저)에 refresh token 노출 | 🔴 치명 | 서버사이드 통합에서 절대 금지 |
| 단일 앱 레벨 대칭키 (KMS 없이) | 🟠 위험 | 키 유출 시 전체 노출 |
| Kubernetes Secret 평문 etcd 저장 | 🟠 위험 | etcd 백업 유출 시 노출 |
| TDE만 믿고 끝 | 🟡 주의 | DB 자격증명 탈취 = 평문 노출 |
6.10 체크리스트 (Must / Should / Nice)
Must (신규 구축 즉시)
- AES-256-GCM 암호화 (평문 저장 금지)
- 키를 KMS / Secrets Manager로 관리 (소스/환경변수 금지)
- RDS TDE 활성화
- TLS 1.2+ 전송 (내부 통신 포함)
- CloudTrail KMS 감사
- 로그 토큰 마스킹
- 환경별(dev/staging/prod) 키 분리
invalid_grant감지 + 재인증 UX- 서비스 해지 시 revoke endpoint 호출
- MS refresh token rotation 원자적 업데이트
Should (분기 내)
- Envelope Encryption (KMS DEK + 앱 AES-GCM)
- AAD에 accountId 바인딩
- 복호화 마이크로서비스 격리 (Bastion 패턴)
- KMS 자동 회전 활성화
- 휴면 토큰 자동 폐기
- 이상 사용 탐지
- K8s External Secrets Operator
- JVM 힙 덤프 차단 (
-XX:+DisableAttachMechanism) - 처리 후 ByteArray 제로화
Nice (분기 로드맵)
- Per-tenant KMS 키 (BYOK)
- 데이터 거주성 (멀티 리전 KMS)
- Vault Transit Secret Engine
- 침투 테스트 (연간)
- SOC 2 Type II
- Cross-Account Protection
7. 이메일 통신 보안 전반
7.1 4개의 독립된 보안 계층
[발신자] [수신자]
│ │
│ ① 클라이언트 ↔ 메일서버 보안 │
│ (IMAP/SMTP/REST API 위의 TLS) │
▼ ▼
[발신자 메일서버] ──② 서버 간 전송 보안 (MTA-to-MTA)── [수신자 메일서버]
(STARTTLS, MTA-STS, DANE)
┌─────────────────────────────────────────────────┐
│ ③ 메시지 자체 보안 (S/MIME, PGP) │ ← 거의 안 씀
│ ④ 도메인 인증 (SPF, DKIM, DMARC) │ ← 위·변조 방지
└─────────────────────────────────────────────────┘
중요: 이메일은 본질적으로 store-and-forward 모델 — 양 끝 사용자 간 보장이 없다. 각 hop 사이만 보호되며, 메일서버는 본문을 평문으로 본다 (Gmail이 광고·검색·스팸 필터 돌리려면 평문 접근 필수).
7.2 계층 ① — 클라이언트 ↔ 메일서버
IMAP/POP3/SMTP은 TLS로 감쌈
일반 TCP
↓ TLS 1.2 또는 1.3 핸드셰이크
암호화된 byte stream
↓ 그 안에 IMAP/SMTP 평문 명령
"AUTHENTICATE XOAUTH2 ..."
"SELECT INBOX"
"FETCH ..."
| 프로토콜 | 평문 포트 | TLS 포트 | 모드 |
|---|---|---|---|
| IMAP | 143 | 993 | Implicit TLS |
| POP3 | 110 | 995 | Implicit TLS |
| SMTP (submission) | 587 | (465 deprecated) | STARTTLS |
| SMTP (서버 간) | 25 | - | STARTTLS (선택적) |
Implicit TLS vs STARTTLS
- Implicit: TCP 연결 즉시 TLS 핸드셰이크. 평문 와이어 없음. → IMAP 993, POP3 995
- STARTTLS: 평문 시작 →
STARTTLS명령 → TLS 협상. STARTTLS Stripping 공격 가능 (MITM이 STARTTLS 응답 제거해서 평문 강요). RFC 8314 권장: STARTTLS 실패 시 무조건 연결 거부.
→ 이 메일 통합 서비스는 implicit IMAPS를 사용한다 (mail.imaps.ssl.protocols=TLSv1.2, port 993).
REST API (HTTPS)
- gmail.googleapis.com, graph.microsoft.com 모두 TLS 1.2 이상만 허용
- HSTS 적용, 다운그레이드 공격 불가
- 평문 모드 자체가 존재 안 함
TLS가 보호하는 것 / 못 하는 것
| 보호 ✓ | 보호 못 함 ✗ |
|---|---|
| 와이어 도청 | 메일서버가 내용을 보는 것 (당연) |
| MITM (인증서 검증 시) | 인증서 검증 안 하면 무력 |
| 토큰·패스워드·메일 본문 노출 | DNS leak, SNI leak |
| 메시지 변조 | 메일서버 자체 침해 |
7.3 계층 ② — 메일서버 간 전송 (MTA-to-MTA)
발신 메일서버 → 수신 메일서버 사이 SMTP. 여러 hop을 거치므로 매 hop마다 TLS 필요.
STARTTLS Opportunistic의 한계
- TLS 안 되면 평문 폴백 (호환성 우선)
- MITM이 STARTTLS 응답 제거 가능 → 평문 강요
- 일부 작은 서버는 인증서 검증 미흡
강제 TLS 표준들
| 표준 | 동작 | 채택률 |
|---|---|---|
| MTA-STS (RFC 8461) | 수신 도메인이 TXT 레코드 + HTTPS 정책으로 “TLS 필수” 선언 | Gmail, Outlook, Yahoo 지원 |
| DANE (RFC 7672) | DNSSEC + TLSA로 인증서 핀 공개 | 적음. MS 2024 outbound DANE |
| TLS-RPT (RFC 8460) | TLS 전송 실패 리포팅 | MTA-STS와 짝 |
→ Gmail ↔ Outlook 사이는 사실상 TLS 1.2+ 강제. 작은 메일서버 사이만 평문 가능성.
헤더에서 hop별 TLS 확인 가능
Received: from mx.google.com (mx.google.com. [209.85.220.41])
by mail.example.com with ESMTPS id k12-...
(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
Mon, 13 May 2026 12:00:00 +0900
7.4 계층 ③ — 메시지 자체 암호화 (E2E)
서버를 신뢰하지 않고 발신자 → 수신자만 평문 접근 보장:
| 방식 | 키 모델 | 사용처 |
|---|---|---|
| S/MIME (RFC 5751) | X.509 인증서 (조직 CA) | 정부·대기업 사내 |
| PGP/GPG (RFC 4880) | Web of Trust 또는 키 직접 교환 | 보안 전문가, 저널리스트 |
| Google Workspace CSE | S/MIME + 외부 KMS | 일부 E5 고객 |
| Proton Mail | OpenPGP 자동 | 소비자용 보안 메일 |
현실: 일반 사용자는 안 씀. UX가 끔찍하고 Gmail/Outlook은 본문이 평문이어야 광고·검색·AI 기능 가능.
→ 이런 통합 서비스는 본문을 평문으로 IMAP/REST 받아 처리한다. E2E는 다루지 않는다.
7.5 계층 ④ — 도메인 인증 (위·변조 방지)
“내용 숨김”이 아니라 “이 메일이 진짜 그 도메인에서 왔는가” 검증.
| 표준 | 역할 |
|---|---|
| SPF (RFC 7208) | “내 도메인 메일은 이 IP들에서만” DNS TXT |
| DKIM (RFC 6376) | 발송 시 도메인 개인키로 서명. 수신자가 DNS 공개키로 검증 |
| DMARC (RFC 7489) | SPF/DKIM 실패 시 정책(quarantine/reject) + 리포팅 |
| BIMI (RFC 9162) | DMARC 통과 + 인증된 로고 표시 |
7.6 TLS 안에서 실제로 일어나는 일
[TLS 핸드셰이크]
1. ClientHello (지원 cipher suites 제안)
2. ServerHello (선택된 cipher suite, 예: TLS_AES_256_GCM_SHA384)
3. 인증서 전송 (X.509로 서버 신원)
4. 키 교환 (ECDHE / X25519, 비대칭으로 대칭키 합의)
5. Finished (양쪽 핸드셰이크 검증)
[TLS 세션]
─ 합의된 AES-256-GCM 같은 대칭키로 모든 데이터 암호화
─ Perfect Forward Secrecy: 세션 키 매번 새로, 서버 개인키 유출돼도 과거 트래픽 안전
─ TLS 1.3: RSA 키 교환 금지, ECDHE만, AEAD cipher만, SHA-1/MD5/RC4 금지
→ 메일 본문이 TLS 안을 흐를 때:
- 알고리즘: AES-256-GCM 또는 ChaCha20-Poly1305 (둘 다 AEAD 대칭)
- 키: 매 세션 ECDHE로 합의된 일회용 키
- 무결성: GCM 태그가 변조 자동 감지
- forward secrecy: 서버 개인키 유출 후에도 과거 통신 안전
7.7 “그래서 이메일은 안전한가” 솔직히
| 측면 | 답 |
|---|---|
| 와이어 도청? | TLS 적용 hop은 안전. opportunistic STARTTLS hop은 가능성 |
| 메일서버는 내용 보나? | 본다. Gmail은 광고·검색·AI 위해 평문 인덱싱 |
| 변조 방지? | TLS 내에서는 ✓. 저장된 메일은 서버 신뢰 |
| End-to-End? | 아니다. S/MIME/PGP 없으면 hop마다 평문 |
| 메일 통합 서비스는 본문 보나? | 본다. 사용자 권한으로 IMAP 접근 |
7.8 메일 통합 서비스 입장에서의 보안 점검 포인트
| 구간 | 무엇이 보호 | 확인할 것 |
|---|---|---|
| Gmail/Outlook ↔ 메일 통합 서비스 | TLS 1.2 over IMAPS (993) | mail.imaps.ssl.enable=true, mail.imaps.ssl.protocols=TLSv1.2 ✓ |
| 서비스 내부 메모리 | OS / 컨테이너 격리 | 힙 덤프 차단, 로그 본문 출력 방지 |
| 서비스 ↔ PostgreSQL | TLS (JDBC sslmode=require) |
datasource 설정 확인 |
| PostgreSQL 저장 | TDE + 선택적 컬럼 암호화 | RDS Encryption ON |
| 서비스 ↔ LLM API | TLS 1.3 over HTTPS | SDK 기본 |
| 서비스 ↔ AWS SES | TLS over HTTPS | SDK 기본 |
| 백업 | RDS 백업 암호화 | KMS 키 적용 |
8. 한 페이지 정리
알고리즘 / 계층 / 위치 매트릭스
| 데이터 | 위치 | 보호 알고리즘 | 키 보관 |
|---|---|---|---|
| 이메일 본문 (전송 중) | 클라이언트 ↔ 서버 | TLS 1.2/1.3 = AES-256-GCM | 매 세션 ECDHE 일회용 |
| 이메일 본문 (서버 ↔ 서버) | MTA-to-MTA | STARTTLS = AES-256-GCM | 매 세션 일회용 |
| 이메일 본문 (메일서버 저장) | Gmail/Outlook 서버 | 사업자 책임 | Gmail/MS가 관리 |
| 이메일 본문 (서비스 DB) | PostgreSQL | TDE (디스크) + 선택적 컬럼 | RDS KMS |
| OAuth access_token | 메모리/캐시 | (사용 즉시 폐기 권장) | 단명 |
| OAuth refresh_token | DB 컬럼 | AES-256-GCM (앱 레벨) | KMS Envelope (DEK + CMK) |
| KMS DEK | 메모리 잠시 / DB는 잠긴 채 | AES-256-GCM (KMS 내부) | CMK |
| KMS CMK | KMS HSM | AES-256 | HSM 하드웨어 격리 |
핵심 개념 한 줄씩
- REST API vs IMAP: stateless HTTP vs stateful TCP. REST는 풍부한 메타데이터·검색·푸시·증분 동기화. IMAP은 표준성·다중 프로바이더
- “IMAP 사형선고”는 부정확: Basic Auth만 사망. IMAP+XOAUTH2는 살아있음
- 이 메일 통합 서비스는 IMAP+XOAUTH2: Gmail/MS/Naver 동시 지원 + CASA 회피로 합리적 선택이지만, 그만큼 트레이드오프도 있다
- OAuth refresh_token 저장 필수: REST든 IMAP+XOAUTH2든 동일. access_token 1시간 갱신용
- SASL ≠ XOAUTH2: SASL은 인증 프레임워크, XOAUTH2는 그 위 메커니즘 중 하나
- AES-256-GCM = 대칭키 AEAD: 깨지지 않음 (10⁷⁷ 키 공간), 깨지는 건 운용
- Envelope Encryption: 잠긴 토큰 + 잠긴 DEK + CMK ARN. KMS 권한 없으면 DB 유출되어도 깡통
- AAD 바인딩: accountId를 GCM AAD로 → 크로스 테넌트 ciphertext 복사 공격 차단
- 이메일은 hop-by-hop TLS: 매 hop AES-256-GCM, 메일서버는 평문 본다. 진짜 E2E는 S/MIME/PGP만
우선순위 개선 액션 (메일 통합 서비스 신규 구축 시)
- 반드시 처음부터: AES-256-GCM + KMS Envelope + AAD 바인딩 + TLS 1.2+ + 로그 토큰 마스킹
- 분기 내: 복호화 Bastion 분리, CloudTrail 감사,
invalid_grant메트릭, 재인증 UX - 연간: SOC 2, 침투 테스트, per-tenant KMS, 데이터 거주성
9. 참고 자료
공식 RFC / IETF
- RFC 9700 - Best Current Practice for OAuth 2.0 Security (2025)
- RFC 6750 - OAuth 2.0 Bearer Token Usage
- RFC 6749 - OAuth 2.0 Authorization Framework
- RFC 4422 - SASL Framework
- RFC 7628 - OAUTHBEARER SASL Mechanism
- RFC 3501 - IMAP4rev1
- RFC 2177 - IMAP IDLE
- RFC 7162 - IMAP CONDSTORE/QRESYNC
- RFC 8461 - MTA-STS
- RFC 8314 - Cleartext Considered Obsolete
Provider 공식 문서
- Gmail API Overview
- Gmail OAuth Scopes
- Microsoft Graph Mail API
- Microsoft Refresh Tokens
- Deprecation of EWS in Exchange Online
- Deprecation of Basic Auth in Exchange Online
AWS / GCP
OWASP
보안 분석
- Refresh Token Security Best Practices (Obsidian)
- Google OAuth invalid_grant 분석 (Nango)
- Java Immutable Strings Security (Include Security)
사고 사례
- GitHub Security Alert - Stolen OAuth Tokens (2022)
- CircleCI 2023 Incident Report
- Vercel April 2026 Security Incident