ClamAV malware scanning finds uploaded shells, droppers and infected PHP files inside your web root before they spread. This guide installs ClamAV on a Linux VPS, tunes it so it can read large web directories, runs a first scan and schedules daily scans with a systemd timer.
You need SSH access with sudo. Run the commands as a sudo user, not as root directly, unless a step says otherwise.
Before you start
- A Linux VPS on Ubuntu 22.04/24.04, Debian 12, AlmaLinux 8–9 or Rocky 8–9.
- A sudo user. If you do not have one yet, see create a sudo user on Linux.
- Roughly 1–2 GB of free RAM for the scanner on a busy site. ClamAV loads its signature database into memory, so a small VPS benefits from adding a swap file.
- The path to your web root. Common values are
/var/www/html,/var/www/example.com/public_htmlor/home/deploy/htdocs.
Note: ClamAV is a signature scanner. It catches known malware, not every backdoor. Treat it as one layer next to a firewall, file integrity monitoring and a web application firewall.
Install ClamAV on your Linux VPS
On Debian and Ubuntu:
sudo apt update
sudo apt install -y clamav clamav-daemonOn AlmaLinux and Rocky Linux, ClamAV lives in EPEL:
sudo dnf install -y epel-release
sudo dnf install -y clamav clamav-update clamav-scanner-systemdThe RHEL packages ship a config file containing the word Example on its own line. The scanner refuses to start while that line exists, so remove it:
sudo sed -i '/^Example$/d' /etc/freshclam.conf
sudo sed -i '/^Example$/d' /etc/clamd.d/scan.confConfig paths differ by family: Debian and Ubuntu use /etc/clamav/clamd.conf and /etc/clamav/freshclam.conf, while AlmaLinux and Rocky use /etc/clamd.d/scan.conf and /etc/freshclam.conf. The rest of this guide names both where it matters.
Update the virus database
The freshclam service downloads signatures on a schedule, but the first run should be manual so you can see errors:
sudo systemctl stop clamav-freshclam 2>/dev/null
sudo freshclam
sudo systemctl start clamav-freshclam 2>/dev/nullOn AlmaLinux and Rocky the service is called clamav-freshclam too, but it is not enabled by default. Enable it so signatures stay current:
sudo systemctl enable --now clamav-freshclamExpect output ending with a line like Database updated (… signatures). If you see Can't open /var/log/clamav/freshclam.log, the log file is owned by the wrong user — check the ownership against the clamav or clamupdate account on your distribution.
Configure ClamAV for web root scanning
Raise the file size limits
Default limits skip large archives and media files, which is exactly where droppers hide. Edit the daemon config:
sudo nano /etc/clamav/clamd.confAdd or change these values:
MaxFileSize 100M
MaxScanSize 400M
MaxRecursion 20
MaxThreads 4MaxScanSize must be larger than MaxFileSize, because it covers the total data read from an archive after unpacking. Keep MaxThreads at or below your vCPU count.
Keep the scanner out of the way
Add exclusions so the daemon does not walk pseudo-filesystems or your own quarantine folder:
ExcludePath ^/proc
ExcludePath ^/sys
ExcludePath ^/dev
ExcludePath ^/var/lib/clamav/quarantineRestart the daemon and confirm it is listening on its local socket:
sudo systemctl restart clamav-daemon
sudo systemctl status clamav-daemonOptional: on-access scanning
ClamAV can scan files the moment they are written, using the kernel fanotify interface. Add to clamd.conf:
OnAccessIncludePath /var/www
OnAccessPrevention yes
OnAccessExcludeUname clamavThis only works on kernels with fanotify support. On OpenVZ or unprivileged LXC containers it usually fails silently, so stick to scheduled scans there. Check the daemon log after restarting to see whether on-access scanning started.
Run a ClamAV malware scan on the web root
Use clamdscan, which talks to the running daemon and reuses the loaded signature database. It is far faster than the standalone clamscan binary.
sudo clamdscan --multiscan --fdpass /var/www--multiscanscans with several threads.--fdpasspasses file descriptors so the unprivilegedclamavuser can read files owned by your web user.
To keep a log and only print findings:
sudo clamdscan --multiscan --fdpass --log=/var/log/clamav/webroot-scan.log /var/wwwA clean run ends with Infected files: 0. A first scan of a large site can take several minutes; later scans are quicker because the signature database is already in memory.
Warning: Do not delete or move files while a scan is running. Wait for it to finish so you have a complete list of detections before you change anything.
Automate daily scans with a systemd timer
Create the service unit:
sudo nano /etc/systemd/system/clamav-webroot.service[Unit]
Description=ClamAV scan of the web root
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/clamdscan --multiscan --fdpass --log=/var/log/clamav/webroot-scan.log /var/wwwCreate the timer:
sudo nano /etc/systemd/system/clamav-webroot.timer[Unit]
Description=Daily ClamAV web root scan
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=30m
[Install]
WantedBy=timers.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable --now clamav-webroot.timer
systemctl list-timers clamav-webroot.timerPersistent=true runs a missed scan after a reboot, and the random delay avoids a scan starting exactly at midnight.
Handle detections safely
When a scan reports a file, do not delete it immediately. Move it to a quarantine directory first, with permissions that stop the web server from executing it:
sudo mkdir -p /var/lib/clamav/quarantine
sudo mv /var/www/example.com/public_html/uploads/shell.php /var/lib/clamav/quarantine/
sudo chmod 000 /var/lib/clamav/quarantine/shell.phpThen check how the file arrived: look at the web server access log around the file's modification time, and compare it with your deployment history. If several files are flagged, assume the account is compromised and follow Linux incident response: isolate a compromised server before cleaning up.
Tip: Keep a copy of every quarantined file until you have finished investigating. It is your only evidence of what the attacker uploaded.
Check it works
The EICAR test string is a harmless file that every antivirus engine flags. Create it in your web root:
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' | sudo tee /var/www/html/eicar.txt
sudo clamdscan --fdpass /var/www/html/eicar.txtYou should see the file listed as Eicar-Test-Signature and Infected files: 1. Remove it afterwards:
sudo rm /var/www/html/eicar.txtIf the test file is not detected, the daemon is not reading the path you think it is — check the LocalSocket value in clamd.conf and confirm clamdscan uses the same one.
Troubleshooting
Could not connect to clamd on LocalSocket /run/clamav/clamd.ctl— the daemon is stopped or crashed. Runsudo systemctl status clamav-daemonand read the last lines of/var/log/clamav/clamav.log. A missingExampleline removal on RHEL is a common cause.freshclamreportsYour ClamAV installation is OUTDATED— the package is older than the signature format. Update the package from your distribution's repositories, or add the vendor's official repository if your distro lags behind.- Scan stops with
Out of memoryor the process is killed — lowerMaxThreadsto 1 or 2, reduceMaxScanSize, and add swap. Scanning a large web root on a 1 GB VPS needs headroom. - On-access scanning never triggers — the kernel has no fanotify support (common in containers). Remove the
OnAccess*lines and rely on the systemd timer instead. - A legitimate file is flagged — check the reported signature name and the file hash before acting. Add a targeted
ExcludePathfor that file only if you have confirmed it is clean, rather than disabling scanning for the whole directory.
Next steps
- Watch the web root in real time with inotify-tools for file system change monitoring.
- Block common upload and injection attacks at the edge with ModSecurity on Nginx.
- Make sure you can restore clean files after a cleanup with rsync and restic backups.
