From b43b7471071bb6870682bdd2b500add017ee9515 Mon Sep 17 00:00:00 2001 From: ttipo Date: Tue, 3 Nov 2026 01:20:20 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=9A=B4=EC=98=81=20mailcow=20?= =?UTF-8?q?=EB=8A=94=20=EB=B0=94=EA=B9=A5=20=EC=A0=91=EC=86=8D=EC=9D=84=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=2010.9.0.1=20=EB=A1=9C=20=EB=B3=B8=EB=8B=A4?= =?UTF-8?q?=20=E2=80=94=20Fail2ban=20=EC=98=88=EC=99=B8=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=97=86=EC=9D=8C=20=C2=B7=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=8A=94=20--tail=20=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 운영 mailcow 에서 확인(2026-11-03): 마스터 두 값은 빈 값(아직 아무도 고정하지 않음). 최근 7일 Dovecot 로그인 줄과 지금 연결이 모두 앞단 터널 주소 10.9.0.1(사설) — 공인 주소 0. mailcow 의 Fail2ban 은 사설 주소를 막지 않으므로 서버 B 를 예외에 넣지 않는다. 남는 것은 Dovecot 의 늦추기(같은 주소의 바깥 직원 모두)라 실패를 되풀이하지 않는다. 이 mailcow 의 docker 로그(json-file · 회전 없음 · 428MB)는 --since 만 주면 0줄이고 --tail 은 된다 — 처음부터 읽다 중간의 읽지 못하는 줄에서 말없이 멈추는 것으로 보인다. 로그 명령을 --tail 로 바꾸고, 로그 없이 지금 연결을 보는 doveadm who 를 더했다. Co-Authored-By: Claude Opus 5.5 --- mail/README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/mail/README.md b/mail/README.md index d9bd14c..d8678fb 100644 --- a/mail/README.md +++ b/mail/README.md @@ -1318,7 +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-마스터-계정)의 접속 주소). + mailcow 가 mx1 을 사설 주소로 보면 막히지 않아 필요 없다 — IMAP(993)은 앞단 터널 주소 `10.9.0.1` 로 보였고(2026-11-03), 25 는 보지 않았다. 넣어도 해는 없다([imapsync › 원본](#원본--mailcow-마스터-계정)의 접속 주소). 3. **새 서버 setup** — 위 "env 와 실행". 이때부터 새 서버의 owner@ 가 동료에게 보낼 수 있다(mailcow 는 아직 owner 앞 메일을 자기 사서함에 넣는다). 4. **반대 방향부터 시험** — 새 서버의 owner@(웹메일)로 동료에게 한 통. 실사용자에게 영향이 없는 방향이라 먼저 한다: ```bash @@ -1500,7 +1500,7 @@ mailcow 를 거친 메일은 SPF · DMARC 결과가 없고(`Authentication-Resul | 단계 | 할 일 | 비고 | | --- | --- | --- | -| 준비 | 단계 2 며칠 전 — mailcow 마스터 계정 고정(아래 "원본") · 서버 B 에 마스터 비밀번호 파일 · **원본 로그인만 미리**(아래 "돌리는 곳"의 준비 줄 — 대상 계정 없이) · mailcow 가 서버 B 를 본 주소가 공인이면 Fail2ban 예외에 · 주소 목록 | 원본 쪽 문제(마스터 · 인증서 · 앞단 길)를 계정을 만들기 전에 고친다 — 단계 2 부터는 짧게 이어야 한다(아래). Fail2ban 예외는 [파일럿 공존](#파일럿-공존-mailcow-와-함께--한-사람-먼저)의 mailcow 절차 2 와 같다 — imapsync 로그인 실패가 쌓여 그 IP 가 막히면 새 서버 → mailcow 의 SMTP 도 끊긴다 | +| 준비 | 단계 2 며칠 전 — mailcow 마스터 계정 고정(아래 "원본") · 서버 B 에 마스터 비밀번호 파일 · **원본 로그인만 미리**(아래 "돌리는 곳"의 준비 줄 — 대상 계정 없이) · 주소 목록 | 원본 쪽 문제(마스터 · 인증서 · 앞단 길)를 계정을 만들기 전에 고친다 — 단계 2 부터는 짧게 이어야 한다(아래). mailcow 의 Fail2ban 예외는 필요 없다 — mailcow 는 바깥 접속을 모두 앞단 터널 주소(사설 `10.9.0.1`)로 보고, 사설 주소는 막지 않는다(아래 "원본"의 접속 주소) | | 2 | 계정 만들기 — 운영 도구 「입사」를 한 사람씩(모두를 한 실행에서), 옮겨 올 메일 y. 처음 보이는 이전용 비밀번호를 **그 자리에서** 서버 B 의 비밀번호 파일로(아래 — 다시 볼 수 없다) | 「메일 이전 › 이전 현황」에 이전 중인 계정 · 끝나는 때 · 메일함 사용량이 보인다 | | 5 | imapsync 1차 — **한 사람으로 먼저**(`--justlogin` · 엔진이 본 접속 주소 · 1차 · 아래 "사람마다 확인할 것"), 그다음 주소 목록 전부 | 메일이 많으면 MX 전날에 — 단계 7 의 차이분이 짧아진다 | | 6 | MX 바꾸기 | 이때부터 단계 9 까지 직원은 새 서버에 들어오지 못한다(비밀번호를 아직 받지 않았다) | @@ -1544,22 +1544,26 @@ cd /opt/mailcow-dockerized && sudo docker compose up -d 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 판(커밋 날짜) ``` - 운영(2026-11-03 확인): 설치 폴더 `/opt/mailcow-dockerized`, 판 2026-05-12(`32156a33`), 두 줄은 주석 없이 있다. + 운영(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` 하나)로 보이면 그 주소가 예외에 있는지 본다. 막히면 모든 직원의 메일 프로그램이 함께 막힌다. +- **mailcow 가 보는 접속 주소 — 바깥 접속은 모두 `10.9.0.1`**(2026-11-03 운영 — 최근 7일 로그인 줄 · 지금 연결 모두): `mail.arasystem.it.kr:993` 은 Contabo 앞단에서 터널을 지나 + 이 PC 의 mailcow 에 닿고, mailcow 는 그 터널 주소 하나로 본다(공인 주소는 0 — 서버 B 도 같은 길이다). 안쪽 `172.22.1.248` 은 SOGo(웹메일), `172.22.1.8` 은 mailcow 안의 다른 컨테이너(7일에 3만 8천 줄 — 상태 점검으로 보인다)다. + - mailcow 의 Fail2ban(netfilter-mailcow — 기본 10분에 실패 10번이면 30분)은 **사설 · 루프백 주소는 막지 않는다**(Python `ipaddress` 의 `is_private` · `is_loopback` — mailcow 원본 `data/Dockerfiles/netfilter/main.py`). + 그래서 **서버 B 를 Fail2ban 예외에 넣을 필요가 없다**. 거꾸로 지금 mailcow 는 바깥에서 오는 IMAP 무차별 대입을 IP 로 막지 못한다(전환 뒤 새 엔진은 실제 주소로 막는다). + - 남는 것은 Dovecot 의 늦추기다 — 실패가 있었던 주소의 다음 로그인을 늦춘다(최대 15초). 바깥 접속이 모두 한 주소라 imapsync 의 실패가 쌓이면 바깥 직원의 로그인도 함께 늦어진다 — 실패를 되풀이하지 않는다. + - 이 mailcow 의 docker 로그(json-file · 회전 없음 · Dovecot 만 428MB)는 `--since` 만 주면 **0줄**이다 — 처음부터 읽다가 중간의 읽지 못하는 줄(10-24 재시작 무렵으로 보인다)에서 docker 가 말없이 멈춘다. + `--tail` 은 끝에서부터 읽어 된다 — 아래처럼 늘 `--tail` 을 준다. 앞단을 바꾸는 등 다시 볼 때: ```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 = "" }' + # 최근 10만 줄(끝에서부터 — 본 범위의 처음 · 끝 시각도 나온다)의 로그인 줄이 어떤 주소였는지. 공인 주소는 세기만 하고 보이지 않는다 + sudo docker logs -t --tail 100000 "$(sudo docker ps -qf name=dovecot-mailcow)" 2>&1 | awk -v front=46.250.248.43 'NR == 1 { a = substr($1, 1, 19) } { z = substr($1, 1, 19) } /-login: / && match($0, /rip=[0-9A-Fa-f:.]+/) { ip = substr($0, RSTART + 4, RLENGTH - 4); if (ip == front) f++; else if (ip ~ /^(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[ip]++; else { if (!(ip in g)) d++; if (++g[ip] > m) m = g[ip]; n++ } } END { print "본 줄", NR, "· 처음", a, "· 끝", z; for (ip in p) print "내부", ip, p[ip] "줄"; print "앞단", front, f + 0, "줄"; print "그 밖의 공인", d + 0, "곳", n + 0, "줄 · 가장 많은 한 곳", m + 0, "줄" }' + # 지금 붙어 있는 IMAP · POP3 연결의 주소(로그 없이 — Dovecot 이 아는 연결) + sudo docker exec "$(sudo docker ps -qf name=dovecot-mailcow)" doveadm -f flow who -1 2>&1 | grep -oE 'ip=[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분 안에 — 마스터 로그인만 골라 그 주소(10.9.0.1 이면 예상대로). 직원의 평소 로그인은 나오지 않는다 + sudo docker logs --since 15m --tail 20000 "$(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` 다. + 100.64–100.127.x(Tailscale 같은 공유 주소)는 사설로 치지 않아 Fail2ban 이 막을 수 있다 — 앞단을 바꿔 바깥 접속이 그런 주소나 공인 주소 하나로 보이게 되면 그 주소가 예외에 있는지 본다(막히면 바깥 직원 모두가 함께 막힌다). + 세 명령은 Dovecot 2.3 의 로그 · doveadm 꼴(`imap-login: Login: user=<…>, …, rip=…` · `Master user logging in as …` · `username=… proto=… pid=… ip=…`)로 맞췄다 — 개발 흉내(Dovecot 2.3.21.1)의 실제 출력과 mailcow 꼴 표본으로, + 우분투 · 데비안의 기본 awk 인 mawk 1.3.4 에서 시험했다(2026-11-03). 앞의 둘은 운영 mailcow 에서도 돌렸다. 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` 로 받았다. @@ -1596,8 +1600,8 @@ imapsync_one() { # $1 = 직원 주소, 그 뒤는 덧붙일 옵션 } # 준비 — 원본 로그인만 미리: 대상 계정 없이 두 쪽 모두 mailcow 로 --justlogin(이전용 비밀번호 파일이 아직 없어도 된다). -# 끝 줄이 `Exiting with return value 0` 이면 된다 — 아니면 grep 없이 다시 보고 아래 "실패할 때"로(되풀이하지 않는다 — 한 번에 실패 둘이 mailcow 의 Fail2ban 에 센다). -# 된 뒤 15분 안에 mailcow PC 에서 서버 B 가 어떤 주소로 보였는지 본다(위 "원본"의 접속 주소) +# 끝 줄이 `Exiting with return value 0` 이면 된다 — 아니면 grep 없이 다시 보고 아래 "실패할 때"로(되풀이하지 않는다 — 한 번에 실패가 둘이고, Dovecot 이 바깥 직원 모두와 같은 주소의 로그인을 늦춘다). +# 된 뒤 15분 안에 mailcow PC 에서 서버 B 가 어떤 주소로 보였는지 본다(예상 10.9.0.1 — 위 "원본"의 접속 주소) A=hong@arasystem.it.kr sudo docker run --rm --network host --user 65534:65534 --cap-drop ALL --security-opt no-new-privileges --read-only --tmpfs /tmp --tmpfs /var/tmp \ --mount type=bind,src=/root/imapsync/mailcow-master.pass,dst=/run/secrets/master,readonly \ @@ -1653,7 +1657,7 @@ sudo rm -f /root/imapsync/mailcow-master.pass /root/imapsync/migration.pass | `… SSL connect attempt failed … certificate verify failed` · `hostname verification failed` (10) | 인증서 — 자체 서명이거나 이름이 다르다(`--add-host` 없이 주소로 붙음) | 위 `--host2` · 인증서 | - 같은 실패를 모두에게 되풀이하지 않는다 — 엔진은 IP 마다 인증 실패 하루 100회면 그 IP 를 막는다(자동 차단 — 위 "setup 스크립트가 하는 일" 4. 서버 B 의 사설 IP 는 신뢰 대역 밖이다). - 한 사람에서 실패하면 멈추고 고친다. mailcow 쪽은 Fail2ban 이다(준비의 예외). + 한 사람에서 실패하면 멈추고 고친다. mailcow 쪽은 Fail2ban 이 막지 않지만(서버 B 도 사설 `10.9.0.1` 로 보인다) Dovecot 이 그 주소 — 바깥 직원 모두 — 의 로그인을 늦춘다(위 "원본"의 접속 주소). - 한쪽 로그인이 실패해도 imapsync 는 다른 쪽 로그인을 해 본 뒤 끝난다(`--justlogin` 에서 봤다) — 출력의 `Err 1/2` 처럼 실패가 둘로 보이면 먼저 나온 것부터 본다. #### 개발에서 확인한 것 (imapsync — 2026-11-02) @@ -1683,7 +1687,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(2026-05-12 판)의 마스터 계정과 mailcow 가 서버 B 를 보는 주소(준비의 "원본 로그인만"으로 본다) · Allowed protocols, 서버 B 의 길(`--network host` + `--add-host` 와 엔진이 보는 주소), 운영의 양 · 시간, 직원이 만든 실제 폴더 이름. +- **확인하지 않은 것** — 운영 mailcow(2026-05-12 판)의 마스터 계정과 서버 B 도 `10.9.0.1` 로 보이는지(준비의 "원본 로그인만"으로 본다) · Allowed protocols, 서버 B 의 길(`--network host` + `--add-host` 와 엔진이 보는 주소), 운영의 양 · 시간, 직원이 만든 실제 폴더 이름. ---