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>
This commit is contained in:
2026-10-31 17:45:23 +09:00
parent c31442e639
commit d1ef0da488
11 changed files with 739 additions and 19 deletions
+16 -1
View File
@@ -1,5 +1,6 @@
# ==========================================
# /api 의 nginx 오류 응답 — 두 server(포털 · 공개 내려받기)가 넣는다 (nginx.conf). 템플릿이 아니다(/etc/nginx/snippets/api-errors.conf)
# (잘못된 원문 요청 대상의 400 은 화면 경로에도 같은 JSON 이다 — 아래 /__request-target-rejected)
#
# message 는 비운다 (문구는 클라이언트가 code 로 사전에서 찾는다, 레지스트리와 겹치지 않게).
# traceId 는 이 nginx 의 요청 ID(로그의 rid=)다. 이름 있는 location 은 server 단계라 화면용 헤더 상속을 끈다
@@ -7,7 +8,8 @@
# return 200 은 본문만 정한다 — error_page 를 = 없이 썼으므로 상태 코드는 원래 값(404·413·429·502·504 등)이 그대로 나간다 (2026-10-27 확인)
# ==========================================
# nginx 가 닫은 경로 404 — 공개 내려받기 server 의 /api/(공개 API 밖) · 포털의 공개 API(공개 호스트를 따로 둘 때). 없는 경로와 같은 RES_001
# nginx 가 닫은 경로 404 — 공개 내려받기 server 의 /api/(공개 API 밖) · 포털의 공개 API(공개 호스트를 따로 둘 때) ·
# 두 server 의 /api/internal/(엔진만 부르는 경로 — 12단계, internal-api-closed.conf). 없는 경로와 같은 RES_001
location @api_not_found {
add_header_inherit off;
default_type application/json;
@@ -37,6 +39,19 @@ location @api_too_many_requests {
return 200 '{"success":false,"error":{"code":"REQ_001","message":"","traceId":"$request_id"}}';
}
# 원문 요청 대상이 잘못된 400 — 경로의 날 역슬래시 · 날 # · 0x80 이상 바이트 (nginx.conf 의 $request_target_rejected — 두 server 의 server 단계 if 가
# 이리로 넘긴다). 화면 경로든 /api 든 같은 JSON 이다 — 브라우저는 이런 요청을 보내지 않는다. 입력 형식 오류의 VAL_001.
# internal 이라 바깥에서 이 경로를 바로 부르면 404 다
location = /__request-target-rejected {
internal;
add_header_inherit off;
default_type application/json;
add_header Cache-Control "no-store" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Request-Id $request_id always;
return 400 '{"success":false,"error":{"code":"VAL_001","message":"","traceId":"$request_id"}}';
}
# 백엔드 응답 없음 · 시간 초과 등 5xx
location @api_unavailable {
add_header_inherit off;