Commit Graph

7 Commits

Author SHA1 Message Date
ttipo 9466a85daa feat(docker): 메일 엔진 전용 DNS 리졸버(unbound)와 Spamhaus 조회 키(DQS)
엔진의 DNS 조회(DNSBL · SPF · DKIM · DMARC · MX · PTR · TLSA)를 공용 · ISP DNS 가 아니라
이 서버의 리졸버로 푼다. Spamhaus 는 공용 DNS 를 거친 조회를 거부하고, DNSSEC 을 검증해야
엔진의 DANE 이 켜지기 때문이다.

- mail-resolver: unbound 1.26.2(digest 고정 · 비-root · cap_drop ALL). mail 과만 닿는
  internal 네트워크 mail-dns 와 바깥 길 mail-dns-egress. 사설 주소의 답 · 질의를 막는다.
- setup: 바꾸기 전에 리졸버 네트워크 · 주소 · DNSSEC 을 확인한다(UDP 는 루트 SOA, TCP 는
  루트 DNSKEY — UDP DNSKEY 는 루트 키 교체 기간에 잘려 오탐이 난다). 엔진 DNS 는 운영 Custom,
  개발 System. Spamhaus 키가 있으면 시험 항목을 확인한 뒤 DQS 로 켜고 없으면 두 항목을 끈다.
  키는 모든 출력에서 가린다.
- mail 은 리졸버를 기다리되(required: false) 리졸버 오류 하나로 메일 전체가 서지 않게 한다.
- README: 리졸버와 스팸 차단 목록, 운영 배포 차례, 리졸버 장애 때(리로드 거절 · restart 비상 길),
  DNSSEC 이 깨진 도메인의 예외.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:01:12 +09:00
ttipo 3029bb20f2 feat(docker): 리버스 프록시가 다른 서버에 있을 때 — frontend 를 사설 IP 에만 게시하는 선택 파일
운영 메일 서버를 NCP 에 따로 두고, 포털의 TLS 는 기존 서버의 Nginx Proxy Manager(다른 사이트들과 80 · 443 을 함께 쓴다)가
같은 VPC 사설망으로 넘기기로 했다. 메일 엔진이 그 서버의 443 을 직접 써야 해서(ACME · MTA-STS) 프록시를 같은 호스트에 둘 수 없다.

- docker-compose.remote-proxy.yml: frontend 를 proxy 네트워크에서 빼고 8080 을 FRONTEND_PRIVATE_BIND(사설 IPv4:포트)에만 게시한다.
  compose 의 ${…:?} 는 빈 값만 막아 포트뿐인 값이면 모든 주소에 열리므로, 한 번짜리 frontend-bind-check 가 값을 먼저 보고
  아니면 frontend 를 띄우지 않는다(네트워크 없음 · 비-root · DB 이미지의 셸).
- .env.production 의 COMPOSE_FILE 로 켠다 — 운영 명령(--env-file .env.production)을 그대로 쓰고 -f 를 빠뜨려 기본 구성으로 도는 일이 없게.
- TRUST_PROXY 는 <EDGE_NETWORK_SUBNET>,<프록시 서버 사설 IP> 주소 목록, NPM 은 v2.16.0 소스로 기본값을 확인해 고급 칸 세 줄
  (읽기 150s · 접근 로그 끔 · 사설 대역의 X-Real-IP 믿지 않기)과 Block Common Exploits 끄기(메일 검색이 403)를 적었다.
- NCP 는 공인 IP 를 사설 IP 로 NAT 하므로 게시 주소로는 바깥을 막지 못한다 — 8080 은 ACG 가 프록시 서버 사설 IP 에서만 받는다.
- CI infra 잡이 이 조합의 config 와 frontend-bind-check 의 받을 값 · 거절할 값을 본다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:07:15 +09:00
ttipo 258ebf2180 feat(docker): mail setup — 파일럿 공존(한 사람 먼저) 켜기 · 끄기와 자체 확인
전면 전환 전에 한 사람만 같은 @arasystem.it.kr 주소로 새 서버를 쓰고 나머지 직원은 mailcow 에 둔다.
MX 는 mailcow 그대로이고 mailcow 의 Transport Map 이 시범 주소 하나만 새 서버로 넘긴다 —
새 서버는 자기에게 없는 사내 주소를 mailcow 로 중계해야 해서 루트 env 두 개(MAIL_PILOT_LEGACY_HOST ·
MAIL_PILOT_LEGACY_IPS)로 setup 이 맞추게 했다.

- 켜는 차례는 막는 것부터: 시범 표시(기존 서버 IP 의 자동 차단 예외) → RCPT 가드(인증 없는 세션의 없는 사내 주소 550) ·
  위조 막기(사내 도메인 MAIL FROM 은 mailcow IP 에서만) · 그 IP 의 SPF · 역방향 DNS · DMARC 검사와 받는 쪽 한도 예외 →
  리로드 → 켜기 전 가드 확인 → legacy 경로 · 발송 전략(인증한 제출 · 엔진이 만든 메일만 legacy) → 리로드 →
  allowRelaying → 켠 뒤 확인. allowRelaying 은 저장 즉시 먹어 가드보다 먼저 켜면 열린 중계가 된다.
- 가드 없는 relay 를 setup 이 만들지 않는다: 가드는 "2초 안에 550"으로 가린다(가드 거절은 곧바로 · 엔진 자체 거절은
  waitOnFail 5초 뒤). 아니면 켜지 않거나 바로 다시 끄고, 끄지 못하면 가드만이라도 다시 건 뒤 사실대로 알리고 멈춘다.
  끌 때는 allowRelaying 을 먼저 끄고 엔진에서 다시 읽어 꺼진 것을 본 뒤에만 나머지를 뺀다 — 주인을 모르는 relay 는
  끄지도 가드를 빼지도 않는다.
- 설명 칸이 없는 식의 주인은 시범 표시의 주소로 가린다 — 같은 꼴의 운영자 설정은 덮어쓰지 않고, 헷갈리면 멈춘다.
- 운영은 env 파일의 값으로만 켜고(포트 25 만 · 문서 대역 · 사설로 풀리는 이름 · 자기 자신 거부), 값이 빠졌다고
  저절로 끄지 않는다(끄기는 --end-pilot). 처음 켤 때 DKIM TXT 가 없거나 엔진의 키와 다르면 켜지 않는다.
  개발은 셸 값으로 한 번 시험한다.

리뷰 세 번(H1 · M1 · M2 · L1~L9 → N1~N8 → R1~R4)을 반영했다. 개발 엔진(0.16.27, 기존 서버 역 Mailpit)에서
켬 · 같은 값으로 다시([유지]) · 끔을 돌려 끈 뒤 엔진 설정이 처음과 같았다.
mailcow 쪽 절차와 운영 흐름은 아직 해 보지 않았다 — mail/README.md "파일럿 공존".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:37:25 +09:00
ttipo 59420df1b9 feat(docker): mail setup — 사내 목록 계정 · 메일을 받지 않는 시스템 계정의 RCPT 거절
사내 목록은 엔진의 계정 목록(이름 · 주소)을 읽어야 하는데, 직원 역할은 그 권한을 일부러 막아 두었고 엔진 관리자는 그 권한을 내주지 못한다.
그래서 setup 이 복구 관리자로 돌 때 비밀번호 없는 읽기 전용 계정(MAIL_DIRECTORY_LOCAL_PART, 기본 mail-directory)을 만든다 —
백엔드는 게이트웨이 대리 접근으로만 쓴다. 메일을 받지 않게 역할에서 받기 권한을 빼고 저장 한도도 작게 둔다.

관리자 · 게이트웨이 · 사내 목록 계정은 메일을 받지 않는데, 엔진은 RCPT 를 받아 놓고 배달할 때 거절해 보낸 사람에게 반송했다(backscatter —
위조된 발신자에게 반송이 나가 서버 평판이 떨어진다). RCPT 단계 스크립트로 이 주소(+태그 꼴 포함)를 없는 주소처럼 550 으로 거절하고,
setup 이 끝날 때 RCPT 만 보내 스스로 확인한다. 다른 설정이 그 자리를 쓰고 있으면 건드리지 않고 알린다.
일반 관리자는 공용 주소(Group 계정)를 만들지 못한다는 것도 운영 배포 절차에 적었다 — 복구 관리자로 만든다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:06:27 +09:00
ttipo ab4124ba94 fix(docker): mail setup — 엔진 임시 업로드 한도를 첨부 합계의 20배로
엔진(Stalwart 0.16.27)은 계정마다 1시간 동안 올린 양을 50MB 까지만 받는다(x:Jmap 의 uploadQuota).
20MB 첨부 셋이면 그 직원의 첨부 올리기와 필터 · 부재중 저장(Sieve 스크립트도 업로드로 올린다)이
한 시간까지 429 REQ_001 로 막혔다(2026-10-29 개발 엔진에서 확인).

- setup 이 uploadQuota 를 첨부 합계 한도(MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB — 백엔드와 같은 계산)의
  20배 이상으로 올린다. 더 크게 둔 값은 내리지 않고, 개수 · 기간 · 한 번 크기는 그대로다
- 첨부 한도 env 를 바꾸면 setup 도 다시 돌린다(env 예시 · mail/README.md · docker 스킬 · env 규칙)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:35:20 +09:00
ttipo 4d95fc28d8 fix(docker): mail setup — 받는 사람 단위 받는 쪽 한도를 인증 없는 세션에만
- 엔진 기본 "Sender address to recipient throttle"(같은 보낸 도메인 → 한 받는 사람, 시간당 25)의 match 를 is_empty(authenticated_as) 로 —
  사내 메일이 동료 모두 합쳐 한 사람에게 25통을 넘으면 웹메일 발송이 452 로 거절되던 문제. 바깥에서 들어오는 메일에는 그대로
- "Sender IP throttle"(초당 연결 5개)은 그대로 — 연결 단계 한도라 웹메일 발송에는 걸리지 않고, 조건을 붙이면 MAIL FROM 단계로 옮겨진다
- mail/README.md "엔진 한도" — 기본값 · 바꾸는 까닭 · 확인 결과 · 운영 전에 정할 것

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:11:26 +09:00
ttipo 238d0ba838 chore(docker): 메일 엔진·네트워크 분리·같은 주소 배포와 Node 24 고정
웹메일 1단계를 개발 환경에서 띄우고 운영에 올리기 위한 인프라 구성이다.

- 메일 엔진 Stalwart(v0.16.27)와 개발용 Mailpit 을 compose profile `mail` 로 추가한다.
  초기 설정은 mail/scripts/setup.mjs 가 맡는다 — 여러 번 실행해도 안전하고,
  운영에서는 사전 검사가 실패하면 아무것도 바꾸지 않고 멈춘다
- 화면과 API 를 한 주소로 배포한다: 프론트 nginx 가 /api 를 백엔드로 넘기고,
  본문 한도·시간 제한·헤더 전달을 env 와 맞춘다. nginx 가 직접 내는 오류도 표준 실패 형식이다
- 인터넷을 향한 nginx 는 edge 네트워크로 백엔드에만 닿고, DB·메일 엔진은 app-network 에 둔다
  (메일 엔진의 신뢰 대역도 app-network 뿐)
- Node 24.22.1·npm 11.6.2·nginx 1.30.6 을 Dockerfile 과 CI 에 한 값으로 고정한다
- 개발 호스트 포트는 127.0.0.1 로 한정하고, data/ 는 클론 때부터 사용자 소유가 되게 .gitkeep 을 추적한다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 01:31:21 +09:00