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>
This commit is contained in:
2026-10-31 00:37:21 +09:00
parent c5cc1a6705
commit 93147b88b7
19 changed files with 1176 additions and 103 deletions
+48
View File
@@ -0,0 +1,48 @@
# ==========================================
# /api 의 nginx 오류 응답 — 두 server(포털 · 공개 내려받기)가 넣는다 (nginx.conf). 템플릿이 아니다(/etc/nginx/snippets/api-errors.conf)
#
# message 는 비운다 (문구는 클라이언트가 code 로 사전에서 찾는다, 레지스트리와 겹치지 않게).
# traceId 는 이 nginx 의 요청 ID(로그의 rid=)다. 이름 있는 location 은 server 단계라 화면용 헤더 상속을 끈다
# (접근 로그는 server 단계의 쿼리 없는 main_rid 를 그대로 물려받는다).
# return 200 은 본문만 정한다 — error_page 를 = 없이 썼으므로 상태 코드는 원래 값(404·413·429·502·504 등)이 그대로 나간다 (2026-10-27 확인)
# ==========================================
# nginx 가 닫은 경로 404 — 공개 내려받기 server 의 /api/(공개 API 밖) · 포털의 공개 API(공개 호스트를 따로 둘 때). 없는 경로와 같은 RES_001
location @api_not_found {
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 200 '{"success":false,"error":{"code":"RES_001","message":"","traceId":"$request_id"}}';
}
# 본문 한도 초과 413 — 첨부 · 업로드 · 대용량 조각 · 공개 내려받기 server 의 /api/(1m)
location @api_payload_too_large {
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 200 '{"success":false,"error":{"code":"FILE_002","message":"","traceId":"$request_id"}}';
}
# 동시 연결 수 초과 429 — 대용량 받기의 limit_conn (11단계 — nginx.conf 의 http 단계 "동시 연결 수"). 백엔드의 요청 수 한도와 같은 REQ_001
location @api_too_many_requests {
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 200 '{"success":false,"error":{"code":"REQ_001","message":"","traceId":"$request_id"}}';
}
# 백엔드 응답 없음 · 시간 초과 등 5xx
location @api_unavailable {
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 200 '{"success":false,"error":{"code":"SYS_001","message":"","traceId":"$request_id"}}';
}