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
+71 -1
View File
@@ -64,4 +64,74 @@ esac
# 도커 내장 DNS — 15-local-resolvers.envsh 가 /etc/resolv.conf 에서 읽어 둔다 (Dockerfile 의 NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1)
NGINX_LOCAL_RESOLVERS="${NGINX_LOCAL_RESOLVERS:-127.0.0.11}"
export NGINX_MAIL_ATTACHMENT_BODY_LIMIT NGINX_UPLOAD_BODY_LIMIT NGINX_BACKEND_UPSTREAM NGINX_LOCAL_RESOLVERS
# ------------------------------------------
# 대용량 첨부의 공개 내려받기 호스트 (11단계 — docs/mail/api-contract.md 4절 "공개 호스트")
#
# SHARE_PUBLIC_URL(출처 — http(s)://호스트[:포트])의 호스트 이름을 포털(CORS_ORIGIN — 콤마로 여럿)의 호스트 이름들과 견준다.
# 백엔드(modules/file-sharing/file-sharing.settings.ts 의 dedicatedPublicHost)와 같은 판정이다 — 포트는 보지 않는다(nginx 는 포트 없는 Host 를 넘긴다):
# 다르면 공개 내려받기 server 의 이름 = 그 호스트 이름, 포털은 /s/ · /api/public-links/ 를 404 로 닫는다(share-closed · share-api-closed)
# 같거나 SHARE_PUBLIC_URL 이 비었으면 포털이 함께 연다(share-open · share-api-open), 공개 내려받기 server 는 어느 요청에도 맞지 않는 이름(share.invalid — RFC 2606)
# SHARE_PUBLIC_URL 의 모양이 틀리면 기동을 멈춘다 — 경로 · 공백 · ; 등이 server_name 에 들어가 설정을 깨거나 지시어를 끼워 넣지 않게.
# CORS_ORIGIN 의 모양은 백엔드가 본다(운영은 틀리면 백엔드가 뜨지 않는다) — 여기서는 읽을 수 있는 주소만 견준다.
# ------------------------------------------
# 출처 → 호스트 이름 (소문자, 포트 없이). 모양이 틀리면 아무것도 내지 않고 1
nginx_env_origin_host() {
case "$1" in
http://*) nginx_env_hostport="${1#http://}" ;;
https://*) nginx_env_hostport="${1#https://}" ;;
*) return 1 ;;
esac
# 영문 · 숫자 · . - : 만 — 경로(/) · 쿼리 · 공백 · 대괄호(IPv6) · 사용자 정보(@) 를 받지 않는다
case "$nginx_env_hostport" in
"" | *[!A-Za-z0-9.:-]*) return 1 ;;
esac
nginx_env_host="${nginx_env_hostport%%:*}"
nginx_env_port="${nginx_env_hostport#"$nginx_env_host"}"
case "$nginx_env_port" in
"") ;;
:0* | : | :*[!0-9]* | :??????*) return 1 ;;
esac
# 빈 이름 · 점으로 시작 · 끝(끝 점 — nginx 의 $host 는 끝 점을 떼어 백엔드와 어긋난다) · 연이은 점 · 하이픈으로 시작 · 끝
case "$nginx_env_host" in
"" | .* | *. | *..* | -* | *-) return 1 ;;
esac
printf '%s' "$nginx_env_host" | tr 'A-Z' 'a-z'
}
nginx_env_share_url="${SHARE_PUBLIC_URL:-}"
nginx_env_share_host=""
if [ -n "$nginx_env_share_url" ]; then
nginx_env_share_host="$(nginx_env_origin_host "$nginx_env_share_url")" \
|| nginx_env_fail "SHARE_PUBLIC_URL 은 공개 내려받기 화면의 출처(http(s)://호스트[:포트] — 경로 · 끝의 / · 끝 점 없이)여야 합니다 (예: https://files.arasystem.it.kr, 현재: \"$nginx_env_share_url\")."
fi
nginx_env_same_host=""
if [ -z "$nginx_env_share_host" ]; then
nginx_env_same_host=1
else
# 콤마로 나눠 하나씩 — 앞뒤 공백과 끝의 / 하나는 떼고 견준다 (백엔드도 출처로 고쳐 견준다)
while IFS= read -r nginx_env_origin; do
nginx_env_origin="$(printf '%s' "$nginx_env_origin" | tr -d ' ')"
nginx_env_origin="${nginx_env_origin%/}"
nginx_env_portal_host="$(nginx_env_origin_host "$nginx_env_origin")" || continue
if [ "$nginx_env_portal_host" = "$nginx_env_share_host" ]; then
nginx_env_same_host=1
fi
done <<NGINX_ENV_ORIGINS
$(printf '%s\n' "${CORS_ORIGIN:-}" | tr ',' '\n')
NGINX_ENV_ORIGINS
fi
if [ -n "$nginx_env_same_host" ]; then
NGINX_SHARE_SERVER_NAME="share.invalid"
NGINX_PORTAL_SHARE="open"
echo "nginx-env.envsh: 공개 내려받기(/s/ · /api/public-links/)를 포털과 같은 호스트에서 연다 (SHARE_PUBLIC_URL: \"${nginx_env_share_url}\")"
else
NGINX_SHARE_SERVER_NAME="$nginx_env_share_host"
NGINX_PORTAL_SHARE="closed"
echo "nginx-env.envsh: 공개 내려받기 호스트 ${nginx_env_share_host} — 포털에서는 /s/ · /api/public-links/ 를 닫는다"
fi
export NGINX_MAIL_ATTACHMENT_BODY_LIMIT NGINX_UPLOAD_BODY_LIMIT NGINX_BACKEND_UPSTREAM NGINX_LOCAL_RESOLVERS \
NGINX_SHARE_SERVER_NAME NGINX_PORTAL_SHARE