本文へ移動
購読
GUIDE#security更新 2026.10.02

ClamAVの使い方:Ubuntuサーバーで常駐スキャンと定期検査を動かす

Ubuntu 26.04でClamAV 1.5.4を動かし、clamdのメモリとclamscan/clamdscanの速度を実測。25MB超が素通りする既定設定の直し方、systemdタイマーでの定期スキャンと通知も。

level
中級
time
約45分
stack
ClamAV 1.5.4 / Ubuntu 26.04 LTS / systemd 259
author
@ryoma_t
// 前提条件
Ubuntu Serverのsudo権限 / clamdを常駐させる場合は空きメモリ1.5GB以上(既定設定なら2.5GB以上) / systemdのユニットファイルの基礎
Ubuntu 26.04のターミナルでClamAV 1.5.4を確認し、常駐するclamdのメモリ使用量を表示したうえで、clamdscanがEICARテストファイルをEicar-Test-Signatureとして検出した画面
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

ClamAVとは、Ciscoが開発しているオープンソース(GPLv2)のウイルス対策エンジンで、ファイルの中身を既知のマルウェアのシグネチャ(ハッシュやバイト列のパターン)と照合して検出します。プロセスのふるまいを見張って侵入後の動きを追うEDRではなく、「そこに置かれたファイルが既知の悪性ファイルかどうか」を調べる道具です。

このガイドは、Ubuntu 26.04 LTSのサーバーにClamAVを入れて、常駐デーモンのclamdと夜間の定期スキャンを運用に乗せたい情シス・インフラ担当者向けです。検証用コンテナで実測したclamdのメモリ使用量、clamscanとclamdscanの所要時間、実際に出たエラーとその直し方、systemdタイマーによるスキャンと通知の仕組みをまとめ、最後にセキュリティチェックシートの「ウイルス対策」をClamAVで答えてよい場面とEDRが必要な場面を表で整理します。

この記事でわかること

  • clamdが実際に使うメモリ(常駐で約1GB、シグネチャ再読み込みの瞬間に約2GB)と、1GBの環境で落ちる理由
  • clamscanとclamdscanを同じファイル群で測った所要時間と、使い分けの基準
  • Ubuntuが生成するclamd.confのままだと25MBを超えるファイルが「OK」で素通りする問題と直し方
  • systemdタイマーで毎晩スキャンし、検出時とエラー時に通知を出すユニット一式
  • チェックシートや監査の設問をClamAVで満たせるか、EDRが要るかの判断表

ClamAVの構成:どのプログラムを常駐させるかで負荷が決まる

ClamAVは単体のソフトではなく、役割の違うプログラムの組み合わせです。Ubuntuでは clamav、clamav-daemon、clamav-freshclam の3パッケージ(と依存で入る clamdscan)に分かれていて、どれを常駐させるかでメモリの使い方が大きく変わります。

プログラムパッケージ動き常駐向いている使い方
clamscanclamav実行のたびにシグネチャを全部読み込んで検査し、終われば終了しないたまの手動検査、clamdを置けないホスト
clamdclamav-daemonシグネチャをメモリに保持し、UNIXソケットで検査依頼を待つする定期スキャンとリアルタイム検査の土台
clamdscanclamdscan検査対象をclamdに渡して結果を表示するだけのクライアントしない定期スキャン、手動検査
freshclamclamav-freshclamシグネチャDBを取得・更新する既定で常駐(-d)必須。更新確認だけなら軽い
clamonaccclamav-daemonfanotifyでファイルのオープンを捕まえ、clamdに検査させる設定したときだけアップロード領域などのリアルタイム検査

この表で一番効いてくるのは、clamscanは実行のたびに約360万件のシグネチャを読み込み直し、clamdは一度読み込んだものを使い回すという違いです。検証環境では、clamdのログの「Reading databases from /var/lib/clamav」から「Loaded 3628083 signatures.」までが7秒でした。この読み込みの有無が、後で出てくる速度とメモリの数字にそのまま表れます。

ClamAVをUbuntu 26.04にインストールする

パッケージはUbuntuの公式リポジトリ(main)にあります。執筆時点のUbuntu 26.04.1 LTSでは、リリース時の1.4.3ではなく、resolute-updates/resolute-securityの1.5.4が候補になります。

terminal
sudo apt update
apt-cache policy clamav
sudo apt install -y clamav clamav-daemon clamav-freshclam
apt-cache policy clamav(インストール前・先頭8行)
clamav:
  Installed: (none)
  Candidate: 1.5.4+dfsg-0ubuntu0.26.04.1
  Version table:
     1.5.4+dfsg-0ubuntu0.26.04.1 500
        500 http://archive.ubuntu.com/ubuntu resolute-updates/main amd64 Packages
        500 http://security.ubuntu.com/ubuntu resolute-security/main amd64 Packages
     1.4.3+dfsg-2ubuntu3 500

検証した構成は次のとおりです(すべて執筆時点)。なお、Ubuntu 24.04でもnoble-updatesに同じ1.5.4系(1.5.4+dfsg-0ubuntu0.24.04.1)が来ていることを apt-cache policy で確認しましたが、この記事の動作確認と測定はすべて26.04で行っています。

  • OS:Ubuntu 26.04.1 LTS(Dockerの ubuntu:26.04 イメージ、CPU 2コア・メモリ上限2500MBのコンテナ)
  • ClamAV:1.5.4(パッケージ版 1.5.4+dfsg-0ubuntu0.26.04.1)、systemd 259.5
  • シグネチャ:daily 28134、main 63、bytecode 339(clamdが読み込んだ件数は3,628,083)

インストールが済んだら、先に更新サービスを有効にしてシグネチャを取りに行かせます。clamdはDBがないと起動できないので、clamdの有効化は取得が終わってからにします。

terminal
sudo systemctl enable --now clamav-freshclam
sudo tail -f /var/log/clamav/freshclam.log   # main と daily の取得が終わったら Ctrl+C

取得先は /var/lib/clamav です。検証時のDBは合計108MBで、中身の大半はmain.cvd(85MB)とdaily.cvd(23MB)でした。1.5.4では各CVDの横に署名ファイル(*.cvd.sign)が並びます。

terminal
ls -lh /var/lib/clamav
出力
total 108M
-rw-r--r-- 1 clamav clamav 8.9K Sep 25 11:17 bytecode-339.cvd.sign
-rw-r--r-- 1 clamav clamav 276K Sep 25 11:17 bytecode.cvd
-rw-r--r-- 1 clamav clamav 8.9K Sep 25 11:17 daily-28134.cvd.sign
-rw-r--r-- 1 clamav clamav  23M Sep 25 11:42 daily.cvd
-rw-r--r-- 1 clamav clamav   90 Sep 25 11:17 freshclam.dat
-rw-r--r-- 1 clamav clamav 8.9K Sep 25 11:17 main-63.cvd.sign
-rw-r--r-- 1 clamav clamav  85M Sep 25 11:17 main.cvd

サービスを止めた状態で sudo freshclam を手動実行すると、DBが最新なら次のように3つとも「up-to-date」で終わります。このときfreshclamの最大RSSは19,336KBで、更新確認だけなら常駐させても負担になりません。サービスが動いている間に手動実行するとロックのエラーになる点は、つまずきポイントで扱います。

sudo freshclam(DBが最新のとき)
Fri Sep 25 11:32:52 2026 -> ClamAV update process started at Fri Sep 25 11:32:52 2026
Fri Sep 25 11:32:53 2026 -> daily.cvd database is up-to-date (version: 28134, sigs: 355678, f-level: 90, builder: svc.clamav-publisher)
Fri Sep 25 11:32:53 2026 -> main.cvd database is up-to-date (version: 63, sigs: 3287027, f-level: 90, builder: tomjudge)
Fri Sep 25 11:32:53 2026 -> bytecode.cvd database is up-to-date (version: 339, sigs: 80, f-level: 90, builder: nrandolp)

freshclamは、まず current.cvd.clamav.net のDNS TXTレコードで最新の版数を確かめ、必要なときだけ database.clamav.net からHTTPSで取得します(freshclam.confの DNSDatabaseInfo と DatabaseMirror)。外向きのDNSや443番を閉じているサーバーでは、ネットワークを切ったコンテナで再現した次の出力になり、5秒後に再試行します。プロキシ経由にするなら、freshclam.confに HTTPProxyServer と HTTPProxyPort を書きます。

外部に出られないときのfreshclam
WARNING: Fri Sep 25 11:42:56 2026 -> Can't query current.cvd.clamav.net
WARNING: Fri Sep 25 11:42:56 2026 -> Invalid DNS reply. Falling back to HTTP mode.
Fri Sep 25 11:42:56 2026 -> Trying to retrieve CVD header from https://database.clamav.net/daily.cvd
WARNING: Fri Sep 25 11:42:56 2026 -> remote_cvdhead: Download failed (6) WARNING: Fri Sep 25 11:42:56 2026 ->  Message: Could not resolve hostname
ERROR: Fri Sep 25 11:42:56 2026 -> check_for_new_database_version: Failed to find daily database using server https://database.clamav.net.
Fri Sep 25 11:42:56 2026 -> Trying again in 5 secs...

freshclam.confの既定は Checks 24(1時間に1回の確認)です。取得に失敗したからといってcronで数分おきにfreshclamを回すと、配布側で一時的に取得を制限されることがあります。制限の条件は変わりうるので公式サイトで確認してください(執筆時点)。サーバーが何台もあるなら、1台で取得したDBを社内から配る構成(freshclam.confの PrivateMirror)にすると、外への問い合わせが1台分で済みます。

mainとdailyがそろったら、clamdを有効にします。

terminal
sudo systemctl enable --now clamav-daemon
systemctl is-enabled clamav-freshclam clamav-daemon clamav-daemon.socket

パッケージの clamav-daemon.service には ConditionPathExistsGlob=/var/lib/clamav/main.{c[vl]d,inc} と、dailyについて同じ条件が付いています。DBの取得が終わる前に起動しようとすると、失敗ではなく「条件を満たさないので起動を見送る」扱いになり、clamdscanは後述の「Could not connect」になります。順番を逆にしてしまったときは、DBがそろってから sudo systemctl start clamav-daemon で起動し直します。DBのないディレクトリを指定してclamdを直接起動すると、次の2行を出して終了しました。

DBが空のままclamdを起動したとき
LibClamAV Error: cli_loaddbdir: No supported database files found in /tmp/emptydb
ERROR: Fri Sep 25 11:43:42 2026 -> Can't open file or directory

検証環境のコンテナではsystemdがPID 1として動いていないため、パッケージのユニットファイルを読んだうえで、そこに書かれたコマンド(clamd --foreground=true と freshclam -d --foreground=true)を直接起動して確かめています。この記事の systemctl enable・status・list-timers の出力と、タイマーが実際に発火する様子は観測していません(執筆時点)。本番に入れたら、後述の動作確認で必ず自分の環境の出力を見てください。

clamdのメモリ使用量:常駐で約1GB、再読み込みの瞬間は約2GB

clamdを常駐させられるかは、まずメモリで決まります。常駐中のclamdの実メモリ(RSS)と、プロセス開始以降の最大値(VmHWM)は次のコマンドで見られます。

terminal
ps -o pid,user,rss,cmd -C clamd
grep -E 'VmRSS|VmHWM' /proc/$(pidof clamd)/status
起動直後の出力(単位はkB)
    PID USER       RSS CMD
     47 clamav   991096 clamd --foreground=true

状況を変えて測った結果が次の表です。再読み込みはclamdに SIGUSR2 を送る方法(パッケージのユニットの ExecReload と同じ)と、DBファイルの更新をSelfCheckに検知させる方法の両方で起こしました。

状況測定値補足
起動直後VmRSS 988,748 kB起動から clamdscan --ping がPONGを返すまで9.5秒
/usr を一通り検査した後VmRSS 1,071,840 kB(VmHWM 1,178,876 kB)検査中の作業領域とキャッシュの分だけ増える
再読み込み中(既定の ConcurrentDatabaseReload yes)VmHWM 2,102,956 kB読み込み中に投げた検査は0.02秒で返った
再読み込み中(ConcurrentDatabaseReload no)VmHWM 1,059,252 kB読み込み中に投げた検査は9.71秒待たされた
メモリ上限1GiBのコンテナ起動はできた(VmRSS 988,712 kB)再読み込みでOOM Killerに終了させられた。ConcurrentDatabaseReload no でも同じ

再読み込みでメモリが倍になるのは、既定の ConcurrentDatabaseReload yes が「新しいエンジンを裏で丸ごと組み立て、切り替えてから古いほうを捨てる」動きだからです。検査を止めない代わりに、一瞬だけシグネチャ2セット分を抱えます。no にすると倍にはなりませんが、読み込みの約10秒間は検査が待たされます。再読み込みはdailyが更新されるたびに起きるので、「起動できたから大丈夫」とは判断できません。

この数字から、筆者は次の基準で決めています。判断に使うのは free -m の available 列、つまりアプリケーションが動いている状態で残っている量です。

  • available が2.5GB以上:既定のまま。再読み込み中も検査が止まらないほうが運用は楽です。
  • 1.5〜2.5GB:ConcurrentDatabaseReload no を足し、更新のたびに10秒ほど検査が待たされるのを受け入れます。リアルタイム検査を使うサーバーでは、その間ファイルのオープンも待たされる点に注意します。
  • 1.5GB未満:clamdを常駐させません。ただしclamscanも実行中は約1GBを使うので(次の章)、メモリ1GBのVPSでは公式DB一式での検査そのものが厳しく、バックアップ先など別のホストで検査する構成を考えます。

1GiB制限のコンテナでclamdが落ちたとき、clamav.log の最終行は「Reading databases from /var/lib/clamav」のままで、エラー行は残りませんでした。OOM Killerに殺されたプロセスは自分でログを書けないためです。「ログにエラーがないのにclamdがいない」ときは、journalctl -k や dmesg でOOMの記録を探してください。clamdscanの表示も「No such file or directory」ではなく「Connection refused」になります(ソケットファイルだけが残るため)。

clamscanとclamdscanの速度を同じファイルで比べる

対象はコンテナの /usr(find で8,853ファイル、du で249MB)です。ディスクの読み込み速度の差が混ざらないよう、先に tar でページキャッシュに載せ、各方式を2回ずつ測りました。比較をそろえるため、clamdの MaxFileSize/MaxScanSize は本家の既定(clamconfが示す100M/400M)に上げています。この環境ではclamscanのファイル上限も100MiBでした(理由と確かめ方は次の章)。方式ごとに1回目の前にclamdを再起動し、検査キャッシュを空にしました。

terminal
sudo apt install -y time
sudo tar cf - /usr | cat > /dev/null
sudo /usr/bin/time -f '%e s elapsed, %M KB maxrss' clamscan -r -i /usr
sudo /usr/bin/time -f '%e s elapsed, %M KB maxrss' clamdscan --fdpass -i /usr
sudo /usr/bin/time -f '%e s elapsed, %M KB maxrss' clamdscan -m --fdpass -i /usr
コマンド1回目2回目メモリ(最大RSS)
clamscan -r -i /usr103.00秒102.38秒1,005,576 kB
clamdscan --fdpass -i /usr93.56秒12.37秒クライアントは7,624 kB(別にclamdが約1.07GB)
clamdscan -m --fdpass -i /usr58.71秒7.58秒同上
clamscan eicar.com(1ファイル)10.57秒-990,840 kB
clamdscan --fdpass eicar.com0.01秒未満-7,588 kB

読み取れることは3つです。まず、clamscanは1ファイルでも10.57秒かかり、その大半がシグネチャの読み込みです。cronで小さな検査を何度も回す使い方ではclamscanは割に合いません。次に、clamdscanの2回目が極端に速いのは、clamdが「検査して問題なかったファイル」を記憶しているからです(clamd.confの DisableCache が既定の no)。キャッシュはファイルの中身から引くので、2回目もファイルの読み込みは発生し、0秒にはなりません。最後に、-m(MULTISCAN)はclamd側で複数スレッドを使い、2コアの環境で93.56秒が58.71秒になりました。スレッドの上限はclamd.confの MaxThreads(Ubuntuの生成値は12)です。

CPUを使っているのがどちらのプロセスかも確かめました。nice -n 19 を付けてclamdscanを動かし、実行中に ps を見た結果(抜粋)です。

terminal
sudo nice -n 19 clamdscan -m --fdpass -i --no-summary /usr/lib &
ps -o pid,ni,pcpu,rss,cmd -C clamd,clamdscan
出力(抜粋)
    PID  NI %CPU   RSS CMD
    303   0  138 1119136 clamd
    308  19  0.1  7276 clamdscan -m --fdpass -i --no-summary /usr/lib

nice値19のclamdscanは0.1%にとどまり、clamdはnice値0のまま動いていて、同じ瞬間の top では240%を示しました。重い処理はclamdの中で行われるので、スキャン用のサービスに Nice= を付けても負荷は下がりません。昼間に検査して業務への影響を抑えたいなら -m を外して1スレッドで回すのが確実です。サービス側(clamav-daemon.service)に CPUQuota= を付ける手もありますが、リアルタイム検査まで遅くなるうえ、この環境では試していません。

clamdが常駐しているサーバーでclamscanを実行すると、シグネチャをもう1セット読み込みます。この検証ではclamd(約1.07GB)とclamscan(最大1,005,576 kB)が同時に載りました。clamdがあるサーバーでは、手動の検査もclamdscanで行ってください。

Ubuntuが生成するclamd.confで見直す設定

Ubuntuのパッケージは /etc/clamav/clamd.conf をインストール時に生成します。clamconf -g clamd.conf が出力する本家の既定値と比べると、いくつかの項目が違い、特にファイルサイズの上限は検出漏れに直結します。

設定Ubuntuの生成値clamconfが示す既定この記事の推奨理由
MaxFileSize25M100M100M25MBを超えるファイルは中身を見ずにOK扱いになる
MaxScanSize100M400M400Mアーカイブを展開して調べる量の上限
EnableReloadCommandfalseyesfalseのままソケットが誰でも書ける666のため
SelfCheck36006003600か600DB更新が反映されるまでの最大秒数
ConcurrentDatabaseReload記載なし(yes)yes空きメモリ次第前の章の表を参照
VirusEvent記載なし無効通知に使う後の章で設定する

25MBを超えるファイルが「OK」で素通りする

後の章の手順で作るEICARファイルを /srv/scantest に置き、30MBの乱数ファイルと1つのzip(31,457,560バイト、無圧縮)にまとめて、既定設定のclamdとclamscanの両方で調べました。

terminal(/srv/scantest で実行)
python3 -c "import zipfile,os; z=zipfile.ZipFile('big.zip','w'); z.write('eicar.com'); z.writestr('filler.bin', os.urandom(30*1024*1024)); z.close()"
sudo clamdscan --fdpass --no-summary /srv/scantest/big.zip; echo "exit=$?"
sudo clamscan --no-summary /srv/scantest/big.zip; echo "exit=$?"
出力
/srv/scantest/big.zip: OK
exit=0
/srv/scantest/big.zip: Eicar-Test-Signature FOUND
exit=1

同じファイルなのに結果が逆になるのは、clamscanがclamd.confを読まず、自分の既定値で動くからです。既定の上限は100MiBで、検証では103,809,304バイトのzipは中まで調べ、105,906,456バイトのzipは「OK」を返しました。clamscanに --max-filesize=25M を渡すと同じく「OK」になり、さらに --alert-exceeds-max=yes を付けると Heuristics.Limits.Exceeded.MaxFileSize FOUND と「Virus(es) detected ERROR」を出して終了コード2で終わりました。つまり既定のclamdは「大きすぎて調べていない」ことを何も言わずにOKと返します。

terminal
sudo sed -i -e 's/^MaxFileSize .*/MaxFileSize 100M/' -e 's/^MaxScanSize .*/MaxScanSize 400M/' /etc/clamav/clamd.conf
grep -E '^Max(File|Scan)Size' /etc/clamav/clamd.conf
sudo systemctl restart clamav-daemon

上限を上げてclamdを再起動すると、同じzipが Eicar-Test-Signature FOUND になりました。AlertExceedsMax yes で「上限超えを検出扱いにする」方法もありますが、ISOイメージやDBのダンプのような正当な大きいファイルが毎晩すべてアラートになるので、筆者は勧めません。上限を上げたうえで、巨大なデータしか置かないディレクトリはスキャン対象から外すほうが通知の意味が保てます。なお、Ubuntuのclamavパッケージはライセンスの都合でRAR展開のコードを分けて配布しており(パッケージのcopyrightファイルに記載)、RARの中まで調べるかどうかはこの記事では検証していません。

RELOADが無効なので、更新の反映はSelfCheckに任せる

Ubuntuの生成値では EnableReloadCommand false です。clamdscan --reload は「ERROR: Clamd did not reload the database」を出して終了コード2になり、freshclamの NotifyClamd も同じRELOADコマンドを使うので、更新の通知はclamdに届きません。代わりにclamdは SelfCheck の秒数ごとにDBディレクトリを確かめ、変化があれば自分で読み込み直します。SelfCheck 20 にしてDBファイルの更新時刻を変えたところ、ログに次の行が出て再読み込みが始まりました。

/var/log/clamav/clamav.log
Fri Sep 25 11:42:22 2026 -> SelfCheck: Database modification detected. Forcing reload.
Fri Sep 25 11:42:23 2026 -> Reading databases from /var/lib/clamav
Fri Sep 25 11:42:32 2026 -> Database correctly reloaded (3628083 signatures)
Fri Sep 25 11:42:32 2026 -> Database reload completed.

ソケット /run/clamav/clamd.ctl は srw-rw-rw-(clamd.confの LocalSocketMode 666)で、サーバー上のどのユーザーも書き込めます。RELOADを有効にすると、誰でもメモリが倍になる再読み込みを起こせてしまうので、無効のままにしておくのが妥当です。新しいシグネチャの反映が最大1時間遅れるのが気になるなら、SelfCheck 600(本家の既定)に縮めます。10分ごとにDBファイルの状態を見るだけなので、負荷はほとんど増えません。

EICARテストファイルで検出を確かめる

EICARテストファイルは、ウイルス対策製品が「検出すべきテスト用の文字列」として扱う68バイトのテキストで、害はありません。ただし、この文字列をそのまま記事やスクリプトに書くと、ページや保存したファイル自体がセキュリティ製品に検出されます。そこでこのガイドでは文字列を2つに分けて書き、printf '%s%s' で連結してファイルにしています。シングルクォートで囲むのは、文字列に含まれる $、!、\ をシェルに解釈させないためです。

terminal
mkdir -p ~/scan-test && cd ~/scan-test
printf '%s%s' 'X5O!P%@AP[4\PZX54(P^)7CC)7}' '$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > eicar.com
wc -c eicar.com
md5sum eicar.com
clamscan --version
clamdscan --fdpass eicar.com; echo "exit=$?"
出力
68 eicar.com
44d88612fea8a8f36de82e1278abb02f  eicar.com
ClamAV 1.5.4/28134/Fri Sep 25 06:25:58 2026
/home/ryoma/scan-test/eicar.com: Eicar-Test-Signature FOUND

----------- SCAN SUMMARY -----------
Infected files: 1
Time: 0.002 sec (0 m 0 s)
Start Date: 2026:09:25 12:05:10
End Date:   2026:09:25 12:05:10
exit=1

clamscan --version の「1.5.4/28134/…」はエンジンの版とdailyの版数・日付で、DBが更新されているかの確認にも使えます。一方 clamdscan --version はUbuntuの生成値(EnableVersionCommand false)のため「WARNING: VERSION command disabled in clamd」と出てエンジンの版しか返しません。終了コードはclamscanとclamdscanで共通で、0が検出なし、1が検出あり、2がエラーです。次の章のタイマーはこの3値の違いを使います。

ホームディレクトリは、Ubuntu 26.04の useradd -m では750で作られます(/etc/login.defs の HOME_MODE 0750)。clamdは clamav ユーザーで動いているので、--fdpass を付けずにホーム配下を指定すると、中に入れず「File path check failure: Permission denied. ERROR」になります。--fdpass はclamdscan側でファイルを開き、そのファイル記述子をUNIXソケット越しにclamdへ渡す指定で、手動でも定期スキャンでも付けておくのが無難です。

systemdタイマーで毎晩スキャンし、検出したら通知する

定期スキャンは、スクリプト2本とユニット3つで組みます。流れは「タイマーがスキャン用サービスを起動 → clamdscanが1か2で終わるとサービスが失敗扱い → OnFailure= で通知用サービスが起動」です。終了コードをそのままサービスの結果にするので、検出(1)とエラー(2)のどちらでも通知が出て、黙っているのは0のときだけになります。「clamdが止まっていてスキャンできなかった」を「異常なし」と取り違えないための作りです。

/usr/local/sbin/clamav-scheduled-scan
#!/bin/sh
# clamdscan の終了コード: 0=検出なし 1=検出あり 2=エラー。そのまま systemd に返す
set -u
LOG=/var/log/clamav/scheduled-scan.log
TARGETS=""
for d in /home /srv /var/www /tmp /var/tmp; do
  [ -d "$d" ] && TARGETS="$TARGETS $d"
done
echo "=== $(date '+%F %T') start:$TARGETS" >> "$LOG"
# clamd が応答しないまま「異常なし」で終わらせない
if ! clamdscan --ping 30:2 >/dev/null 2>&1; then
  echo "ERROR: clamd is not responding" | tee -a "$LOG" >&2
  exit 2
fi
clamdscan --multiscan --fdpass --infected --log="$LOG" $TARGETS
  • 対象を存在確認してから渡すのは、ないディレクトリを1つでも渡すと「ERROR: Can't access file /var/www」で終了コード2になり、毎晩誤報が出るためです。
  • --ping 30:2 は2秒おきに最大30回clamdの応答を待ちます。再起動直後のようにclamdがシグネチャを読み込んでいる最中(検証では約10秒)に始まっても、応答を待ってから検査に入ります。clamdを止めて試すと、58秒待ってから終了コード2で終わりました。ソケット起動(clamav-daemon.socket)で立ち上がる場合の待ち方は、この環境では試していません。
  • --multiscan は夜間に速く終わらせるためです。昼間に回すなら外します(前の章の数字を参照)。
  • --infected で検出分だけをログに残し、--log は追記なので、実行ごとの区切りとして「=== 日時」の行を先に書いています。
  • $TARGETS をあえてクォートしていないのは、空白区切りで複数の引数に分けるためです。空白を含むパスは対象にしない前提です。
/usr/local/sbin/clamav-alert
#!/bin/sh
# 使い方: clamav-alert <失敗したユニット名>   DRY_RUN=1 なら送らずに表示だけ
set -u
UNIT="${1:-clamav-scan.service}"
TO="${ALERT_TO:[email protected]}"
LOG=/var/log/clamav/scheduled-scan.log
case "${MONITOR_EXIT_STATUS:-}" in
  1) KIND="VIRUS FOUND" ;;
  2) KIND="SCAN ERROR" ;;
  *) KIND="FAILED (${MONITOR_SERVICE_RESULT:-unknown})" ;;
esac
SUBJECT="[ClamAV] $(hostname): $KIND - $UNIT"
# ログから最後の実行分("=== " 行以降)だけを本文にする
BODY=$(awk '/^=== /{b=""} {b=b $0 "\n"} END{printf "%s", b}' "$LOG" 2>/dev/null)
if [ "${DRY_RUN:-0}" = 1 ]; then
  printf 'To: %s\nSubject: %s\n\n%s\n' "$TO" "$SUBJECT" "$BODY"
  exit 0
fi
logger -t clamav-alert -p user.alert "$SUBJECT" || true
printf 'To: %s\nSubject: %s\n\n%s\n' "$TO" "$SUBJECT" "$BODY" | /usr/sbin/sendmail -t

OnFailure= から起動されたユニットには、失敗したユニットの結果が MONITOR_EXIT_STATUS や MONITOR_SERVICE_RESULT といった環境変数で渡されます(systemd.execの仕様。この環境ではタイマー経由の発火は観測していません)。件名を英語にしているのは、日本語の件名はMIMEエンコードしないと受信側によって文字化けするためです。変数が空でも「FAILED (unknown)」として必ず送られるようにしています。

/etc/systemd/system/clamav-scan.service
[Unit]
Description=ClamAV scheduled scan
Wants=clamav-daemon.socket
After=clamav-daemon.socket clamav-daemon.service
OnFailure=clamav-alert@%n.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/clamav-scheduled-scan
TimeoutStartSec=4h
/etc/systemd/system/clamav-scan.timer
[Unit]
Description=Run ClamAV scheduled scan every night

[Timer]
OnCalendar=*-*-* 03:30:00 Asia/Tokyo
RandomizedDelaySec=20m
Persistent=true

[Install]
WantedBy=timers.target
/etc/systemd/system/[email protected]
[Unit]
Description=ClamAV alert for %i

[Service]
Type=oneshot
[email protected]
ExecStart=/usr/local/sbin/clamav-alert %i

スクリプトとユニットの ALERT_TO に入っている [email protected] は検証用の宛先です。自分の通知先に置き換えてください。タイマーの OnCalendar に Asia/Tokyo を付けているのは、クラウドのサーバーやコンテナがUTCで動いていることが多く、時刻だけを書くと日本時間の12時30分に業務時間中のスキャンが走るためです。RandomizedDelaySec=20m は複数台が同じ瞬間に共有ストレージを読みに行かないようにずらす指定、Persistent=true は停止中に過ぎた回を次の起動後に実行する指定です。スキャンのサービスに Nice= を書いていないのは、前の章で見たとおり効かないからです。ユニットの書式と時刻の解釈は、systemdがPID 1でなくても確かめられます。

terminal
cd /etc/systemd/system
systemd-analyze verify clamav-scan.service clamav-scan.timer [email protected]; echo "exit=$?"
systemd-analyze calendar --iterations=2 '*-*-* 03:30:00 Asia/Tokyo'
出力(コンテナはUTC)
exit=0
Normalized form: *-*-* 03:30:00 Asia/Tokyo
    Next elapse: Fri 2026-09-25 18:30:00 UTC
       From now: 6h left
   Iteration #2: Sat 2026-09-26 18:30:00 UTC
       From now: 1 day 6h left

verify は問題がなければ何も出力しません。calendar はUTCの18時30分、つまり日本時間の3時30分を次回として示しています。確認できたら権限を整えて有効にします。

terminal
sudo chmod 755 /usr/local/sbin/clamav-scheduled-scan /usr/local/sbin/clamav-alert
sudo systemctl daemon-reload
sudo systemctl enable --now clamav-scan.timer
systemctl list-timers clamav-scan.timer

スクリプトは手で実行して、3通りの結果を確かめました。空の状態、/srv/upload にEICARファイルを置いた状態、clamdを止めた状態です。検出時の通知は DRY_RUN=1 で送信せずに内容だけを表示しています。

terminal
sudo /usr/local/sbin/clamav-scheduled-scan; echo "exit=$?"
sudo DRY_RUN=1 MONITOR_EXIT_STATUS=1 /usr/local/sbin/clamav-alert clamav-scan.service
EICARを置いた状態での出力
--------------------------------------
/srv/upload/eicar.com: Eicar-Test-Signature FOUND

----------- SCAN SUMMARY -----------
Infected files: 1
Time: 0.051 sec (0 m 0 s)
Start Date: 2026:09:25 12:05:11
End Date:   2026:09:25 12:05:11
exit=1
To: [email protected]
Subject: [ClamAV] nagoyadevlab01: VIRUS FOUND - clamav-scan.service

=== 2026-09-25 12:05:11 start: /home /srv /tmp /var/tmp
--------------------------------------
/srv/upload/eicar.com: Eicar-Test-Signature FOUND

----------- SCAN SUMMARY -----------
Infected files: 1
Time: 0.051 sec (0 m 0 s)
Start Date: 2026:09:25 12:05:11
End Date:   2026:09:25 12:05:11

何も置いていない状態では「Infected files: 0」で終了コード0、clamdを止めた状態では「ERROR: clamd is not responding」で終了コード2になり、通知の件名は「SCAN ERROR」になりました。/var/www はこのサーバーにないので、対象から自動的に外れています。

送信にはサーバー上で /usr/sbin/sendmail を提供するMTA(社内のメールリレーに向けたpostfixやmsmtp-mtaなど)が必要です。MTAのない検証コンテナで DRY_RUN を外して実行すると、「/usr/local/sbin/clamav-alert: 20: /usr/sbin/sendmail: not found」で終了コード127になりました。この状態では通知用サービス自体が失敗し、誰にも知らせが届きません。まず DRY_RUN=1 で内容を確かめ、MTAを設定してから本物のテスト送信を1回行ってください。TeamsやSlackのWebhookに送りたい場合は最後の行を置き換えますが、受け口の仕様はサービスごとに違い、変わることもあるため、利用中のサービスの説明で確認してください(執筆時点)。

clamdの検出をその場で知るVirusEvent

タイマーは夜にまとめて知らせる仕組みですが、clamdには検出のたびにコマンドを実行する VirusEvent もあります。この検証では、clamdscanで検査したファイルをclamdが検出した時点で呼ばれました。リアルタイム検査の判定もclamdが行うので同じ経路を通るはずですが、その組み合わせは試していません。1.5.4ではファイル名を引数に埋め込む %f がコマンドインジェクション対策で無効になっていて、ファイル名とシグネチャ名は環境変数で受け取ります。

/usr/local/sbin/clamav-virusevent
#!/bin/sh
# clamd の VirusEvent から clamav ユーザー権限で呼ばれる。ファイル名とシグネチャ名は環境変数で受け取る
printf '%s %s %s\n' "$(date '+%F %T')" "$CLAM_VIRUSEVENT_VIRUSNAME" "$CLAM_VIRUSEVENT_FILENAME" >> /var/log/clamav/virusevent.log
terminal
sudo chmod 755 /usr/local/sbin/clamav-virusevent
echo 'VirusEvent /usr/local/sbin/clamav-virusevent' | sudo tee -a /etc/clamav/clamd.conf
sudo systemctl restart clamav-daemon

EICARファイルを検査すると、/var/log/clamav/virusevent.log に「2026-09-25 11:50:05 Eicar-Test-Signature /srv/upload/eicar.com」の1行が追記されました。スクリプトはclamdと同じ clamav ユーザーで動くので、書き込み先は clamav が書ける場所にします。ここからメールを送る場合も、環境変数の値をコマンドラインに直接展開せず、スクリプトの中で引用符付きで扱ってください。

ClamAVのリアルタイムスキャン(clamonacc)は対象を絞る

リアルタイムスキャンは、clamonaccがLinuxのfanotifyでファイルのオープンを捕まえ、clamdに検査させてから開くのを許す仕組みです。OnAccessPrevention yes のときは、検出したファイルのオープンをその場で拒否します。clamonaccはfanotifyを使うためroot権限が必要で、パッケージの clamav-clamonacc.service も User=root で動き、--move=/root/quarantine で検出ファイルを隔離する設定になっています。検証環境では、インストールしただけではこのサービスの有効化リンクは作られていませんでした。

/etc/clamav/clamd.conf に追記
OnAccessIncludePath /srv/upload
OnAccessPrevention yes
OnAccessExcludeUname clamav

OnAccessExcludeUname clamav は、clamd自身がファイルを読むときのオープンを検査対象から外す設定です。これがないと、clamdの読み込みがまたclamonaccに捕まります。検証は、--privileged を使わず --cap-add SYS_ADMIN だけを与えたコンテナで、監視先をtmpfsにして行いました。再起動したclamdに対してclamonaccを起動し、一般ユーザー(検証ではryoma)でEICARファイルを作って読むと、次のように拒否されました。

ryomaで cat した結果と /var/log/clamav/clamonacc.log
cat: /srv/upload/eicar.com: Operation not permitted

ClamInotif: watching '/srv/upload' (and all sub-directories)
/srv/upload/eicar.com: Eicar-Test-Signature FOUND

拒否されるのは「開くこと」で、ファイル自体は68バイトのまま残っていました。消したり隔離したりするには、パッケージのユニットのように --move を付けます。監視の対象は、外部から持ち込まれるファイルが置かれる場所(Webのアップロード先、Sambaの共有、SFTPの受け取りディレクトリ)に絞るのが筆者の推奨です。ファイルを開くたびに検査が挟まるので、DBのデータディレクトリやログのように頻繁に開かれる場所を含めると、アプリケーション全体が遅くなります。また OnAccessMaxFileSize の既定は5Mで、それより大きいファイルはリアルタイム検査の対象外です。実運用でのオーバーヘッドとsystemd経由の起動は、この環境では測っていません。

セキュリティチェックシートの「ウイルス対策」:ClamAVで足りる場合とEDRが要る場合

取引先から届くセキュリティチェックシートや社内監査で、Linuxサーバーのウイルス対策を問われる場面は多くあります。「はい」と答えてよいかは、設問が何を求めているかで決まります。次の表の設問はよくある書き方の例で、実際の文言は相手ごとに違います。

設問・要件の例ClamAVで答えられるか根拠として出せるもの/足りない理由
例:サーバーにウイルス対策ソフトを導入しているか答えられるパッケージの版(dpkg -l clamav-daemon)、clamdの稼働状況
例:定義ファイルを自動で更新しているか答えられるfreshclam.log の更新記録、clamscan --version のDB版数と日付
例:定期的にスキャンしているか対象範囲を書けば答えられるタイマーの設定と scheduled-scan.log。全領域を毎晩は時間的に難しいので、対象ディレクトリを明記する
例:アップロードされたファイルや共有フォルダを検査しているか答えられるclamonaccの設定、またはアップロード処理からclamdscanを呼ぶ仕組み
例:不審なふるまいを検知し、侵入後の経緯(どのプロセスが何をしたか)を調べられるか答えられないClamAVはファイルの中身しか見ない。プロセスの記録と追跡はEDRの領域
例:検知したサーバーを遠隔でネットワークから隔離できるか答えられない隔離の機能を持たない
例:全台の状態を管理画面で一覧し、24時間監視しているか自前の仕組みがあれば一部中央管理画面はない。ログをSIEMに集めて監視するか、EDR/MDRを使う

判断の基準はシンプルです。設問が「導入・定義の更新・定期検査」までなら、ClamAVと上の証跡で答えられます。設問に「ふるまい」「EDR」「隔離」「調査」「24時間監視」の語があれば、ClamAVでは「はい」と書けません。回答欄には「シグネチャ型のClamAVを導入し、定義は1時間ごとに確認、毎晩3時30分に指定ディレクトリを検査」のように方式と範囲を書き、EDR相当だと誤読されないようにしてください。相手が製品名や第三者評価の有無を指定している場合や、対象にWindows端末が含まれる場合もClamAVでは答えられません。EDRの仕組みと選び方はEDRとは?アンチウイルスとの違い・XDRとの関係、ログの集約はSIEMとは?ログ監視の仕組みと小さく始める方法にまとめています。

稟議で説明するときは、ライセンス費用がかからない代わりに、サーバー1台あたり2GB前後のメモリと、通知を受けて判断する人の手間が要ることを書いておくと、後から「無料と聞いていた」と言われずに済みます。OSの脆弱性やSSHの設定の甘さはClamAVでは防げないので、Linuxサーバーの基本ハードニングと組み合わせて初めて意味を持ちます。

つまずきポイント

freshclamを手で実行すると「Failed to lock the log file」

更新サービス(freshclam -d)が動いている横で sudo freshclam を実行すると、ログファイルのロックが取れずに終了コード2で止まります。

sudo freshclam(サービス稼働中)
ERROR: Problem with internal logger (UpdateLogFile = /var/log/clamav/freshclam.log).
ERROR: initialize: libfreshclam init failed.
ERROR: Initialization error!
ERROR: Failed to lock the log file /var/log/clamav/freshclam.log: Resource temporarily unavailable

今すぐ更新したいなら、sudo systemctl stop clamav-freshclam で止めてから sudo freshclam を実行し、終わったら sudo systemctl start clamav-freshclam で戻します。検証でも、常駐側を止めた直後の手動実行はロックのエラーを出さずに更新確認へ進みました。なお sudo を付け忘れると、同じ3行のあとに「Failed to open log file /var/log/clamav/freshclam.log: Permission denied」が出ます。

clamdscanが「Could not connect to clamd」

clamdscan(clamdが止まっているとき)
ERROR: Could not connect to clamd on LocalSocket /var/run/clamav/clamd.ctl: No such file or directory

末尾が「No such file or directory」ならソケットがなく、clamdが起動していません。インストール直後なら、まずDBの取得が終わっていない(ユニットの起動条件を満たさない)可能性を疑い、ls /var/lib/clamav でmainとdailyがあるかを見てから sudo systemctl start clamav-daemon を実行します。末尾が「Connection refused」なら、ソケットは残っているのにclamdが死んでいます。検証ではメモリ不足でOOM Killerに終了させられたときにこの表示になりました。メモリの章の基準に照らして、ConcurrentDatabaseReload no にするか、常駐をやめる判断をします。

「File path check failure: Permission denied. ERROR」

clamdの実行ユーザー clamav が入れないディレクトリを、--fdpass なしで指定したときの表示です。rootでclamdscanを実行しても、実際にファイルを開くのはclamdなので同じエラーになり、終了コードは2です。clamdscan --fdpass にすると、検証ではroot・一般ユーザーのどちらでも Eicar-Test-Signature FOUND まで進みました。--fdpass はUNIXソケットでつないでいるときだけ使える仕組みです。

clamonaccが「fanotify_init failed: Operation not permitted」で終了する

一般ユーザーでclamonaccを起動したとき
ERROR: Clamonacc: fanotify_init failed: Operation not permitted
ERROR: Clamonacc: clamonacc must have elevated permissions ... exiting ...

fanotifyにはCAP_SYS_ADMINが必要なため、root以外で起動するとこうなります。サーバーではパッケージのユニット(User=root)から起動してください。コンテナの中でリアルタイム検査を試す場合も同じ理由で SYS_ADMIN のケーパビリティが必要ですが、本番のコンテナに与えるのは権限が強すぎるので、ホスト側でclamonaccを動かしてボリュームのディレクトリを監視する構成を筆者は選びます(この構成はこの環境では試していません)。

動作確認

導入後と設定変更のたびに、次の順で確かめます。右側のコメントは検証環境で得た結果で、最後の行だけは見るべき点を書いています。版数や日付はDBの更新で変わります。

terminal
clamscan --version                      # ClamAV 1.5.4/28134/Fri Sep 25 06:25:58 2026
clamdscan --ping 1                      # PONG
ps -o pid,user,rss,cmd -C clamd         # clamav ユーザーで RSS 約 990000(kB)
grep -E '^Max(File|Scan)Size' /etc/clamav/clamd.conf   # MaxScanSize 400M / MaxFileSize 100M
cd ~/scan-test && clamdscan --fdpass eicar.com; echo "exit=$?"   # Eicar-Test-Signature FOUND / exit=1
sudo DRY_RUN=1 MONITOR_EXIT_STATUS=1 /usr/local/sbin/clamav-alert clamav-scan.service | head -2   # To: と VIRUS FOUND を含む Subject:
systemctl list-timers clamav-scan.timer # NEXT が日本時間の 3:30〜3:50 になっているか

最後の list-timers は検証環境では実行できていないため、出力例は載せていません。確認するのは、NEXTの列が日本時間の3時30分から3時50分(RandomizedDelaySec の幅)に入っていることと、翌朝 journalctl -u clamav-scan.service にスキャンの要約が残っていることです。EICARファイルは確認が済んだら rm ~/scan-test/eicar.com で消しておきます。残しておくと、毎晩のスキャンがそれを検出して通知を出し続けます。

よくある質問

ClamAVはアンチウイルスソフトですか?

はい、シグネチャ照合型のウイルス対策エンジンです。ただしWindows向けの製品のような画面や中央管理の仕組みは標準では持たず、Linuxではclamd・clamdscan・freshclamを組み合わせ、定期スキャンや通知は自分で組みます。ふるまい検知や侵入後の調査をするEDRとは別物です。

ClamAVはCiscoのソフトですか?

開発元はCiscoで、1.5.4のclamscanやclamdscanのヘルプにも「(C) 2025 Cisco Systems, Inc.」と表示されます(執筆時点)。ライセンスはGPLv2のオープンソースで、UbuntuではCanonicalの公式リポジトリからパッケージとして入るため、Ciscoと契約しなくてもaptで導入できます。

ClamAVは商用利用できますか?

GPLv2なので、社内のサーバーで業務に使うことにライセンス費用はかかりません。自社製品に組み込んで配布する場合は、ソースコードの提供などGPLの義務が生じます。公式シグネチャの配布サービスの利用条件は変わることがあるため、執筆時点の条件を公式サイトで確認してください。

ClamAVの検出率はどのくらいですか?

この記事では検出率を測っていません。EICARは仕組みが動くかを確かめるテストファイルで、性能の指標にはなりません。照合の対象は既知のマルウェアのハッシュやパターンが中心なので、出たばかりの検体やファイルを残さない攻撃は苦手と考え、必要な範囲はEDRで補います。

まとめ

ClamAVを常駐させるかは、free -m のavailableが2.5GBあるか、1.5GBで ConcurrentDatabaseReload no にするか、常駐をやめるかの3択で決まります。生成されたままのclamd.confは25MB超のファイルを黙って通すので、MaxFileSize と MaxScanSize の変更を最初の作業に入れてください。

次にやることは、対象サーバーで free -m を見てから導入し、EICARで検出を確かめ、DRY_RUN=1 で通知の中身を見たうえでMTAから1通テスト送信することです。チェックシートには、方式と範囲を書いた回答を用意しておきます。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。