Files
runbooks/aws-ses-setup.md
kappa bafc79c81b Improve security documentation based on Context7 review
aws-ses-setup.md:
- Add SPF records for email authentication
- Add DMARC policy configuration
- Add bounce/complaint handling with SNS
- Add DNS verification commands

n8n-setup-guide.md:
- Use official Docker registry (docker.n8n.io)
- Add N8N_ENCRYPTION_KEY requirement
- Add N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS
- Add N8N_PUBLIC_API_DISABLED option
- Add security headers to nginx config
- Add healthcheck configuration

gitea-setup.md:
- Add password policy (MIN_PASSWORD_LENGTH, PASSWORD_COMPLEXITY)
- Add argon2 password hashing
- Add DISABLE_GIT_HOOKS for security
- Add Docker Secrets configuration
- Add file-based secret management (SECRET_KEY_URI)
- Add REVERSE_PROXY_TRUSTED_PROXIES setting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 00:45:40 +09:00

376 lines
9.4 KiB
Markdown

# AWS SES 메일 발송 설정 가이드
> 작성일: 2026-01-12
> 리전: ap-northeast-2 (서울)
## 개요
AWS SES(Simple Email Service)를 사용하여 서버에서 메일을 발송하기 위한 설정 가이드입니다.
---
## 계정 상태
| 항목 | 값 |
|------|-----|
| 리전 | ap-northeast-2 (서울) |
| 모드 | **프로덕션** (샌드박스 해제됨) |
| 24시간 발송 한도 | 50,000통 |
| 초당 발송률 | 14통/초 |
---
## 인증된 도메인
| 도메인 | 도메인 인증 | DKIM |
|--------|------------|------|
| `anvil.it.com` | ✅ Success | ✅ Success |
| `ironclad.it.com` | ✅ Success | ✅ Success |
### 사용 가능한 발신자 주소
```
*@anvil.it.com (예: noreply@anvil.it.com, support@anvil.it.com)
*@ironclad.it.com (예: noreply@ironclad.it.com, admin@ironclad.it.com)
```
---
## 이메일 인증 (SPF, DKIM, DMARC)
> DMARC 준수를 위해 SPF와 DKIM 모두 설정 필요. 두 가지 중 하나라도 통과하면 DMARC 통과.
### 인증 방식 비교
| 방식 | 역할 | 검증 대상 |
|------|------|----------|
| **SPF** | 발신 서버 허용 목록 | MAIL FROM 도메인 |
| **DKIM** | 메일 서명 검증 | 메일 헤더 서명 |
| **DMARC** | SPF/DKIM 정책 통합 | From 도메인 정렬 |
---
## DNS 설정 (Cloudflare)
### anvil.it.com
**TXT 레코드 (도메인 소유권 확인)**
| Type | Name | Content |
|------|------|---------|
| TXT | `_amazonses.anvil.it.com` | `0cuw9v32N+aeFiNlTh2Poxglgzf3BlmFRjVOjeLEdy4=` |
**TXT 레코드 (SPF)** ⭐ 신규
| Type | Name | Content |
|------|------|---------|
| TXT | `anvil.it.com` | `v=spf1 include:amazonses.com ~all` |
**TXT 레코드 (DMARC)** ⭐ 신규
| Type | Name | Content |
|------|------|---------|
| TXT | `_dmarc.anvil.it.com` | `v=DMARC1; p=quarantine; rua=mailto:dmarc@anvil.it.com; pct=100` |
**CNAME 레코드 (DKIM)**
| Type | Name | Target |
|------|------|--------|
| CNAME | `dgcehnldehfmfgpvrrbc6drwasiibhnp._domainkey` | `dgcehnldehfmfgpvrrbc6drwasiibhnp.dkim.amazonses.com` |
| CNAME | `spopdscdt2sxngqzl5ir66k3ed6og7ut._domainkey` | `spopdscdt2sxngqzl5ir66k3ed6og7ut.dkim.amazonses.com` |
| CNAME | `55l5wnmktvacgyfpt6sovcgb2rqexrpy._domainkey` | `55l5wnmktvacgyfpt6sovcgb2rqexrpy.dkim.amazonses.com` |
### ironclad.it.com
**TXT 레코드 (도메인 소유권 확인)**
| Type | Name | Content |
|------|------|---------|
| TXT | `_amazonses.ironclad.it.com` | `C+RMHyLd/U2H5WSCu8L2avRn8NmuwEll0xxYjTyvoEY=` |
**TXT 레코드 (SPF)** ⭐ 신규
| Type | Name | Content |
|------|------|---------|
| TXT | `ironclad.it.com` | `v=spf1 include:amazonses.com ~all` |
**TXT 레코드 (DMARC)** ⭐ 신규
| Type | Name | Content |
|------|------|---------|
| TXT | `_dmarc.ironclad.it.com` | `v=DMARC1; p=quarantine; rua=mailto:dmarc@ironclad.it.com; pct=100` |
**CNAME 레코드 (DKIM)**
| Type | Name | Target |
|------|------|--------|
| CNAME | `5sycmzu364y2rgefnqaloptgodymasct._domainkey` | `5sycmzu364y2rgefnqaloptgodymasct.dkim.amazonses.com` |
| CNAME | `qm7d7qgkdikpcbrbgo7bmgqfmuulrbah._domainkey` | `qm7d7qgkdikpcbrbgo7bmgqfmuulrbah.dkim.amazonses.com` |
| CNAME | `tu6oey5tktoqub753cxpayhhwrlzcskk._domainkey` | `tu6oey5tktoqub753cxpayhhwrlzcskk.dkim.amazonses.com` |
### DMARC 정책 옵션
| 정책 | 설명 | 권장 단계 |
|------|------|----------|
| `p=none` | 모니터링만 (조치 없음) | 초기 테스트 |
| `p=quarantine` | 스팸함으로 이동 | 중간 단계 |
| `p=reject` | 수신 거부 | 최종 단계 |
---
## IAM 권한 설정
### 최소 권한 정책 (권장)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SESSendEmail",
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}
```
### 특정 발신자 제한 정책
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SESSendFromVerified",
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": [
"arn:aws:ses:ap-northeast-2:*:identity/anvil.it.com",
"arn:aws:ses:ap-northeast-2:*:identity/ironclad.it.com"
]
}
]
}
```
---
## 사용 방법
### AWS CLI
```bash
aws ses send-email \
--from "noreply@anvil.it.com" \
--to "recipient@example.com" \
--subject "메일 제목" \
--text "메일 본문" \
--region ap-northeast-2
```
### Python (boto3)
```python
import boto3
from botocore.exceptions import ClientError
def send_email(to_email: str, subject: str, body: str, sender: str = "noreply@anvil.it.com"):
client = boto3.client('ses', region_name='ap-northeast-2')
try:
response = client.send_email(
Source=sender,
Destination={'ToAddresses': [to_email]},
Message={
'Subject': {'Data': subject, 'Charset': 'UTF-8'},
'Body': {
'Text': {'Data': body, 'Charset': 'UTF-8'},
# HTML 본문 사용 시:
# 'Html': {'Data': '<h1>Hello</h1>', 'Charset': 'UTF-8'}
}
}
)
print(f"발송 완료: {response['MessageId']}")
return response
except ClientError as e:
print(f"발송 실패: {e.response['Error']['Message']}")
raise
# 사용 예시
send_email(
to_email='recipient@example.com',
subject='테스트 메일',
body='안녕하세요!'
)
```
### Python (HTML 메일)
```python
def send_html_email(to_email: str, subject: str, html_body: str, text_body: str = None):
client = boto3.client('ses', region_name='ap-northeast-2')
body = {'Html': {'Data': html_body, 'Charset': 'UTF-8'}}
if text_body:
body['Text'] = {'Data': text_body, 'Charset': 'UTF-8'}
response = client.send_email(
Source='noreply@anvil.it.com',
Destination={'ToAddresses': [to_email]},
Message={
'Subject': {'Data': subject, 'Charset': 'UTF-8'},
'Body': body
}
)
return response
# 사용 예시
html = """
<html>
<body>
<h1>안녕하세요!</h1>
<p>AWS SES로 발송된 <strong>HTML 메일</strong>입니다.</p>
</body>
</html>
"""
send_html_email('recipient@example.com', '테스트', html)
```
---
## 환경변수 설정 (컨테이너용)
```bash
export AWS_ACCESS_KEY_ID="your_access_key"
export AWS_SECRET_ACCESS_KEY="your_secret_key"
export AWS_DEFAULT_REGION="ap-northeast-2"
```
환경변수 설정 시 코드에서 자격증명 생략 가능:
```python
client = boto3.client('ses') # 환경변수에서 자동 로드
```
---
## 상태 확인 명령어
```bash
# 발송 활성화 상태
aws ses get-account-sending-enabled
# 발송 한도 확인
aws ses get-send-quota
# 등록된 Identity 목록
aws ses list-identities
# 도메인 인증 상태
aws ses get-identity-verification-attributes --identities anvil.it.com ironclad.it.com
# DKIM 상태
aws ses get-identity-dkim-attributes --identities anvil.it.com ironclad.it.com
```
---
## 새 도메인 추가 방법
### 1. SES 도메인 등록
```bash
aws ses verify-domain-identity --domain newdomain.com
aws ses verify-domain-dkim --domain newdomain.com
```
### 2. Cloudflare DNS 레코드 추가
출력된 토큰으로 TXT, CNAME 레코드 추가 (Cloudflare API 또는 대시보드)
### 3. 인증 확인
```bash
aws ses get-identity-verification-attributes --identities newdomain.com
aws ses get-identity-dkim-attributes --identities newdomain.com
```
---
## 반송/불만 처리 (SNS 연동)
> 반송(Bounce)과 불만(Complaint) 처리는 SES 평판 유지에 필수
### SNS 토픽 생성
```bash
# 반송 알림용 토픽
aws sns create-topic --name ses-bounces --region ap-northeast-2
# 불만 알림용 토픽
aws sns create-topic --name ses-complaints --region ap-northeast-2
```
### SES 알림 설정
```bash
# 반송 알림 연결
aws ses set-identity-notification-topic \
--identity anvil.it.com \
--notification-type Bounce \
--sns-topic arn:aws:sns:ap-northeast-2:ACCOUNT_ID:ses-bounces
# 불만 알림 연결
aws ses set-identity-notification-topic \
--identity anvil.it.com \
--notification-type Complaint \
--sns-topic arn:aws:sns:ap-northeast-2:ACCOUNT_ID:ses-complaints
```
### 반송 유형
| 유형 | 설명 | 조치 |
|------|------|------|
| **Hard Bounce** | 영구적 실패 (주소 없음) | 즉시 목록에서 제거 |
| **Soft Bounce** | 일시적 실패 (용량 초과) | 재시도 후 제거 |
| **Complaint** | 스팸 신고 | 즉시 목록에서 제거 |
### 권장 임계값
- **반송률**: 5% 미만 유지 (10% 초과 시 계정 정지 위험)
- **불만률**: 0.1% 미만 유지
---
## 인증 검증 명령어
```bash
# SPF 레코드 확인
dig TXT anvil.it.com +short
# DMARC 레코드 확인
dig TXT _dmarc.anvil.it.com +short
# DKIM 레코드 확인
dig CNAME dgcehnldehfmfgpvrrbc6drwasiibhnp._domainkey.anvil.it.com +short
```
---
## 참고 사항
- **프로덕션 모드**: 수신자 제한 없음
- **DKIM**: 메일 서명으로 위변조 방지
- **SPF**: 허용된 발신 서버 목록 정의
- **DMARC**: SPF/DKIM 정책 통합 및 리포트 수신
- **발송 한도**: 필요시 AWS에 증가 요청 가능
- **비용**: $0.10 / 1,000통
---
## 관련 링크
- [AWS SES 콘솔](https://ap-northeast-2.console.aws.amazon.com/ses/)
- [AWS SES 개발자 가이드](https://docs.aws.amazon.com/ses/latest/dg/)
- [Cloudflare DNS](https://dash.cloudflare.com/)