# ========================================== # clamd — 대용량 첨부(11단계)의 악성 검사기 · docker-compose.yml 의 clamav 가 읽는다 # (계약 docs/mail/api-contract.md 4절 "악성 검사", mail/README.md "악성 검사 (ClamAV)") # # 백엔드가 다 올린 파일을 TCP 3310 의 INSTREAM 으로 흘려보내고 판정 한 줄(\0 으로 끝)을 받는다(backend modules/file-storage/clamav-client.ts). # 이 컨테이너는 백엔드와만 닿는 internal 네트워크(_clamav)에만 붙는다 — 바깥 길이 없다. # 서명은 clamav-updater(freshclam)가 받아 둔 볼륨을 읽기 전용으로 읽는다. # # 판정 — 2026-10-30 ClamAV 1.4.6 · 이 설정으로 확인 (mail/README.md "악성 검사 (ClamAV)" 의 표): # stream: OK 깨끗함 — 2,047MB 짜리도 끝까지(코어 하나로 5분 안팎) # stream: Eicar-Test-Signature FOUND 위험(EICAR — 압축 안에 있어도 같다). 그 밖의 서명 이름도 같은 꼴 # stream: Heuristics.Encrypted.Zip FOUND 검사하지 못함 — 암호 zip(ZipCrypto · AES). 7z 는 .7Zip, 사용자 암호 pdf 는 .PDF # stream: Heuristics.Limits.Exceeded.<한도> FOUND 검사하지 못함 — MaxFileSize · MaxRecursion · MaxFiles · MaxScanTime 등 아래 한도 # INSTREAM size limit exceeded. ERROR StreamMaxLength 를 넘는 스트림 — 답한 뒤 연결을 닫는다(FIN, 계속 보내면 RST) # 같은 압축 안에 진짜 서명과 휴리스틱(암호 · 한도)이 함께 있으면 진짜 서명이 나온다(HeuristicScanPrecedence 기본값 끔) # # 이미지의 기본 설정을 이 파일로 통째로 바꾼다. compose 가 처음부터 비-root(이미지의 clamav 사용자 100:101)로 띄우므로 # 이 파일은 그 사용자가 읽을 수 있어야 한다(644). 바꾼 뒤에는 docker compose … up -d --force-recreate clamav # CI(infra 잡)가 clamconf 로 이 파일을 읽고 아래 한도 · 경보 값이 그대로인지 본다 # ========================================== DatabaseDirectory /var/lib/clamav # INSTREAM 으로 받은 내용 · 압축을 푼 내용을 여기에 잠시 쓰고 검사가 끝나면 지운다 — 컨테이너 쓰기 계층(도커 data-root 의 디스크)이다. # 한 번에 검사하는 수(백엔드가 2개) × (파일 2GB + 푼 내용 MaxScanSize 까지)만큼 든다. tmpfs 로 바꾸지 않는다 — 메모리 한도에 잡힌다 TemporaryDirectory /tmp # 로그는 표준 출력으로만(docker logs) — 파일에 남기지 않는다. 깨끗한 파일은 남기지 않고(LogClean 기본값 끔), # 판정은 "instream(<백엔드 주소>@<포트>): <이름> FOUND" — 파일 이름은 남지 않는다 Foreground yes LogTime yes # 받는 곳 — 컨테이너의 3310. 닿는 것은 백엔드뿐이다(internal 네트워크 · 호스트 포트 없음). 유닉스 소켓은 쓰지 않는다 TCPSocket 3310 # ------------------------------------------ # 한도 — 한 파일 한도(MAIL_LARGE_FILE_MAX_SIZE_MB, 최대 2,047MB = 2,146,435,072바이트)까지 끝까지 검사한다 # ------------------------------------------ # INSTREAM 으로 받는 양 — 이 값까지는 받는다(같으면 받는다 · 1바이트 넘으면 "INSTREAM size limit exceeded. ERROR"). # 한 파일 한도의 최대값과 같다 — 백엔드는 이보다 큰 파일을 만들지 않는다(env 는 2,047 을 넘지 못한다). 넘으면 설정 오류다 StreamMaxLength 2047M # 한 파일(밖의 파일 · 압축 안의 파일)의 크기 — 넘는 것은 검사하지 않는다. ClamAV 는 2GiB − 1 바이트 넘게는 검사하지 못한다(1.4.6 은 그 위를 2,147,483,645 로 줄인다) MaxFileSize 2047M # 파일 하나에서 검사하는 양의 합 — 압축 · 문서 안을 풀어 검사하는 양까지 센다. 한 파일 한도의 두 배 — 풀면 커지는 압축도 대부분 끝까지 본다 MaxScanSize 4094M # 압축(ZIP) 하나를 검사하는 시간 — 1.4 에서는 ZIP 에만 걸린다(풀면 수 GB 인 zip 은 이 시간에 "Heuristics.Limits.Exceeded.MaxScanTime"). # 백엔드가 다 보낸 뒤 판정을 기다리는 시간(30분 + 보낸 1MB 마다 1초 — 2,047MB 면 약 64분, backend file-storage.constants.ts 의 clamavVerdictTimeoutMs)보다 # 짧게 둔다 — 넘으면 백엔드가 끊고 미뤄서 처음부터 다시 검사한다. zip 이 아닌 압축(7z · tar.gz 등)에는 시간 상한이 없다(MaxScanSize 까지 푼다 — # 4,094MB 를 코어 하나로 10분 안팎, 백엔드의 30분 몫이 이것을 덮는다) MaxScanTime 300000 # MaxRecursion(압축 겹 17) · MaxFiles(압축 안 파일 10,000) 등 나머지 한도는 기본값 # 한도를 넘거나(Heuristics.Limits.Exceeded.*) 암호가 걸려(Heuristics.Encrypted.* — zip · 7z · rar · pdf) 끝까지 보지 못한 파일을 판정으로 알린다. # ⚠️ 끄면 그런 파일이 "깨끗함(stream: OK)"으로 온다(확인함) — 백엔드는 이 두 이름을 "검사하지 못함"으로 가른다(받을 수는 있다) AlertExceedsMax yes AlertEncrypted yes # ------------------------------------------ # 메모리 — 서명을 읽으면 1GB 안팎(2026-10 서명 363만 개). 서명을 다시 읽을 때(아래 SelfCheck) 두 벌을 들지 않는다 — # 옛 서명을 버리고 새로 읽는 30초 안팎 동안 검사 · PING 이 기다린다(백엔드의 검사 차례는 그동안 쌓였다가 이어진다). # 켜 두면(기본값) 새 서명을 다 읽을 때까지 두 벌을 든다(ClamAV 문서 — 2GB 남짓, 한도 2g 를 넘는다). 메모리 예산은 mail/README.md "메모리 예산" # ------------------------------------------ ConcurrentDatabaseReload no # 서명이 바뀌었는지 10분마다 본다 — 갱신(clamav-updater)이 알려 주지 않으므로(네트워크로 잇지 않는다) 이것이 새 서명을 읽는 길이다 SelfCheck 600 # libclamav 가 메모리를 얻지 못하면 끝낸다 — 재시작 정책으로 다시 뜬다(어정쩡한 상태로 남지 않게) ExitOnOOM yes