docs: 메일 모듈에 맞춰 README·규칙·스킬 문서 갱신

구현한 인증·메일·인프라에 맞춰 공통 규칙과 스택별 스킬 문서를 실제 파일과 일치시킨다.

- security: 세션 회전·로그인 실패 한도·출처 검사·프록시 신뢰·네트워크 분리·게이트웨이 비밀 등급,
  외부로 나가는 지문은 받는 쪽이 모르는 비밀로 HMAC, 의존성 유예 표(2026-10-27 점검)와 보안 릴리스 예외
- error-codes: AUTH_004 와 세션 만료(AUTH_002) 흐름
- env-structure·checklist: 메일 엔진 env 와 초기 설정 순서(관리자 아이디는 bootstrap 전에)
- nestjs·vue3·docker 스킬: 기본 제공 골격 표와 확장 규칙, Node·npm·이미지 고정 위치

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 01:32:55 +09:00
parent a5afbc040e
commit d9ad6fc178
8 changed files with 355 additions and 46 deletions
+27 -5
View File
@@ -13,11 +13,21 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{
│ (docker compose --env-file 로 로드)
├─▶ backend : env_file + environment 로 DB/Redis/CORS/포트 주입
│ (backend/.env.{APP_ENV} 는 "존재"만 하면 됨, 내용 최소)
├─▶ frontend : BACKEND_API_URL → build arg(VITE_API_BASE_URL) 로 주입
├─▶ frontend : 빌드 arg VITE_API_BASE_URL=/api 고정 (같은 주소 배포 — env 로 바꾸지 않는다)
│ 런타임 env MAIL_ATTACHMENT_MAX_SIZE_MB·UPLOAD_MAX_SIZE_MB → nginx 본문 한도,
│ NGINX_BACKEND_UPSTREAM=http://${PROJECT_NAME}-backend:${BACKEND_PORT} (frontend/nginx-env.envsh — 값이 틀리면 기동 중단)
│ (호스트에서 `npm run dev` 로 띄울 때는 compose 를 거치지 않으므로
│ vite.config.ts 가 이 파일을 `loadEnv` 로 직접 읽어 FRONTEND_PORT/BACKEND_PORT 사용)
├─▶ network : APP_NETWORK_SUBNET → app-network 서브넷 고정 (메일 엔진의 신뢰 대역 — 사설 대역 /24~/28, frontend 는 붙지 않는다)
│ EDGE_NETWORK_SUBNET → edge(frontend ↔ backend) 서브넷 고정 (TRUST_PROXY 주소 목록용)
├─▶ db : DB_USER/DB_PASSWORD/DB_NAME, DB_DATA_DIR 볼륨
└─▶ redis : REDIS_PASSWORD, REDIS_DATA_DIR 볼륨
├─▶ redis : REDIS_PASSWORD, REDIS_DATA_DIR 볼륨
├─▶ uploads : 운영은 named volume <PROJECT_NAME>_uploads (env 없음), 개발만 UPLOAD_DATA_DIR 바인드 마운트
├─▶ mail : MAIL_PUBLIC_URL·MAIL_RECOVERY_ADMIN → STALWART_* (backend 에는 MAIL_JMAP_URL·MAIL_DOMAIN·MAIL_GATEWAY_*·MAIL_ATTACHMENT_*·
│ MAIL_ADMIN_LOCAL_PART·MAIL_BLOCKED_ACCOUNTS)
│ 운영의 MAIL_RECOVERY_ADMIN 은 파일에 두지 않고 셸 일회성 override 로만 준다 (mail/README.md "운영 배포")
│ MAIL_ADMIN_LOCAL_PART 는 setup.mjs 와 backend 가 함께 읽는다 — bootstrap 전에만 정할 수 있다
└─▶ mail/scripts/setup.mjs : `node --env-file=.env.{APP_ENV}` 로 같은 파일을 직접 읽는다 (mail/README.md)
```
규칙:
@@ -33,18 +43,30 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{
포트를 `frontend/.env` 나 `vite.config.ts` 에 중복 작성하지 말 것 — 루트가 SSOT 다.
(읽은 값은 dev 서버 설정에만 쓰고 클라이언트 번들에는 넣지 않는다 — `VITE_` 없는 값의 번들 노출 금지, [[security]])
- **Flutter 는 예외** — docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립적으로 관리한다.
- **루트 env 를 Node 스크립트가 직접 읽을 때**(`node --env-file=…`) 두 가지를 주의한다.
- Node 는 **셸에 이미 있는 변수를 덮어쓰지 않는다** — 셸에 `APP_ENV=development` 가 남은 채 `.env.production` 을 주면 개발 값으로 돈다.
스크립트는 env 파일 값과 셸 값이 다르면 중단해야 한다 (`mail/scripts/setup.mjs` 의 `findShellOverrides()`).
- Node 는 따옴표 없는 값의 `#` 부터를 **공백이 없어도** 주석으로 자르고, compose 는 그대로 읽는다.
비밀번호 등은 영문·숫자로만 만든다 (`#`·따옴표·`$`·공백 금지).
- 모든 env 파일은 커밋 금지, `*.example` 만 커밋한다. (`.gitignore` 의 `!*.example` 재포함 규칙)
- **설정이 잘못되면 서버가 뜨지 않아야 한다(fail-fast).** 조용히 기본값으로 굴러가다 운영에서 터지는 것이 더 위험하다.
검증은 `backend/src/shared/config/env.validation.ts` 가 담당하며 `ConfigModule.forRoot({ validate })` 에 연결되어 있다.
- 숫자 항목은 형식 검사 (모든 환경)
- 운영 환경: `DB_HOST`/`DB_USER`/`DB_PASSWORD`/`DB_NAME` 필수
- 운영 환경 필수: `DB_HOST`/`DB_USER`/`DB_PASSWORD`/`DB_NAME`, `JWT_SECRET`, `MAIL_JMAP_URL`/`MAIL_DOMAIN`/`MAIL_GATEWAY_*`,
`CORS_ORIGIN`(화면의 공개 주소 — 상태 변경 요청의 출처 검사), `TRUST_PROXY`(백엔드 앞 프록시 홉 수 — 운영에서 `true`·`0`·`false` 는 거부)
- 운영 환경: `change-me`·`password` 같은 **예시 값이 그대로 배포되는 것을 차단**
- 운영 환경: `CORS_ORIGIN`·`BACKEND_API_URL` 에 **로컬 주소(`localhost`·`127.0.0.1` 등)나
예시 도메인(`example.com`)이 남아 있으면 차단** — 기동은 되는데 브라우저만 조용히 실패하는 조합을 막는다.
로컬에서 운영 이미지를 검증하느라 일부러 localhost 를 쓸 때만 `ALLOW_LOCALHOST_URL=true` 로 건너뛴다
(`BACKEND_API_URL` 은 앱이 쓰지 않지만 이 검사를 위해 compose 가 backend 에 주입한다)
(`BACKEND_API_URL` 은 앱도 프론트 빌드도 쓰지 않는다 — 같은 주소 배포라 "화면 주소 + /api" 이고, 이 검사를 위해서만 compose 가 backend 에 주입한다)
- 문제는 하나씩이 아니라 **한 번에 모아서** 보여준다 (재기동 반복 방지)
- 새 필수 설정을 추가하면 이 파일의 목록과 테스트도 함께 갱신한다
> 루트 env의 대표 키: `PROJECT_NAME`, `APP_ENV`, `BUILD_MODE`, 각종 `*_PORT`, `BACKEND_API_URL`, `CORS_ORIGIN`, `DB_*`/`DB_DATA_DIR`, `REDIS_*`/`REDIS_DATA_DIR`, `THROTTLE_TTL_MS`/`THROTTLE_LIMIT`. 실제 목록은 `.env.development.example` 참조.
> 루트 env의 대표 키: `PROJECT_NAME`, `APP_ENV`, `BUILD_MODE`, 각종 `*_PORT`, `APP_NETWORK_SUBNET`·`EDGE_NETWORK_SUBNET`, `BACKEND_API_URL`, `CORS_ORIGIN`, `TRUST_PROXY`,
> `DB_*`/`DB_DATA_DIR`, `REDIS_*`/`REDIS_DATA_DIR`, `THROTTLE_TTL_MS`/`THROTTLE_LIMIT`, `UPLOAD_MAX_SIZE_MB`(·개발 전용 `UPLOAD_DATA_DIR`),
> 메일 `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_JMAP_URL`·`MAIL_ATTACHMENT_MAX_SIZE_MB`/`MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB`·`MAIL_TRUSTED_NETWORKS`(비우면 `APP_NETWORK_SUBNET`, 그보다 넓으면 setup 실패)·
> `MAIL_BLOCKED_ACCOUNTS`(선택 — 관리자 외에 웹메일 로그인·대리 접근을 막을 주소, 콤마. 비어 있어도 관리자는 항상 막힌다)
> (개발 전용 `MAIL_ADMIN_PORT`·`MAIL_DEV_USER_PASSWORD`·`MAILPIT_IMAGE`/`MAILPIT_UI_PORT`). 실제 목록은 `.env.development.example` 참조.
> 실행/배포 명령은 [[docker]], 보안 규칙은 [[security]] 참조.