From a8ea204e4757997021d4e7f7f6ef30189b799274 Mon Sep 17 00:00:00 2001 From: ttipo Date: Sat, 31 Oct 2026 04:34:28 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=9A=B4=EC=98=81=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=20=C2=B7=20=ED=8C=8C=EC=9D=BC=EB=9F=BF=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B2=AA=EC=9D=80=20=EA=B2=83=20=E2=80=94=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B4=EB=A6=84=20=C2=B7=20mailcow=20=EB=82=98?= =?UTF-8?q?=EA=B0=80=EB=8A=94=20IP=20=C2=B7=20=EA=B0=80=EB=B9=84=EC=95=84?= =?UTF-8?q?=20TXT=20=EC=A1=B0=EA=B0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-10-31 운영 서버 첫 배포와 파일럿 공존을 켜며 실제로 막힌 것을 mail/README 에 적었다. - 도메인 자동 인증서의 Additional Hostnames 를 비우면 기본 이름 묶음을 한 주문으로 요청해, 옛 서버를 가리키는 autoconfig 등 때문에 mx1 까지 못 받고 자체 서명 인증서를 냈다 — mx1 만 넣고 AcmeRenewal 한 번. - mailcow 는 받는 IP 와 나가는 IP 가 달랐다 — Transport Map Test 가 550 5.7.1 이면 새 서버 로그의 remoteIp 로 찾아 목록에 더한다. - 가비아는 TXT 조각마다 250자 미만이라 RSA DKIM 을 다시 나눠 넣는다(명령 포함). - 첫 전달의 mailcow 451 지연, Gmail 의 ed25519 neutral, Spamhaus 시험 일시 SERVFAIL, 새 셸의 MAIL_ADMIN_URL 은 문제 해결 줄로. Co-Authored-By: Claude Opus 5.5 --- mail/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/mail/README.md b/mail/README.md index eca814d..8e30445 100644 --- a/mail/README.md +++ b/mail/README.md @@ -314,6 +314,13 @@ node --env-file=.env.production mail/scripts/setup.mjs **리졸버 전에 띄운 서버**(이 기능 전 배포)는 `sudo docker compose --env-file .env.production up -d`(mail 이 다시 만들어진다 — 몇 초 끊긴다) 뒤 5)와 같은 형태로 setup 을 한 번 돌린다. `up` 이 `optional dependency "mail-resolver" failed to start` 경고를 내면 mail 은 떴지만 리졸버가 unhealthy · 멈춤이다 — [리졸버 장애 때](#리졸버-장애-때). - 운영 setup 은 끝에 **권장 DNS 레코드**(DKIM·SPF·MX·DMARC·SRV·MTA-STS·autoconfig)를 출력한다. 그대로 붙이지 말고 아래 전환 절차 순서를 따른다. +- **TLS 인증서 이름을 `MAIL_HOSTNAME` 하나로 정한다** (2026-10-31 운영에서 겪음). bootstrap 이 켠 도메인의 자동 인증서(ACME)는 이름을 정하지 않으면 기본 묶음 — + `mta-sts` · `autoconfig` · `autodiscover` · `ua-auto-config` · MX 이름 · 호스트 이름 — 을 **한 주문**으로 요청하고, ACME 는 하나라도 실패하면 전체가 실패한다. + 전환 전에는 그 이름들이 옛 서버(mailcow)를 가리키거나 DNS 에 없으므로 mx1 까지 인증서를 받지 못하고, 엔진은 993 · 465 · 25(STARTTLS)에 자체 서명 인증서(`CN=rcgen self signed cert`)를 낸다. + 관리 화면 Domains › `arasystem.it.kr` › Certificate Management 의 **Additional Hostnames** 에 `mx1`(도메인은 엔진이 붙인다 — Enter 로 항목을 만든 뒤 저장)만 넣고, + Management › Tasks › Scheduled 에서 **AcmeRenewal** 을 **한 번** 건다(재시작만으로는 바로 요청하지 않는다. 걸 때마다 새로 발급하니 여러 번 걸지 않는다 — Let's Encrypt 같은 인증서 주에 5번). + 확인: `docker logs --since 5m "${PROJECT_NAME}-mail" 2>&1 | grep -i acme` 에 `hostname = ["mx1.arasystem.it.kr"]` · `acme.order-completed`, 바깥에서 `openssl s_client -connect mx1.arasystem.it.kr:993` 의 발급자가 Let's Encrypt. + **전면 전환** 때 `autoconfig` · `autodiscover` · `mta-sts` 의 DNS 를 새 서버로 옮긴 뒤 그 이름들을 이 목록에 더하고 AcmeRenewal 을 한 번 더 건다. - 공개 443 차단은 setup 이 컨테이너 안과 `MAIL_PUBLIC_URL`(호스트에서) 두 곳에서 확인한다. 호스트에서 자기 공인 주소로 되돌아오는 경로가 막힌 환경이면 경고만 남기므로, 그때는 바깥에서 확인한다: `curl -s -o /dev/null -w '%{http_code}\n' https:///jmap/session` → `403`. @@ -787,10 +794,17 @@ sudo docker exec "${PROJECT_NAME}-backend" node -e "const s=require('net').conne - **DNS 는 더하기만** (가비아) — [전환 절차](#arasystemitkr-전환-절차-mailcow--stalwart)의 단계 1 · 3 · 4: `mx1` A → `` · PTR → `mx1.arasystem.it.kr`, setup 이 출력한 새 DKIM selector TXT 두 개(`v1-ed25519-…` · `v1-rsa-…`), SPF 에 `ip4:`. **DKIM TXT 는 첫 시범 실행 전에 퍼져 있어야 한다** — 운영 setup 이 이 호스트의 리졸버로 읽어, 없거나 키가 다르면 켜지 않고 멈춘다. + setup 출력의 한 줄은 `<이름>.arasystem.it.kr. IN TXT "<값>"` 이다 — 가비아의 호스트 칸에는 끝의 `.arasystem.it.kr.` 를 뗀 이름, 값 칸에는 따옴표 안의 글만 넣는다. + **RSA 값(420자 안팎)은 setup 이 255자 + 나머지로 나눠 보이지만 가비아는 조각마다 250자 미만 · 모두 500자까지만 받는다** — 조각을 다시 나눠 `"…" "…"` 꼴로 넣는다(2026-10-31): + `awk '/v1-rsa-/{on=1} on{print} on && /\)/{on=0}' | grep -o '"[^"]*"' | tr -d '"\n' | fold -w 210 | sed 's/.*/"&"/' | paste -sd' '`. + 넣은 뒤 `Resolve-DnsName v1-rsa-….arasystem.it.kr -Type TXT -Server ns.gabia.co.kr` 로 조각이 이어져 키가 끝까지(`…IDAQAB`) 들어갔는지 본다. **MX · DMARC · autodiscover 는 바꾸지 않는다.** mx1 에 AAAA 는 두지 않는다(아래 "주의" 의 IPv6). - 새 서버의 계정은 **시범 사용자 하나만**. 다른 직원 · 별칭 · 공용 주소 · `dmarc@` · `postmaster@` 는 만들지 않는다(아래 "주의"). - 새 서버 → mailcow **25** 가 열려 있다(setup 이 확인한다 — 운영은 25 만 받는다. 587 · 465 는 인증이 있어야 중계되는데 setup 은 발송 경로에 인증을 넣지 않는다). mailcow 가 새 서버에 붙을 때의 IP 를 안다 — 보통 `mail.arasystem.it.kr` 의 A(`46.250.248.43`). + **받는 IP 와 나가는 IP 가 다를 수 있다** — 운영 mailcow 는 받기 `46.250.248.43` · 나가기 `125.143.81.248`(KT, PTR 없음)이었다(2026-10-31). + 빗나가면 Transport Map 의 Test 가 `550 5.7.1 Sender address not allowed` 다 — 새 서버 로그 `docker logs --since 10m "${PROJECT_NAME}-mail" 2>&1 | grep -o 'remoteIp = [0-9.]*' | sort | uniq -c` + 에서 Test 의 EHLO(`mail.arasystem.it.kr`)를 보낸 IP 를 찾아 `MAIL_PILOT_LEGACY_IPS` 에 콤마로 더하고 setup 을 다시 돌린다. 그 IP 가 고정인지 mailcow 쪽에 확인한다(바뀌면 동료 메일이 다시 550). - `MAIL_ADMIN_LOCAL_PART`(기본 `admin`) · `MAIL_DIRECTORY_LOCAL_PART`(기본 `mail-directory`) · 게이트웨이 `MAIL_GATEWAY_USER` 의 @ 앞(예 `mail-gateway`)이 mailcow 의 실제 주소와 겹치지 않는다 — 셋 다 새 서버의 로컬 주소(메일을 받지 않는 시스템 계정)라, 시범 사용자가 mailcow 의 같은 이름 주소로 보내면 새 서버가 `not-found` 로 거절한다. - 기존 서버 IP(`MAIL_PILOT_LEGACY_IPS`)에 **사람이 만든 자동 차단 예외(`x:AllowedIp`)가 없다** — setup 이 같은 주소로 시범 표시 항목을 만들어야 한다(같은 주소의 항목은 둘일 수 없다). 있으면 setup 이 멈춘다 — 지운 뒤 다시 실행한다. @@ -1007,6 +1021,10 @@ mailcow 를 거친 메일은 SPF · DMARC 결과가 없고(`Authentication-Resul 조사 때 p=reject 바깥 발신(서명 없음)을 넘기자 이 설정으로 3.5점(받은편지함), 검사 예외가 없을 때는 11.0점(스팸함)이었다. - mailcow 가 8~15점에 붙이는 `X-Spam-Flag: YES` 는 새 서버 규칙(`SPAM_FLAG` +5)이 받아 스팸함으로 간다. owner 의 mailcow Sieve · 부재중 · 전달 규칙은 더 돌지 않는다 — 새 서버에 다시 만든다. - 동료가 받은 owner 의 메일 — `X-Spamd-Result` 에 `SPOOFED_UNAUTH` · `R_DKIM_REJECT` 가 없어야 한다(위 4). +- 운영 결과(2026-10-31): 새 서버 → 동료는 spf · dkim(두 selector) · dmarc pass · mailcow 점수 −2.95, Gmail → mailcow → 새 서버는 0.80(ham), 새 서버 → Gmail 은 spf · dkim(RSA) · dmarc pass. + Gmail 의 `dkim=neutral (no key)`(ed25519 selector)는 정상이다 — Gmail 은 ed25519 를 검증하지 않고 RSA 서명이 대신한다. +- **새 서버 → mailcow 의 첫 전달은 `451 timeout processing message`(mailcow rspamd)로 두어 번 미뤄질 수 있다** — 처음 보는 IP · 도메인 · DKIM 키를 조회하느라 늦은 것으로 보이고, + 새 서버가 2분 · 5분 뒤 다시 보내 전달됐다(반송 없음 — 451 은 일시 오류). 계속되면 mailcow 의 Rspamd 로그를 본다. ### 되돌리기 (시범을 멈추고 mailcow 로) @@ -1437,6 +1455,9 @@ sudo docker logs --tail 3 "${PROJECT_NAME}-frontend" | --- | --- | | `셸 환경변수가 env 파일 값을 가리고 있습니다` | 표시된 변수를 지우고 다시 (`unset APP_ENV` · PowerShell `Remove-Item Env:APP_ENV`) | | `메일 엔진에 연결할 수 없습니다` | `COMPOSE_PROFILES=mail` 로 떠 있는지, `MAIL_ADMIN_PORT` 가 맞는지. 운영은 `MAIL_ADMIN_URL` 로 컨테이너 내부 주소를 준다 ("운영 배포" 3) | +| 운영에서 `메일 엔진이 응답하지 않습니다: http://mail:8080 (EAI_AGAIN)` | 새 셸이라 `MAIL_ADMIN_URL` 이 비었다 — 그러면 도커 안에서만 풀리는 `mail` 로 붙는다. `PROJECT_NAME` · `MAIL_ADMIN_URL` 을 다시 export ("운영 배포" 3). 아무것도 바뀌지 않았다 | +| `Spamhaus 키 … 시험 항목 … 을 받지 못해 그 항목을 꺼 둡니다 — SERVFAIL` (종료 1) | 한 번은 일시 오류였다 — setup 을 다시 돌리면 다시 켜진다(2026-10-31). 거듭되면 Spamhaus 계정에서 키 · 서비스(ZEN · DBL)가 켜져 있는지 본다. 그동안 메일은 오간다 | +| mx1 이 자체 서명 인증서(`CN=rcgen self signed cert`)를 낸다 · 로그에 autoconfig · autodiscover · mta-sts 의 `acme.auth-error` | 인증서 이름이 기본 묶음이다 — "운영 배포"의 TLS 인증서 줄(Additional Hostnames 에 `mx1` + AcmeRenewal 한 번) | | `운영에서는 docker 를 쓸 수 있어야 합니다` | docker 그룹 사용자로, 또는 `sudo -E env "PATH=$PATH" node …` 로 호스트에서 실행 ("운영 배포" 3). 아무것도 바뀌지 않았다 | | `임시 비밀번호 파일 자리(…/data/mail)에 쓸 수 없어 bootstrap 을 시작하지 않고 중단합니다` | 아무것도 바뀌지 않았다. `data/`·`data/mail/` 을 실행하는 사용자 소유로 (`sudo chown "$(id -u):$(id -g)" data && sudo chown -R "$(id -u):$(id -g)" data/mail`) 또는 늘 `sudo -E` 로 — 같은 명령 다시 | | bootstrap 직후 멈춤 (`bootstrap 은 끝났지만 임시 비밀번호를 보관하지 못했습니다` 등) | [bootstrap 직후에 멈췄다면](#bootstrap-직후에-멈췄다면-다시-실행하기-전에) — 볼륨을 지우지 말고 원인을 고친 뒤 같은 명령(`--recovery`) 다시. config.json 으로 판단해 재시작만 한다 | -- 2.52.0