Commit Graph

8 Commits

Author SHA1 Message Date
ttipo d57e6bfe6c chore(docker): DB 메모리 한도를 1g · 공유 버퍼를 256MB 로 올린다(14단계 검색 색인)
1만 통 측정에서 색인 찾기는 표 · 색인이 메모리에 있을 때만 1초 안이었고 캐시가 비면
3~8배 느렸다(사용자 결정 2026-11-05). 8GB 서버의 한도 합은 6.65GB · 백업 때 7.65GB 다.
복원 연습 때는 8.15GB 로 8GB 를 넘는다 — 사실만 적고 다룰 방법은 따로 정한다.
mail/README 에 운영에 올리는 차례(db 만 먼저 up · 한도 · shared_buffers 확인)를 적었다.
docker 스킬의 연습 주기를 기준 문서(README — 한 달에 한 번)에 맞췄다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:00:44 +09:00
ttipo 73babeaa2c feat(docker): 13단계 운영 기반 — 백업 · 감시 · 관리자 단단히 · 휴지통 30일 · 메일 프로그램 자동 설정
전 직원 전환 전에 서버가 스스로 지키고 되살릴 수 있게 했다.
- 관리자 단단히(setup): 관리자 역할에서 impersonate 를 빼고 관리자 허용 IP 를 앱 네트워크로 — 관리자 비밀번호로 바깥 993 · JMAP 에서 직원 메일함에 대리 로그인되던 길을 막았다.
  운영에서 처음 걸 때는 되돌릴 복구 관리자가 있을 때만 건다(관리자 잠김 방지).
- 휴지통 · 스팸 30일 정리를 KST 03:00 으로 명시. imapsync 로 옮긴 메일도 옮긴 때부터 센다(임시 엔진 실측).
- restic 백업(호스트 systemd 03:30 — 메일 볼륨 warm → 엔진 멈춤 9초 안팎 → final, pg_dump, 업로드, env) · 복원(새 서버 차례를 빈 클론에서 실측) · 15분 점검 · 부팅 되살리기, Healthchecks 하트비트.
- 메일 프로그램 자동 설정: frontend nginx 의 autoconfig · autodiscover 고정 응답(엔진 것은 POP3 를 알리고 입력을 되돌려 쓴다).
- 모든 서비스 docker 로그 보존, DB 이미지 판 · digest 고정.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:44:44 +09:00
ttipo d1ef0da488 feat(docker): 12단계 계정 보안 — 엔진 웹훅 · /api/internal 닫기 · 요청 대상 400
메일 프로그램(IMAP · SMTP) 로그인 기록을 엔진 웹훅으로 받는다.
- 루트 env MAIL_WEBHOOK_SECRET(setup · backend 공용, 운영 필수)과 backend 의 APP_NETWORK_SUBNET
- setup 이 엔진 웹훅을 맞추고(표시 머리글 — 소금 + 키 지문) 서명한 빈 묶음으로 받는 곳을 확인한다
  키를 바꾼 실행 · 게이트웨이 OTP · 테스트 직원의 402 를 따로 다룬다
- nginx 두 server 가 /api/internal/ 을 닫고, 요청 대상의 날 \(경로) · # · 0x80 이상은 location 전에 400
  Express 가 이런 대상을 다시 읽어 닫은 경로(/api/internal · /api/health)가 백엔드에서 열렸다(2026-10-31 리뷰)
- CI "닫는 경로 시험"(두 server × 세 경로의 변형) · 지시어만 세는 정적 검사
- mail/README: 웹훅 · 12단계 전부터 운영 중인 서버를 올리는 차례 · 휴대폰을 잃은 직원(관리 API 명령)
엔진 DNS 가 Custom(운영)이어도 웹훅은 backend 를 푼다 — 메일 컨테이너가 도커 DNS 를 쓴다(개발에서 확인).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:45:23 +09:00
ttipo 93147b88b7 feat(docker): 대용량 첨부 — ClamAV 검사기 · 공개 내려받기 호스트 · 받기 동시 연결 상한
- ClamAV 1.4.6 LTS 를 둘로 나눴다: clamd 는 백엔드와만 닿는 internal 네트워크(바깥 길 없음 · 서명 볼륨 읽기 전용),
  freshclam 은 자기 바깥 길에만. 비-root · cap_drop ALL. 운영은 늘 뜨고 개발은 profile clamav.
  clamd.conf 는 한 파일 한도 2047M 까지 끝까지 검사하고 암호 · 한도 초과를 판정으로 알린다(실측표는 mail/README).
  검사 중에 죽으면 남던 임시 파일을 띄울 때 지운다.
- nginx 를 포털 · 공개 내려받기(files.) 두 server 로 가른다(SHARE_PUBLIC_URL 과 CORS_ORIGIN 의 호스트 비교 — 같으면 포털이 함께 연다).
  조각 PUT 17m, 받기는 버퍼링 · 압축 끔 + X-Accel-Buffering 넘김 + 머리글 버퍼 16k,
  limit_conn 은 X-Forwarded-For 마지막 칸으로 IP 마다 공개 10 · 앱 안 30, 전체 공개 400 · 앱 안 200.
  받기 location 은 /api/ 블록 안의 대소문자를 가리지 않는 정규식이다(리뷰 — /FILES/ 같은 변형이 상한을 비켜 갔다).
  접근 로그는 경로 모양과 43자 토큰 모양으로 토큰을 가린다.
- 파일은 named volume <PROJECT_NAME>_files(개발도 — builder 단계가 /app/files 를 node 소유로 만든다), 백업하지 않는다.
- CI infra 잡: nginx 두 꼴 · 받기 경로 변형 시험(가짜 백엔드) · clamconf(-n 없이 기본값까지) · 틀린 SHARE_PUBLIC_URL 거부.
- mail/README: 악성 검사 · 공개 호스트 · NPM 칸(Advanced 다섯 줄) · 메모리 예산 · 문제 해결.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:37:21 +09:00
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 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 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
ttipo 18f10cece4 Initial commit 2026-10-26 12:28:48 +09:00