diff --git a/.claude/rules/env-structure.md b/.claude/rules/env-structure.md index b4fe872..05cd94a 100644 --- a/.claude/rules/env-structure.md +++ b/.claude/rules/env-structure.md @@ -30,6 +30,7 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{ │ MAIL_DIRECTORY_LOCAL_PART(사내 목록 계정, 기본 mail-directory)도 함께 읽는다 — 계정은 setup 이 복구 관리자로만 만든다(운영 --recovery) └─▶ mail/scripts/setup.mjs : `node --env-file=.env.{APP_ENV}` 로 같은 파일을 직접 읽는다 (mail/README.md) MAIL_ATTACHMENT_*(첨부 한도)도 읽는다 — 엔진의 임시 업로드 한도를 그 20배로 맞추므로 첨부 한도를 바꾸면 setup 도 다시 돌린다 + MAIL_PILOT_LEGACY_HOST · MAIL_PILOT_LEGACY_IPS(10단계 파일럿 공존 — 옛 서버 주소 · 그 서버가 보내는 IP)는 setup 만 읽는다(compose · backend 에 넘기지 않는다) ``` 규칙: @@ -53,6 +54,8 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{ - **루트 env 를 Node 스크립트가 직접 읽을 때**(`node --env-file=…`) 두 가지를 주의한다. - Node 는 **셸에 이미 있는 변수를 덮어쓰지 않는다** — 셸에 `APP_ENV=development` 가 남은 채 `.env.production` 을 주면 개발 값으로 돈다. 스크립트는 env 파일 값과 셸 값이 다르면 중단해야 한다 (`mail/scripts/setup.mjs` 의 `findShellOverrides()`). + 예외는 복구 관리자 `MAIL_RECOVERY_ADMIN`(원래 셸에서 일회성으로 주는 값이라 검사하지 않는다)과 파일럿 키 둘이다 — 파일럿 키는 개발은 한 번 시험하려고 셸로 줄 수 있고(`[참고]` 를 낸다), + 운영은 반대로 파일에 없는 파일럿 값이 셸에 있으면 중단한다 (파일럿은 엔진에 남는 설정이라 파일만 보고도 알 수 있어야 한다). 운영에서 파일럿을 끄는 것은 값을 비우고 `--end-pilot` 을 줄 때뿐이다 — 값이 빠졌다고 저절로 끄지 않는다. - Node 는 따옴표 없는 값의 `#` 부터를 **공백이 없어도** 주석으로 자르고, compose 는 그대로 읽는다. 비밀번호 등은 영문·숫자로만 만든다 (`#`·따옴표·`$`·공백 금지). - 모든 env 파일은 커밋 금지, `*.example` 만 커밋한다. (`.gitignore` 의 `!*.example` 재포함 규칙) @@ -74,6 +77,7 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{ > 메일 `MAIL_IMAGE`(패치까지 고정)·`MAIL_DOMAIN`·`MAIL_HOSTNAME`·`MAIL_PUBLIC_URL`·`MAIL_ADMIN_LOCAL_PART`(관리자 아이디, 기본 `admin` — setup·backend 공용, bootstrap 전에만)· > `MAIL_ADMIN_PASSWORD`·`MAIL_RECOVERY_ADMIN`(운영은 일회성 override)·`MAIL_GATEWAY_USER`/`MAIL_GATEWAY_PASSWORD`·`MAIL_DIRECTORY_LOCAL_PART`(사내 목록 계정 아이디, 기본 `mail-directory` — setup·backend 공용)· > `MAIL_JMAP_URL`·`MAIL_ATTACHMENT_MAX_SIZE_MB`/`MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB`·`MAIL_TRUSTED_NETWORKS`(비우면 `APP_NETWORK_SUBNET`, 그보다 넓으면 setup 실패)· -> `MAIL_BLOCKED_ACCOUNTS`(선택 — 관리자 외에 웹메일 로그인·대리 접근을 막을 주소, 콤마. 비어 있어도 관리자는 항상 막힌다) +> `MAIL_BLOCKED_ACCOUNTS`(선택 — 관리자 외에 웹메일 로그인·대리 접근을 막을 주소, 콤마. 비어 있어도 관리자는 항상 막힌다)· +> `MAIL_PILOT_LEGACY_HOST`/`MAIL_PILOT_LEGACY_IPS`(선택 — 파일럿 공존 동안만, setup 전용. mail/README.md "파일럿 공존") > (개발 전용 `MAIL_ADMIN_PORT`·`MAIL_DEV_USER_PASSWORD`·`MAILPIT_IMAGE`/`MAILPIT_UI_PORT`). 실제 목록은 `.env.development.example` 참조. > 실행/배포 명령은 [[docker]], 보안 규칙은 [[security]] 참조. diff --git a/.claude/skills/docker/SKILL.md b/.claude/skills/docker/SKILL.md index 6300747..b736d4c 100644 --- a/.claude/skills/docker/SKILL.md +++ b/.claude/skills/docker/SKILL.md @@ -317,6 +317,23 @@ node --env-file=.env.development mail/scripts/setup.mjs - 영구 관리자 아이디는 `MAIL_ADMIN_LOCAL_PART`(기본 `admin`) — setup 과 백엔드가 함께 읽는다 (compose 가 backend 에 넘긴다). **bootstrap 전에만 정할 수 있다** — 바꾼 값이 엔진의 관리자와 다르면 setup 이 멈춘다. 다른 관리자 계정은 `MAIL_BLOCKED_ACCOUNTS` 에 넣는다. - 사내 목록 계정 `@도메인`(기본 `mail-directory`, compose 가 backend 에 넘긴다) — 백엔드가 엔진의 계정 목록에서 이름·주소만 읽는 비밀번호 없는 계정(게이트웨이 대리 접근으로만). 엔진 관리자는 그 권한을 내줄 수 없어 setup 이 **복구 관리자로 돌 때만** 만든다(개발은 늘, 운영은 `--recovery` — 이미 운영 중이면 한 번 더). 없으면 `[주의]` 만 내고 백엔드는 로그인한 직원 기록으로 대신한다 (mail/README.md "사내 목록 계정"). +- **파일럿 공존**(10단계 — mail/README.md "파일럿 공존"): 루트 env `MAIL_PILOT_LEGACY_HOST`(옛 서버 mailcow 주소)·`MAIL_PILOT_LEGACY_IPS`(그 서버가 보내는 IP)를 넣으면 + setup 이 새 서버에 없는 사내 주소를 옛 서버로 중계하게 한다. 켜는 차례는 막는 것부터 — 시범 표시(옛 서버 IP 의 자동 차단 예외 — 아래 식들의 주인 표시) → + 받는 쪽 가드(인증 없는 모르는 사내 주소 550)·위조 막기·옛 서버 IP 의 검사 · 한도 예외 → 리로드 → **켜기 전 가드 확인** → 경로(리로드) → + 도메인의 `allowRelaying`(저장 즉시 먹어 가드보다 먼저 켜면 열린 중계가 된다) → 켠 뒤 확인. 끌 때는 거꾸로(`allowRelaying` 먼저) — 운영은 값을 비우고 `--end-pilot` 일 때만. + **가드 없는 relay 를 setup 이 만들지 않는다** — 가드 확인은 "2초 안에 550"이다(가드 거절은 곧바로 · 엔진 자체 거절은 `waitOnFail` 5초 뒤라 응답 시간으로 가른다 — 그래서 `waitOnFail` 은 4초 이상). + 켜기 전에 아니면 켜지 않고, 켠 뒤에 아니면 relay 를 바로 다시 끈다(둘 다 종료 코드 1). 끄지 못하면 "끄지 못했습니다"로 멈춘다 — 이미 켜진 relay 를 처음 확인할 때(0단계)는 + 멈추기 전에 RCPT 가드만 다시 걸고 그 결과를 싣는다. "대신 RCPT 가드를 다시 걸어 지금은 가드가 답합니다"면 열린 relay 는 아니고(relay 는 켜진 채), + "열려 있습니다 · 열려 있을 수 있습니다"면 관리 화면에서 그 도메인의 relay 허용을 바로 끈다. + 가드는 `allowRelaying` 이 꺼진 것을 엔진에서 다시 읽어 확인한 뒤에만 뺀다 — 켜져 있는데 setup 의 `legacy` 경로가 없으면 + 끄지 않고 가드도 남긴 채 종료 코드 1(요약 `끝내지 못함`, 시범 설정이 하나도 없으면 `시범 설정 없음 · allowRelaying 켜짐`). + 개발은 셸로 한 번 시험한다(`MAIL_PILOT_LEGACY_HOST=mailpit:1025 node --env-file=.env.development mail/scripts/setup.mjs` — 다음 평소 실행이 끈다. + 엔진 로그에서 개발 외부 경로 `mailpit` 과 가르려면 컨테이너 이름 `-mailpit:1025` 를 준다). + 운영 확인 항목: 새 호스트 → mailcow **25** 가 열려 있을 것(운영은 25 만 받는다), 파일럿 동안 mx1 에 AAAA 를 두지 않을 것(SPF 는 ip4 만), mailcow 가 보내는 IP 가 목록과 같을 것(빗나가면 동료 메일이 `550 5.7.1`), + 그 IP 로 사람이 만든 자동 차단 예외(`x:AllowedIp`) · 검사 예외(SPF · DMARC 등) · IP 한도 예외가 없을 것(setup 이 시범 표시를 붙여 만든다 — 있으면 멈춘다), + 새 DKIM TXT 를 **켜기 전에** DNS 에 넣을 것(운영은 처음 켤 때 TXT 가 없거나 다르면 켜지 않는다), + 끄기 · 되돌리기 전에 새 서버 대기열에 동료 앞 메일이 남지 않았을 것(끈 뒤에는 로컬 실패 → 반송). + 개발의 인증 발송 확인(465)은 백엔드 컨테이너의 node 로 돈다(메일 이미지에는 node 가 없다 — 비밀번호는 표준 입력으로 넘기고 출력하지 않는다). - ⚠️ **게이트웨이 비밀번호 = 관리자급 비밀.** Stalwart 0.16 은 대리 접근 대상을 제한하지 못해, 신뢰 대역 안에서는 게이트웨이로 관리자 계정도 대리 접근된다 (setup 이 매번 `[주의]` 로 보여 준다). 막는 일은 백엔드가 한다 — mail/README.md "보안".