diff --git a/mail/README.md b/mail/README.md index a5998c8..d9bd14c 100644 --- a/mail/README.md +++ b/mail/README.md @@ -1318,6 +1318,7 @@ alias dcx='docker compose exec postfix-mailcow' 1. **postscreen 예외** — `data/conf/postfix/custom_postscreen_whitelist.cidr` 에 `/32 permit` (재시작 없이 다시 읽는다). 새 IP 가 차단 목록 하나에만 있어도 postscreen 에서 끊길 수 있다. Forwarding Hosts 에는 넣지 않는다 — 넣으면 그 IP 의 메일은 스팸 검사 · 위조 판정 없이 들어온다. 2. **Fail2ban 예외** — System → Configuration → Options → Fail2ban parameters → "Allowlisted networks/hosts" 에 `` (imapsync 로그인 실패가 쌓여 mx1 IP 가 막히면 SMTP 도 끊긴다). + mailcow 가 mx1 을 사설 주소로 보면(앞단 · WSL 을 지나며 바뀌면) 막히지 않아 필요 없다 — 넣어도 해는 없다([imapsync › 원본](#원본--mailcow-마스터-계정)의 접속 주소). 3. **새 서버 setup** — 위 "env 와 실행". 이때부터 새 서버의 owner@ 가 동료에게 보낼 수 있다(mailcow 는 아직 owner 앞 메일을 자기 사서함에 넣는다). 4. **반대 방향부터 시험** — 새 서버의 owner@(웹메일)로 동료에게 한 통. 실사용자에게 영향이 없는 방향이라 먼저 한다: ```bash @@ -1455,7 +1456,7 @@ mailcow 를 거친 메일은 SPF · DMARC 결과가 없고(`Authentication-Resul | 5. 메일 이전 1차 | [imapsync](#imapsync) — 한 사람으로 먼저(`--justlogin` · 폴더 맞춤), 그다음 주소 목록 전부 | 사람마다 `Detected 0 errors` · 폴더별 수 · 「이전 현황」의 메일함 사용량 | | 6. MX 전환 | `arasystem.it.kr MX 10 mx1.arasystem.it.kr` 로 교체. mailcow 는 **계속 켜 둔다** (전파 중에는 mailcow 로도 들어온다). **이때부터 9 까지 직원은 새 서버에 들어오지 못한다** — 짧게(예: 금요일 저녁 ~ 월요일 아침) | 외부에서 보낸 메일이 새 엔진 로그에 수신 | | 7. 마지막 imapsync | TTL 이 지나 mailcow 에 새 수신이 없어진 뒤 같은 명령을 한 번 더(차이분 — 이미 옮긴 메일은 건너뛴다). 그 전에 직원에게 mailcow 를 그만 쓰게 알린다 | mailcow 로그에 새 수신 없음 · 사람마다 `Detected 0 errors` | -| 8. 이전 마치기 | 마지막 imapsync 뒤 5분쯤 지나 운영 도구 「메일 이전 › 이전 마치기」(주소 빈 값 — 이전 중인 계정 모두) → 주소 · 임시 비밀번호 표가 **한 번** 보인다. mailcow 메일함은 `Disallow login`, 마스터 계정 두 줄 · 서버 B 의 비밀번호 파일 지우기([imapsync](#imapsync) 단계 8) | 「이전 현황」이 비었다 | +| 8. 이전 마치기 | 마지막 imapsync 뒤 5분쯤 지나 운영 도구 「메일 이전 › 이전 마치기」(주소 빈 값 — 이전 중인 계정 모두) → 주소 · 임시 비밀번호 표가 **한 번** 보인다. mailcow 메일함은 `Disallow login`, 마스터 계정 두 값 비우기 · 서버 B 의 비밀번호 파일 지우기([imapsync](#imapsync) 단계 8) | 「이전 현황」이 비었다 | | 9. 임시 비밀번호 나눠 주기 | 직원에게 직접(말 · 쪽지). 직원은 첫 로그인 때 새 비밀번호를 정하고, 회사 정책이면 2단계 인증을 켠다 | 직원이 웹메일에 들어온다 | | 10. 병행 1~2주 후 정리 | SPF 에서 `ip4:46.250.248.43` 제거, `dkim._domainkey` 삭제, mailcow 중지. 필요하면 `mail.arasystem.it.kr` 을 새 호스트로 옮기고(인증서 포함) `MAIL_PUBLIC_URL` 변경 | — | @@ -1493,18 +1494,18 @@ mailcow 를 거친 메일은 SPF · DMARC 결과가 없고(`Authentication-Resul 이전 중인 계정은 운영 도구 메뉴 `11. 메일 이전` 이 다룬다 — `1. 이전 현황` · `2. 이전용 비밀번호 다시 만들기` · `3. 이전 마치기`(2 · 3 은 `주소 (빈 값이면 이전 중인 계정 모두): `). "이전 중"은 엔진 비밀번호 만료가 미래(30일 안)이면서 운영 도구의 실행 기록에서 마지막으로 건 비밀번호가 이전용인 계정이다(계약 6절 "메일 이전"). 엔진의 **비밀번호 기본 만료**(`x:Authentication` 의 `passwordDefaultExpiry` — 관리 화면의 인증 설정)는 켜지 않는다 — 켜면 직원 비밀번호가 주기적으로 만료되고, 끈 뒤에도 그동안 바꾼 비밀번호의 만료가 남는다. 운영 도구는 켜져 있으면 첫 화면에 알린다. -- 근거는 아래 [개발에서 확인한 것](#개발에서-확인한-것-imapsync--2026-11-02) — mailcow 를 흉내 낸 Dovecot → 개발 엔진. **운영 mailcow · 서버 B 에서는 아직 돌려 보지 않았다** — 단계 5 의 첫 사람으로 확인한다. +- 근거는 아래 [개발에서 확인한 것](#개발에서-확인한-것-imapsync--2026-11-02) — mailcow 를 흉내 낸 Dovecot → 개발 엔진. **운영 mailcow · 서버 B 에서는 아직 돌려 보지 않았다** — 준비의 "원본 로그인만"과 단계 5 의 첫 사람으로 확인한다. #### 차례 (단계 번호는 [전환 절차](#arasystemitkr-전환-절차-mailcow--stalwart)의 것) | 단계 | 할 일 | 비고 | | --- | --- | --- | -| 준비 | mailcow 마스터 계정 고정(아래 "원본") · mailcow Fail2ban 예외에 새 서버 IP · 서버 B 에 마스터 비밀번호 파일 · 주소 목록(아래 "돌리는 곳") | Fail2ban 예외는 [파일럿 공존](#파일럿-공존-mailcow-와-함께--한-사람-먼저)의 mailcow 절차 2 와 같다 — imapsync 로그인 실패가 쌓여 그 IP 가 막히면 새 서버 → mailcow 의 SMTP 도 끊긴다 | +| 준비 | 단계 2 며칠 전 — mailcow 마스터 계정 고정(아래 "원본") · 서버 B 에 마스터 비밀번호 파일 · **원본 로그인만 미리**(아래 "돌리는 곳"의 준비 줄 — 대상 계정 없이) · mailcow 가 서버 B 를 본 주소가 공인이면 Fail2ban 예외에 · 주소 목록 | 원본 쪽 문제(마스터 · 인증서 · 앞단 길)를 계정을 만들기 전에 고친다 — 단계 2 부터는 짧게 이어야 한다(아래). Fail2ban 예외는 [파일럿 공존](#파일럿-공존-mailcow-와-함께--한-사람-먼저)의 mailcow 절차 2 와 같다 — imapsync 로그인 실패가 쌓여 그 IP 가 막히면 새 서버 → mailcow 의 SMTP 도 끊긴다 | | 2 | 계정 만들기 — 운영 도구 「입사」를 한 사람씩(모두를 한 실행에서), 옮겨 올 메일 y. 처음 보이는 이전용 비밀번호를 **그 자리에서** 서버 B 의 비밀번호 파일로(아래 — 다시 볼 수 없다) | 「메일 이전 › 이전 현황」에 이전 중인 계정 · 끝나는 때 · 메일함 사용량이 보인다 | | 5 | imapsync 1차 — **한 사람으로 먼저**(`--justlogin` · 엔진이 본 접속 주소 · 1차 · 아래 "사람마다 확인할 것"), 그다음 주소 목록 전부 | 메일이 많으면 MX 전날에 — 단계 7 의 차이분이 짧아진다 | | 6 | MX 바꾸기 | 이때부터 단계 9 까지 직원은 새 서버에 들어오지 못한다(비밀번호를 아직 받지 않았다) | | 7 | **마지막 imapsync** — TTL 이 지나 mailcow 에 새 수신이 없어진 뒤 같은 명령 | 그사이 mailcow 에 온 메일 · 바뀐 읽음 표시만 옮긴다. 직원에게는 그 전에 mailcow 를 그만 쓰게 알린다 — 이 뒤에 mailcow 에서 한 일은 옮겨지지 않는다 | -| 8 | **마지막 imapsync 가 끝나고 5분쯤 지난 뒤** 운영 도구 「메일 이전 › 이전 마치기」 — 주소 빈 값(이전 중인 계정 모두). 그다음 mailcow 정리 — 메일함을 `Disallow login (incoming mail is still accepted)` 로, 마스터 계정 두 줄 지우기, 서버 B 의 비밀번호 파일 지우기 | 사람마다 직원용 임시 비밀번호(만료 — 첫 로그인 때 바꾸기), imapsync 의 로그인 기록 지우기. 끝에 주소 · 임시 비밀번호 표가 **한 번** 보인다. 5분을 기다리는 까닭 — 엔진 웹훅은 1초씩 묶어 보내고 실패하면 5분까지 다시 보낸다, 그 전에 하면 늦게 온 IMAP 로그인 몇 줄이 직원의 로그인 기록에 남는다. mailcow 메일함은 지우거나 Inactive 로 두지 않는다(파일럿 mailcow 절차 9 와 같다) | +| 8 | **마지막 imapsync 가 끝나고 5분쯤 지난 뒤** 운영 도구 「메일 이전 › 이전 마치기」 — 주소 빈 값(이전 중인 계정 모두). 그다음 mailcow 정리 — 메일함을 `Disallow login (incoming mail is still accepted)` 로, 마스터 계정 두 값 비우기(아래 "원본"), 서버 B 의 비밀번호 파일 지우기 | 사람마다 직원용 임시 비밀번호(만료 — 첫 로그인 때 바꾸기), imapsync 의 로그인 기록 지우기. 끝에 주소 · 임시 비밀번호 표가 **한 번** 보인다. 5분을 기다리는 까닭 — 엔진 웹훅은 1초씩 묶어 보내고 실패하면 5분까지 다시 보낸다, 그 전에 하면 늦게 온 IMAP 로그인 몇 줄이 직원의 로그인 기록에 남는다. mailcow 메일함은 지우거나 Inactive 로 두지 않는다(파일럿 mailcow 절차 9 와 같다) | | 9 | 임시 비밀번호 나눠 주기 — 직원에게 직접(말 · 쪽지) | 직원은 첫 로그인 때 새 비밀번호를 정하고, 회사 정책이면 2단계 인증을 켠다(계약 6절 "제한된 세션") | - **단계 6 ~ 9 는 짧게 잡는다** — 예: 금요일 저녁 MX → 토요일 마지막 imapsync · 이전 마치기 → 월요일 아침 나눠 주기. 그동안 바깥 메일은 새 서버에 쌓이고, 직원은 단계 7 전까지 mailcow 를 그대로 쓸 수 있다. @@ -1521,29 +1522,45 @@ mailcow 문서 "Static master user"( # 로그인 이름에는 @mailcow.local 이 저절로 붙는다 -DOVECOT_MASTER_PASS=<비밀번호> # 영문 · 숫자로 길게 -# 그리고 설치 폴더에서 다시 띄운다 — dovecot-mailcow 가 다시 만들어진다 +DOVECOT_MASTER_PASS=<비밀번호> # 영문 · 숫자만(특수 문자 금지 — mailcow 설정의 주석) · 길게 — 그 PC 에서 openssl rand -hex 24 로 만들어 비밀번호 관리자에 둔다 +# 그리고 설치 폴더에서 다시 띄운다 — 두 값을 받는 dovecot-mailcow 만 다시 만들어진다 cd /opt/mailcow-dockerized && sudo docker compose up -d ``` - IMAP 로그인 이름은 `<직원 주소>*<이름>@mailcow.local`(예 `hong@arasystem.it.kr*migrator@mailcow.local` — `@` 가 둘), 비밀번호는 `DOVECOT_MASTER_PASS` 다. **IMAP 전용** — SOGo(mailcow 웹메일) 로그인에는 쓰지 못한다. -- 끝나면(단계 8) 두 줄을 지우고 `docker compose up -d` — 무작위로 돌아간다. 마스터 비밀번호는 모든 메일함을 연다. +- 다시 띄우는 동안 직원의 IMAP · 웹메일(SOGo)이 잠깐 끊긴다 — 메일 프로그램은 다시 잇고, 들어오는 메일은 postfix 가 붙잡았다가 넘긴다. 한가한 때에 한다. +- 마스터 비밀번호는 모든 메일함을 연다 — 고정은 단계 2 며칠 전(원본 로그인을 미리 볼 수 있게)부터 단계 8 까지만 둔다. + 끝나면(단계 8) 두 값을 비우고(줄은 둔다) 같은 명령으로 다시 띄운다 — 무작위로 돌아간다. - `mailcow.conf` 는 **mailcow 호스트** — mailcow 컨테이너가 도는 사무실 PC(Windows 의 WSL — 설치 폴더 `/opt/mailcow-dockerized`, 바깥 `mail.arasystem.it.kr` 은 Contabo 앞단을 거쳐 이 PC 로 온다)에 있다. 운영자가 그 PC 의 WSL 셸에서 고친다(2026-11-02 사용자 확인 — 파일럿 때 적은 "관리 화면만"([파일럿 공존](#파일럿-공존-mailcow-와-함께--한-사람-먼저)의 "SSH 가 없을 때")은 원격으로 들어갈 길 이야기다). 이전하는 동안 그 PC 가 절전 · 재부팅되면 mailcow 가 멈춰 imapsync 가 끊긴다 — 전원 연결 시 절전 안 함으로 둔다. - `mailcow.conf` 는 root 만 읽고 저장소도 root 소유라 확인 명령은 sudo 로 한다 — 값(비밀번호)은 출력하지 않고 이름만 본다(`git config safe.directory` 는 건드리지 않는다): + `mailcow.conf` 는 root 만 읽고 저장소도 root 소유라 확인 명령은 sudo 로 한다 — 값(비밀번호)은 출력하지 않는다(`git config safe.directory` 는 건드리지 않는다): ```bash # mailcow PC 의 WSL 셸에서 (sudo 를 쓸 수 있는 사용자) sudo docker inspect "$(sudo docker ps -qf name=dovecot-mailcow)" --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' # → /opt/mailcow-dockerized - sudo grep -o '^#\?DOVECOT_MASTER_[A-Z]*' /opt/mailcow-dockerized/mailcow.conf # 줄이 있는지 · 주석인지만 + sudo awk -F= '/^DOVECOT_MASTER_(USER|PASS)=/ { print $1, ($2 == "" ? "비어 있음" : "값 있음") }' /opt/mailcow-dockerized/mailcow.conf # 값 대신 비었는지만. 아무것도 안 나오면 줄이 없거나 주석 sudo git -C /opt/mailcow-dockerized log -1 --format='%cd %h' --date=short # mailcow 판(커밋 날짜) ``` -- **확인하지 않은 것** — imapsync 가 붙는 `mail.arasystem.it.kr:993` 이 Contabo 앞단을 어떻게 지나 사무실 PC 의 mailcow 에 닿는지 — - mailcow 가 보는 접속 주소가 서버 B 의 IP 가 아니라 앞단의 주소라면 Fail2ban 예외도 그 주소여야 하고, 그 주소가 막히면 모든 직원의 메일 프로그램이 함께 막힌다(첫 사람의 `--justlogin` 뒤 mailcow 관리 화면의 로그인 기록에서 그 주소를 본다). - 운영 mailcow 판에서 이 설정이 문서대로인지(같은 꼴로 인증에 실패했다는 커뮤니티 글이 있다 — 해결 기록 없음. 그래서 한 사람의 `--justlogin` 으로 먼저 본다), + 운영(2026-11-03 확인): 설치 폴더 `/opt/mailcow-dockerized`, 판 2026-05-12(`32156a33`), 두 줄은 주석 없이 있다. + "값 있음"이면 누군가 이미 고정해 둔 것이다 — 바꾸기 전에 그 값을 쓰는 곳(백업 스크립트 등)이 있는지 알아본다. +- **mailcow 가 보는 접속 주소** — `mail.arasystem.it.kr:993` 은 Contabo 앞단을 지나 이 PC(WSL)의 mailcow 에 닿는다 — 가는 길에 접속 주소가 바뀔 수 있다. + mailcow 의 Fail2ban(netfilter-mailcow — 기본 10분에 실패 10번이면 30분)은 **사설 · 루프백 주소는 막지 않는다**(Python `ipaddress` 의 `is_private` · `is_loopback` — mailcow 원본 `data/Dockerfiles/netfilter/main.py`, 2026-11-03 확인). + 그래서 mailcow 가 서버 B 를 사설 주소(10 · 172.16–31 · 192.168 · 127)로 보면 예외가 필요 없고, 공인 주소로 보면 그 주소를 예외에 넣는다. + 100.64–100.127.x(Tailscale 같은 공유 주소)는 사설로 치지 않아 막힐 수 있다 — 바깥 접속이 모두 그 주소 하나(또는 앞단 `46.250.248.43` 하나)로 보이면 그 주소가 예외에 있는지 본다. 막히면 모든 직원의 메일 프로그램이 함께 막힌다. + ```bash + # 아무 때나 — 지난 사흘의 IMAP · POP3 접속이 어떤 주소로 보였는지(공인 주소는 세기만 하고 보이지 않는다) + sudo docker logs --since 72h "$(sudo docker ps -qf name=dovecot-mailcow)" 2>&1 | grep -E '(imap|pop3)-login:' | grep -oE 'rip=[0-9A-Fa-f:.]+' | cut -d= -f2 \ + | awk -v front=46.250.248.43 '$0 == front { f++; next } /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|::1$|[Ff][CcDd]|[Ff][Ee]80:)/ { p[$0]++; next } { if (!($0 in g)) d++; if (++g[$0] > m) m = g[$0]; n++ } END { for (ip in p) print "내부", ip, p[ip] "줄"; print "앞단", front, f + 0, "줄"; print "그 밖의 공인", d + 0, "곳", n + 0, "줄 · 가장 많은 한 곳", m + 0, "줄" }' + # 준비의 "원본 로그인만"(서버 B — 아래 "돌리는 곳") 뒤 15분 안에 — mailcow 가 서버 B 를 본 주소. 마스터 로그인만 고른다(직원의 평소 로그인은 나오지 않는다) + sudo docker logs --since 15m "$(sudo docker ps -qf name=dovecot-mailcow)" 2>&1 \ + | awk '/Master user logging in as / { m = $NF; next } m != "" && index($0, "Login: user=<" m ">") { if (match($0, /rip=[0-9A-Fa-f:.]+/)) print m, substr($0, RSTART + 4, RLENGTH - 4); m = "" }' + ``` + `내부 172.22.1.248` 은 mailcow 안의 SOGo(웹메일)다. 두 명령은 Dovecot 2.3 의 로그 꼴(`imap-login: Login: user=<…>, …, rip=…` · `Master user logging in as …`)로 맞췄다 — + 개발 흉내(Dovecot 2.3.21.1)의 실제 로그와 mailcow 꼴 표본으로, 우분투 · 데비안의 기본 awk 인 mawk 1.3.4 에서 시험했다(2026-11-03). mailcow 는 지금 원본 compose 에서도 `dovecot:2.3.21.1-2` 다. +- **확인하지 않은 것** — 운영 mailcow 판(2026-05-12)에서 이 설정이 문서대로인지(같은 꼴로 인증에 실패했다는 커뮤니티 글이 있다 — 해결 기록 없음. 그래서 준비의 "원본 로그인만"으로 먼저 본다), 메일함 편집의 **Allowed protocols** 에서 IMAP 이 꺼진 메일함이 마스터로도 막히는지(그래서 mailcow 접근 줄이기는 마지막 imapsync 뒤 — 단계 8). - 개발 흉내(Dovecot 2.3 에 같은 꼴 — 구분자 `*` · 마스터 이름 `migrator@mailcow.local`)에서는 imapsync 가 그 이름을 그대로 넘겨 Dovecot 이 `Master user logging in as kim@legacy.test` 로 받았다. mailcow 에 없는 메일함은 마스터로도 `NO [AUTHORIZATIONFAILED] Authorization failed` 다. @@ -1578,6 +1595,17 @@ imapsync_one() { # $1 = 직원 주소, 그 뒤는 덧붙일 옵션 --automap --syncinternaldates --addheader --nolog --noreleasecheck "${@:2}" &1 | grep auth.success | grep imaps | tail -1 @@ -1637,6 +1665,7 @@ mailcow 를 흉내 낸 Dovecot(`dovecot/dovecot:2.3.21.1@sha256:547d28f9a893c93a | 무엇 | 결과 | | --- | --- | | 원본 로그인 | `<주소>*migrator@mailcow.local` 을 imapsync 가 그대로 넘겼다. 원본에 없는 메일함은 마스터로도 `NO [AUTHORIZATIONFAILED]` | +| 원본 로그인만(준비) | 두 쪽을 같은 원본 · 같은 마스터 로그인으로 준 `--justlogin` 을 imapsync 가 받는다 — `Host1: success login` · `Host2: success login` · `Exiting with return value 0`(2026-11-03). Dovecot 로그는 로그인마다 `Master user logging in as <주소>` 다음 `Login: user=<주소>, …, rip=` | | 폴더 맞춤 | INBOX → Inbox(`inbox`) · Sent → Sent Items(`sent`) · Drafts → Drafts(`drafts`) · Trash → Deleted Items(`trash`) · Junk → Junk Mail(`junk`) — **따로 생긴 폴더 없음, `--f1f2` 가 필요 없었다**. Archive 는 보통 폴더 "Archive"(role 없음) | | 폴더마다 메일 수 | 원본과 같다 (메일 14 · 5통, 폴더 8 · 7개) | | 받은 때 | 모두 같다 — 2025년 내부 날짜가 초까지 그대로(JMAP `receivedAt`) | @@ -1654,7 +1683,7 @@ mailcow 를 흉내 낸 Dovecot(`dovecot/dovecot:2.3.21.1@sha256:547d28f9a893c93a - **개발과 운영이 다른 점** — 대상 인증서가 자체 서명이라 `--sslargs2 SSL_verify_mode=0` 으로 돌렸고, imapsync 를 잠시 앱 네트워크에 붙여 `mail:993` 으로 갔다(엔진은 신뢰 대역 주소로 본다 — 운영에서는 하지 않는다). 원본은 시험 CA 로 `SSL_verify_mode=1` 이었다. -- **확인하지 않은 것** — 운영 mailcow 의 마스터 계정 · Allowed protocols, 서버 B 의 길(`--network host` + `--add-host` 와 엔진이 보는 주소), 운영의 양 · 시간, 직원이 만든 실제 폴더 이름. +- **확인하지 않은 것** — 운영 mailcow(2026-05-12 판)의 마스터 계정과 mailcow 가 서버 B 를 보는 주소(준비의 "원본 로그인만"으로 본다) · Allowed protocols, 서버 B 의 길(`--network host` + `--add-host` 와 엔진이 보는 주소), 운영의 양 · 시간, 직원이 만든 실제 폴더 이름. ---