2026年08月21日 03:40 / - PV

図書館で本を予約したら、タイミングが被ってしまい一気に5冊も届いてしまいました...。
全部読み切れるかな... そんなyuku_tasです。
Hackerの皆様はいかがお過ごしでしょうか。
Linux用のアンチウィルスソフト ClamAVをPHPプロジェクトに組み込みたくなりました。
Laravelをよく使うので、Validationでついでにウィルスチェックできる clamav-validatorを見つけて、試行錯誤してみたのですが 動く感じがせず。
Github Issueでは 「これは打ち捨てられてるレポジトリじゃないか?」 と言われてしまっていました...。
ないものは仕方ありません。ClamAVをライブラリを経由せず直接利用し、アップロードされたファイルをその場でスキャンして、不備がある場合は削除する。という設計思想で運用しようと思います。
私のLinuxサーバのカーネルは
ですが、以下のコマンドで入りました。Amazon Linux2やCentOSならばyum
でインストールしましょう。
sudo dnf update
sudo dnf install -y clamav clamav-update clamd
本体はあくまでclamavですが、clamdをデーモンとして常時待機 させ、clamscan [file or dir]で、clamd経由でファイルスキャンを行う。という形で運用したいと思います。
ウィルスをブロックするための定義ファイルを以下のコマンドで 最新のものに更新しておきます。
sudo freshclam
cronで以下を設定します。わたしは、ログイン中のsshユーザからcrontab -eで入れました。
N * * * * /usr/bin/freshclam --quiet
Nのところは、3~57の間にして欲しいと。10の倍数は選択しないで欲しいと公式ドキュメントにも書いてあります 理由は、00分に実行されるスクリプトが多いので、とのことでした。そちらに従って、適当(ちゃんとした意味で)な数字を設定します。
to check for a new database every hour. N should be a number between 3 and 57 of your choice. Please don’t choose any multiple of 10, because there are already too many clients using those time slots.
1時間ごとに新しいデータベースをチェックすることになります。Nは、3から57の間で好きな数字を選んでください。10 の倍数は選択しないでください。これらのタイムスロットを使用するクライアントがすでに多すぎるからです。
/etc/freshclam.confを変更します。
下記の部分を探し、
# Send the RELOAD command to clamd.
# Default: no
#NotifyClamd /path/to/clamd.conf
NotifyClamd /etc/clamd.d/scan.conf
こちらのように修正して、定義ファイルの更新後に、clamd(デーモンスクリプト)に設定がすぐに伝わるように します。
# ログ出力設定
LogFile /var/log/clamd.scan
LogFileMaxSize 2M
LogSyslog yes
LogRotate yes
# TCPのソケットを使う
#LocalSocket /run/clamd.scan/clamd.sock
TCPSocket 3310
TCPAddr 127.0.0.1
このように設定してログを出したり、clamdが待ち受けるポートやIPなどを定義しておきます。
他、コメントアウトしてある箇所はそのまま変更なしでセーブします。
下記がsystemdで管理されているclamdのユニット設定ファイルです。起動後、こう動きなさいというレシピみたいなもの ですね。
/usr/lib/systemd/system/clamd@.service
これをviで開いて、以下のように変更しておきます。(変更点のみ書きます)
sudo vi /usr/lib/systemd/system/clamd@.service
[Unit]
...
[Service]
...
TimeoutStartSec=600
MemoryLimit=512M
CPUQuota=30%
[Install]
...
後述しますが clamscanの処理が重いので、clamdは軽く動くようにメモリの上限値・CPUの上限値を指定して起動する ことにします。
ここまで設定を行ってきたclamdをsystemctlで起動時に立ち上がるプログラムとして登録し、実際にここで開始しておきます。
sudo systemctl enable clamd@scan
sudo systemctl start clamd@scan
これで、ファイルスキャンが実行できる準備が整いました。
こちらからテスト用のウィルス定義ファイルeicar.comをダウンロードしてFTPを経由して設置します。
もしくは、下記のcurlコマンドを実行して、Linuxサーバに直接落としてくることも可能です。
curl -O https://secure.eicar.org/eicar.com
定義ファイルを入れたディレクトリに移動し、clamscanコマンドを実行したところマシンが固まってしまいました。
cd /path/to/eicar.com/exist
clamscan eicar.com
どうも、このインスタンスの2GB ではメモリが足りないようです。
そのままでは何もできないので、実験用にマシンをスケールアップしました。
その結果、実行中に3GBくらいはclamscanの実行にメモリを使ってしまう感じですが、30秒くらいで結果が出ました。
$ clamscan eicar.com
/home/ec2-user/work/eicar.com: Win.Test.EICAR_HDB-1 FOUND
----------- SCAN SUMMARY -----------
Known viruses: 8683058
Engine version: 0.103.9
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.00 MB
Data read: 0.00 MB (ratio 0.00:1)
Time: 27.969 sec (0 m 27 s)
Start Date: 2024:01:23 02:32:20
End Date: 2024:01:23 02:32:48
成功?してウィルスとして判定されたようです。
clamscanコマンドをシェルなどを経由して叩き、標準出力に出た結果を受け取って、preg_matchなどの正規表現で判定する形になるかな、と思っています。
例えば、Laravelのファイル置き場はstorage/app/data以下などになりますので、
clamscan /storage/app/data/upload/xxxx.zip
と呼び出すシェルスクリプトをLaravelのバッチから呼ぶといったイメージでしょうか。「Infected files」が1以上ならウィルスとして削除する、という判定はできるでしょう。
本当はkissit/php-clamav-scanのようなライブラリがあるといいのですが 残念ながらメンテされていないようですので、直接呼ぶ方向になりそうです。
ここまでの経緯でご説明した通り、2GBのマシンで動かないのはキツく、気軽に導入するわけには行かなそうでした。よって、別の方法でウィルススキャンする方法を考えるようにしました。
(→ その後、clamdscanを使えばこの問題は解決できることが分かりました。次の【2026年追記】をご覧ください。)
記事公開後に運用・調査を重ねた結果、「clamscanが重い」問題への正しい答えはclamdscanだった ので追記します。
本文で実行したclamscanは、実行するたびに800万件以上のウィルス定義DBをメモリに読み込むコマンドです。1回のスキャンに3GB近いメモリと30秒近い時間がかかったのはこれが原因で、マシンスペックの問題ではありませんでした。
一方clamdscanは、すでに起動しているclamdデーモンにファイルを渡してスキャンさせるコマンドです。定義DBはclamdが常駐メモリに1回だけ読み込んでおり、スキャンのたびに読み直すことはありません。
| コマンド | 定義DBの読み込み | 1スキャンあたりの負荷 |
|---|---|---|
clamscan | 実行のたびに読み込む | メモリ約3GB・数十秒 |
clamdscan | clamdが常駐分を使い回す | ほぼゼロ・通常は数秒以内 |
本文の構成ではせっかくclamdを立ち上げていたのに、スキャンにclamscanを使っていたため、デーモンの意味がなくなっていた訳です...。
clamdscan --fdpass /path/to/upload/file.zip
--fdpassは、ファイルディスクリプタごとclamdに渡すオプションです。clamdはclamscanユーザーなど別ユーザーで動くことが多く、スキャン対象ファイルへの読み取り権限がなくてエラーになる事故を防げるので、基本付けておくのがおすすめです。
判定は終了コードで行えます。
0 ... ウィルスなし1 ... ウィルス検出2 ... スキャン自体のエラー(clamdが起動していない等)clamd自体も定義DBの分(1GB強)のメモリを常駐で使いますが、/etc/clamd.d/scan.confに以下を追加すると
定義DB更新時のピークメモリを抑えられます。
ConcurrentDatabaseReload no
デフォルトでは定義DBの再読み込み時に新旧2つのDBを同時にメモリへ持つため、瞬間的に倍のメモリを消費します。この設定を入れると再読み込み中のスキャンは待たされますが、メモリ2GBのマシンで運用するなら実質必須の設定です。
それでも足りない場合は、スワップを追加しておくと定義DB更新時のOOM killerによるclamd強制終了を防げます。
本文で「シェルを経由して叩く」と書いた部分も、Laravel 10以降ならProcessファサードでシンプルに書けます。
use Illuminate\Support\Facades\Process;
$result = Process::run([
'clamdscan', '--fdpass', '--no-summary',
storage_path('app/data/upload/uploaded.zip'),
]);
if ($result->exitCode() === 1) {
// ウィルス検出。ファイルを削除し、ユーザーにエラーを返す
} elseif ($result->exitCode() !== 0) {
// スキャン自体の失敗(clamdが落ちている等)。fail-close にするのが安全
}
ポイントは、スキャンできなかった場合(終了コード2)を「安全」と扱わないことです。clamdが落ちている間にアップロードされたファイルを素通しにしないよう、エラー時は受け付けない設計(fail-close)をおすすめします。
以下のサイト様を参考にさせていただきました。(他にもあるかもです、気づき次第追加させていただきます。)
https://www.fumibu.com/use_clam-anti-virus_debian-package/#toc4
https://souiunogaii.hatenablog.com/entry/clamd-memory-limit
http://safe-linux.homeip.net/mail/clamav/clamd.html
https://blog.kamata-net.com/archives/12891.html
実行ユーザーは今回は実用しないということでrootでやってしまいましたが、商用利用する場合は
clamavユーザーなどを作り、rootで実行させないようにした方が安全だと公式ドキュメントに書いてありました。
実際に使う機会があればその方向でユーザーを作る形で考えたいと思います。
その場合、パーミッションの設定は残念ながらちょっと面倒そうでしたね。(追記の通りclamdscan --fdpassを使えば、この権限まわりの面倒もかなり軽減されます。)
当初「2GBのマシンでは実用が厳しい」と結論づけましたが、clamdを常駐させてclamdscanでスキャンする構成に
すれば、2GBクラスのマシンでも十分実用できます。 これから導入される方は最初からその構成で組んでください。
この記事が何かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました!
Share Me!