From 7856791eaed0e08bccd156a15fe1b601fdca471b Mon Sep 17 00:00:00 2001 From: kaffa Date: Thu, 9 Apr 2026 00:50:21 +0900 Subject: [PATCH] =?UTF-8?q?smtp-relay:=20K3s=20=EB=82=B4=EB=B6=80=20SMTP?= =?UTF-8?q?=20=EB=A6=B4=EB=A0=88=EC=9D=B4=20(postfix=20=E2=86=92=20Mailgun?= =?UTF-8?q?)=20=EC=8B=A0=EA=B7=9C=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 아키텍처, ConfigMap/Secret 구성, 사용법, 검증 절차 - 2026-04-08 초기 배포 검증 기록 (queue 6895722DB89, 250 Great success) --- infra/smtp-relay.md | 138 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 infra/smtp-relay.md diff --git a/infra/smtp-relay.md b/infra/smtp-relay.md new file mode 100644 index 0000000..f5e011c --- /dev/null +++ b/infra/smtp-relay.md @@ -0,0 +1,138 @@ +--- +title: smtp-relay (K3s 내부 SMTP 게이트웨이 via Mailgun) +updated: 2026-04-08 +tags: [k3s, mail, postfix, mailgun, smtp, relay] +--- + +> 코드: `gitea.inouter.com/kaffa/smtp-relay` (private) +> ArgoCD App: `argocd/smtp-relay` (auto-sync + self-heal) + +# smtp-relay + +K3s 클러스터 내부 애플리케이션이 표준 SMTP로 메일을 보내면 [[mailgun|Mailgun]]을 통해 실제 발송되는 릴레이. [[k3s-backup|K3s]] 안에서만 접근 가능. + +## 아키텍처 + +``` +K3s 앱 (pod, 임의 네임스페이스) + └─> smtp-relay.mail.svc.cluster.local:25 + └─> smtp.mailgun.org:587 (SASL auth + STARTTLS) +``` + +## 배치 + +| 항목 | 값 | +|------|-----| +| 클러스터 | incus-kr1/kr2/hp2 단일 K3s (v1.34.5) | +| Namespace | `mail` | +| Deployment | `smtp-relay` 1 replica, strategy=Recreate | +| 이미지 | `boky/postfix:v4.2.0-alpine` ([github](https://github.com/bokysan/docker-postfix)) | +| Service | ClusterIP, port 25 → 587, port 587 → 587 | +| Resources | req 20m CPU / 64Mi, limit 200m / 256Mi | +| Probe | TCP 587 readiness + liveness | + +## DNS / 접근 + +| 용도 | 주소 | +|------|------| +| 내부 앱 | `smtp-relay.mail.svc.cluster.local:25` | +| 같은 네임스페이스(`mail`) | `smtp-relay:25` | +| 외부 / incus 컨테이너 | **접근 불가** (ClusterIP only) | + +외부 접근이 필요해지면 NodePort 또는 Gateway TCP route 추가 고려. + +## Secret 처리 + +**Mailgun SMTP 인증 정보는 Gitea 저장소에 커밋하지 않는다.** Vault `secret/messaging/mailgun/smtp` 에서 읽어 클러스터에 out-of-band로 apply: + +```bash +USERNAME=$(vault kv get -field=username secret/messaging/mailgun/smtp) +PASSWORD=$(vault kv get -field=password secret/messaging/mailgun/smtp) + +kubectl -n mail create secret generic smtp-relay-mailgun \ + --from-literal=username="$USERNAME" \ + --from-literal=password="$PASSWORD" \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Secret 회전 시 동일 명령 재실행 후 `kubectl -n mail rollout restart deploy smtp-relay`. + +## ConfigMap (`smtp-relay-config`) + +postfix 동작을 결정하는 주요 환경변수: + +| 변수 | 값 | 의미 | +|------|-----|------| +| `RELAYHOST` | `[smtp.mailgun.org]:587` | 상위 릴레이 | +| `RELAYHOST_TLS_LEVEL` | `encrypt` | STARTTLS 필수 | +| `RELAYHOST_USERNAME`/`_PASSWORD` | Secret에서 주입 | Mailgun SASL | +| `ALLOWED_SENDER_DOMAINS` | `inouter.com` | 발신 도메인 화이트리스트 | +| `POSTFIX_myhostname` | `smtp-relay.mail.svc.cluster.local` | HELO 이름 | +| `POSTFIX_mynetworks` | `10/8 127/8 172.16/12 192.168/16` | 릴레이 허용 네트워크 | +| `POSTFIX_message_size_limit` | 25 MiB | Mailgun 메시지 한계 | +| `POSTFIX_smtpd_helo_required` | yes | HELO 강제 | +| `POSTFIX_maximal_queue_lifetime` / `bounce_queue_lifetime` | 1h | 큐 체류 상한 | + +## 사용법 (내부 앱) + +```python +# Python 예시 +import smtplib +from email.message import EmailMessage + +msg = EmailMessage() +msg["From"] = "noreply@inouter.com" +msg["To"] = "kaffa@inouter.com" +msg["Subject"] = "hello from k3s" +msg.set_content("본문") + +with smtplib.SMTP("smtp-relay.mail.svc.cluster.local", 25) as s: + s.send_message(msg) +``` + +인증 불필요. `From` 도메인은 반드시 `inouter.com` (아니면 postfix가 reject). + +## 검증 절차 + +```bash +# TCP 접속 +kubectl -n mail run probe --rm -it --restart=Never --image=busybox -- \ + sh -c 'echo -e "EHLO test\nQUIT" | nc smtp-relay 25' + +# 실제 발송 (swaks 사용) +kubectl -n mail run swaks-test --restart=Never --image=nicolaka/netshoot --command -- \ + swaks --server smtp-relay:25 \ + --from claude@inouter.com \ + --to you@example.com \ + --h-Subject '테스트' \ + --body '본문' +sleep 4 +kubectl -n mail logs swaks-test +kubectl -n mail delete pod swaks-test + +# postfix 큐에서 처리 내역 확인 +kubectl -n mail logs deploy/smtp-relay --tail=30 | grep -E 'status=|relay=' +``` + +성공 시 로그에 `status=sent (250 Great success)` 와 `relay=smtp.mailgun.org[...]:587` 가 나와야 한다. + +## 배포 검증 기록 (2026-04-08 초기) + +- Pod 1/1 Running, readiness/liveness OK +- swaks 테스트 성공: queue ID `6895722DB89` → `to=, relay=smtp.mailgun.org[34.160.157.95]:587, status=sent (250 Great success)` +- 지연 2.2초 (DNS lookup + STARTTLS 포함) + +## 운영 주의사항 + +- **DKIM 서명**: postfix 자체 DKIM 설정 안 함. Mailgun이 자체 DKIM으로 재서명하므로 수신 측 SPF/DKIM 검증은 통과. 자체 도메인 DKIM 원하면 `DKIM_AUTOGENERATE=yes` 설정 + DNS TXT 추가. +- **Rate limit**: postfix 자체 제한 없음. 폭주 시 Mailgun throttle 걸림 → 큐에 쌓이다 1h 후 bounce. 앱 레벨에서 throttle 권장. +- **sender spoofing 차단**: `ALLOWED_SENDER_DOMAINS=inouter.com` 으로 제한. 다른 도메인 발신이 필요해지면 ConfigMap에서 쉼표로 추가. +- **NetworkPolicy**: 현재 없음. 필요 시 특정 네임스페이스만 발송 가능하도록 제한 권장. +- **Monitoring / 알림**: 실패 메일 bounce 처리 알림 없음. Mailgun 대시보드로 확인하거나 Discord webhook 추가 필요. + +## 관련 리소스 + +- Vault: `secret/messaging/mailgun/smtp` (SMTP creds), `secret/messaging/mailgun/api-key` (API 키) +- Mailgun 도메인: `inouter.com` +- ArgoCD App 등록은 [[infra-hosts|클러스터 노드]]의 `argocd` 네임스페이스에 있음 +- 유사 GitOps 패턴 앱: `kaffa/bunnycdn-mcp`, `kaffa/cf-bouncer-manager`, `kaffa/namecheap-api`, `kaffa/vultr-api`