docs: 운영 배포 · 파일럿에서 겪은 것 — 인증서 이름 · mailcow 나가는 IP · 가비아 TXT 조각 #8

Merged
ttipo merged 1 commits from docs/pilot-deploy-notes into main 2026-10-31 11:07:46 +09:00
+21
View File
@@ -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://<MAIL_HOSTNAME>/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 → `<MX1_IP>` · PTR → `mx1.arasystem.it.kr`, setup 이 출력한 새 DKIM selector TXT 두 개(`v1-ed25519-…` · `v1-rsa-…`), SPF 에 `ip4:<MX1_IP>`.
**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}' <setup 출력 파일> | 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 으로 판단해 재시작만 한다 |