Security

Malware Scanning on Linux: Configuring ClamAV for Web Root Protection

Install ClamAV on a Linux VPS, tune it for large web directories, scan your web root for malware and schedule daily scans with a systemd timer.

IntermediateLinux server7 min read

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_html or /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:

Bash
sudo apt update
sudo apt install -y clamav clamav-daemon

On AlmaLinux and Rocky Linux, ClamAV lives in EPEL:

Bash
sudo dnf install -y epel-release
sudo dnf install -y clamav clamav-update clamav-scanner-systemd

The 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:

Bash
sudo sed -i '/^Example$/d' /etc/freshclam.conf
sudo sed -i '/^Example$/d' /etc/clamd.d/scan.conf

Config 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:

Bash
sudo systemctl stop clamav-freshclam 2>/dev/null
sudo freshclam
sudo systemctl start clamav-freshclam 2>/dev/null

On AlmaLinux and Rocky the service is called clamav-freshclam too, but it is not enabled by default. Enable it so signatures stay current:

Bash
sudo systemctl enable --now clamav-freshclam

Expect 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:

Bash
sudo nano /etc/clamav/clamd.conf

Add or change these values:

INI
MaxFileSize 100M
MaxScanSize 400M
MaxRecursion 20
MaxThreads 4

MaxScanSize 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:

INI
ExcludePath ^/proc
ExcludePath ^/sys
ExcludePath ^/dev
ExcludePath ^/var/lib/clamav/quarantine

Restart the daemon and confirm it is listening on its local socket:

Bash
sudo systemctl restart clamav-daemon
sudo systemctl status clamav-daemon

Optional: on-access scanning

ClamAV can scan files the moment they are written, using the kernel fanotify interface. Add to clamd.conf:

INI
OnAccessIncludePath /var/www
OnAccessPrevention yes
OnAccessExcludeUname clamav

This 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.

Bash
sudo clamdscan --multiscan --fdpass /var/www
  • --multiscan scans with several threads.
  • --fdpass passes file descriptors so the unprivileged clamav user can read files owned by your web user.

To keep a log and only print findings:

Bash
sudo clamdscan --multiscan --fdpass --log=/var/log/clamav/webroot-scan.log /var/www

A 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:

Bash
sudo nano /etc/systemd/system/clamav-webroot.service
INI
[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/www

Create the timer:

Bash
sudo nano /etc/systemd/system/clamav-webroot.timer
INI
[Unit]
Description=Daily ClamAV web root scan

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=30m

[Install]
WantedBy=timers.target

Enable it:

Bash
sudo systemctl daemon-reload
sudo systemctl enable --now clamav-webroot.timer
systemctl list-timers clamav-webroot.timer

Persistent=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:

Bash
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.php

Then 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:

Bash
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.txt

You should see the file listed as Eicar-Test-Signature and Infected files: 1. Remove it afterwards:

Bash
sudo rm /var/www/html/eicar.txt

If 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. Run sudo systemctl status clamav-daemon and read the last lines of /var/log/clamav/clamav.log. A missing Example line removal on RHEL is a common cause.
  • freshclam reports Your 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 memory or the process is killed — lower MaxThreads to 1 or 2, reduce MaxScanSize, 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 ExcludePath for that file only if you have confirmed it is clean, rather than disabling scanning for the whole directory.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.