ClamAV & Malware Protection

Chapter 7 — ClamAV & Malware Protection

The previous chapters secured the doors — locking down SSH, hardening the firewall, adding browser-enforced policies. This chapter deals with what happens if something slips through anyway. Malware on a web server most commonly arrives as a PHP web shell uploaded through a vulnerable application, as a compromised package, or injected into legitimate files during an attack. ClamAV is the Linux standard for scanning and catching these — and a few Apache configuration tricks can prevent uploaded files from being executed at all.

What this chapter covers: What ClamAV does and where it fits in your defences. The three ClamAV packages (clamscan, freshclam, clamd daemon). Keeping virus definitions current. Running on-demand scans with the most useful flags — and why to quarantine rather than auto-delete. Scheduled scans via cron and systemd timer. Protecting upload directories by denying PHP execution in Apache. On-access scanning for new uploads. The incident response playbook when a scan finds something. Complementary tools: rkhunter (rootkits), AIDE (file integrity). Resource considerations on a home server.

What ClamAV Does — and Its Limits

ClamAV's role in the security stack: Layer 1 — Network: UFW (Chapter 3) blocks unwanted connections Layer 2 — Auth: fail2ban (Ch.4) bans brute-force sources SSH keys (Ch.5) eliminates password attacks Layer 3 — Browser: Security headers (Ch.6) limits what browser runs Layer 4 — Files: ClamAV (this chapter) detects malicious files Apache upload rules prevents PHP execution Layer 5 — Monitoring: logwatch, unattended-upgrades (Chapter 8) ClamAV specifically watches for: ├── PHP web shells (uploaded .php files that give remote shell access) ├── Malicious JavaScript injected into HTML/PHP files ├── Trojan scripts hidden in archives or disguised as images ├── Known malware signatures in downloaded packages └── Modifications to existing files (when combined with AIDE)

ClamAV is signature-based — it recognises malware it has seen before (or variants close enough to match a pattern). It won't catch genuinely novel malware until signatures are updated. For a web server, the most important threats it catches are well-known PHP web shells and common malicious scripts — exactly the kind of tool-based attacks that automated exploit kits deploy.

  • It catches: known web shells, common trojans, malicious scripts, infected archives, most real-world automated attacks.
  • It doesn't catch: zero-day exploits, custom malware written specifically for your server, compromised dependencies in your code (use composer audit / npm audit for those).
  • It's not real-time by default: scanning is on-demand or scheduled. On-access scanning (daemon mode) can make it reactive, but at a resource cost.

The Three ClamAV Components

clamav
Scanner
Contains clamscan — the command-line scanner. Loads signatures into memory each run (slower, but no daemon needed). Good for scheduled scans and one-off checks.
clamav-freshclam
Definition updater
The freshclam service downloads new virus signatures from ClamAV's mirrors. Runs as a daemon that checks for updates multiple times daily. Definitions are useless if they're stale.
clamav-daemon
Background daemon
Runs clamd — keeps signatures loaded in memory between scans. clamdscan talks to it and is much faster than clamscan for repeated scans. Also enables on-access scanning.
clamdscan
Daemon client
The client that sends scan jobs to clamd. Same flags as clamscan but faster because signatures are pre-loaded. Use this for scheduled scans if clamd is running.
Home server resource note: clamd loads the entire signature database into RAM — roughly 500 MB–1 GB. On a server with 2–4 GB of RAM also running Apache, MySQL, and cloudflared, that's a significant chunk. If RAM is tight, skip clamav-daemon and use clamscan directly for scheduled scans — it's slower but uses no persistent RAM.

Installing ClamAV

# Install ClamAV and the daemon $ sudo apt install clamav clamav-daemon -y # Stop freshclam service before running a manual update # (freshclam service and manual freshclam can't run simultaneously) $ sudo systemctl stop clamav-freshclam # Download the latest virus definitions (takes a few minutes on first run) $ sudo freshclam ClamAV update process started at Sun Jun 15 12:00:00 2026 daily.cvd database is up-to-date (version: 27249, sigs: 2054253, ...) main.cvd database is up-to-date (version: 62, sigs: 6647427, ...) bytecode.cvd is up-to-date (version: 334, sigs: 91, ...) # Restart the freshclam service to resume automatic updates $ sudo systemctl start clamav-freshclam $ sudo systemctl enable clamav-freshclam # Start clamd (if you're using the daemon) $ sudo systemctl start clamav-daemon $ sudo systemctl enable clamav-daemon # Verify everything is running $ sudo systemctl status clamav-freshclam clamav-daemon ● clamav-freshclam.service — ClamAV virus database updater Active: active (running) ● clamav-daemon.service — Clam AntiVirus userspace daemon Active: active (running) # Check signature database version and age $ clamscan --version ClamAV 1.0.3/27249/Sun Jun 15 09:00:01 2026 # ^^^^^ daily DB version / date — should be within 2-3 days of today

Running Scans

Scanning the web root (most important scan to run regularly)

# Scan the web root recursively — show only infected files (-i flag) $ sudo clamscan -r -i /var/www/osztromok.com/public_html/ ----------- SCAN SUMMARY ----------- Known viruses: 9580123 Engine version: 1.0.3 Scanned files: 847 Infected files: 0 ← nothing found Time: 12.234 sec (0 m 12 s) # Scan entire /var/www (covers all virtual hosts) $ sudo clamscan -r -i /var/www/ # Using the daemon for faster scanning (clamd must be running) $ sudo clamdscan -i /var/www/osztromok.com/public_html/

Key flags

-r / --recursive
Scan directories recursively — descends into subdirectories. Essential for scanning a web root. Without it, only files in the top directory are scanned.
-i / --infected
Only print infected file names. Without this, every scanned file is printed — noisy and hard to read in large directories. Always use this.
--move=DIR
Move infected files to a quarantine directory instead of leaving them in place. Preferred over --remove — you can review what was found before permanent deletion.
--remove
Use with caution — permanently deletes infected files immediately with no review. Appropriate for automated scans of upload temp directories, not for scanning your web root.
--exclude-dir=DIR
Skip a directory. Always exclude /proc, /sys, /dev when scanning the whole system — these are virtual filesystems, not real files.
--log=FILE
Write scan results to a log file. Essential for scheduled scans so you can review results later without watching the terminal.
--max-filesize=MB
Skip files larger than this size (default 25 MB). Large video files don't need virus scanning — adjust if your site serves large files.
-v / --verbose
Show every file being scanned (useful for debugging why a specific file isn't being scanned). Don't use in scheduled scans — generates huge log files.

Reading scan output — infected file found

/var/www/osztromok.com/public_html/uploads/image_20260615.php: Php.Webshell.Generic-2 FOUND ----------- SCAN SUMMARY ----------- Known viruses: 9580123 Engine version: 1.0.3 Scanned files: 1247 Infected files: 1 Time: 18.451 sec (0 m 18 s) ↑ Signature name "Php.Webshell.Generic-2" tells you: Php = PHP malware Webshell = remote command execution shell Generic-2 = a known generic web shell variant The path shows it was uploaded to the uploads directory — classic attack.
Do not delete without investigating first. When ClamAV finds something, the immediate question is not "how do I delete this?" — it's "how did it get there?" If you delete the file without finding the entry point, the attacker will just upload another. See the incident response playbook below.

Setting Up a Quarantine Directory

# Create a quarantine directory — outside the web root $ sudo mkdir -p /var/quarantine $ sudo chmod 700 /var/quarantine # only root can access # Run a scan that moves infected files to quarantine instead of leaving them $ sudo clamscan -r -i --move=/var/quarantine /var/www/ /var/www/osztromok.com/public_html/uploads/shell.php: Php.Webshell.Generic-2 FOUND /var/www/osztromok.com/public_html/uploads/shell.php: moved to '/var/quarantine/shell.php' # Review what's in quarantine before deleting $ sudo ls -la /var/quarantine/ $ sudo file /var/quarantine/shell.php # confirm file type $ sudo cat /var/quarantine/shell.php # read the content — understand what it does # When satisfied — delete quarantined files $ sudo rm -rf /var/quarantine/*

Scheduled Scans

Option A — Cron job (simple)

$ sudo crontab -e # edit root's crontab
# Run at 3:00 AM daily — scan web root, log results, email on detection 0 3 * * * clamscan -r -i --move=/var/quarantine --log=/var/log/clamav/daily-scan.log /var/www/ && echo "ClamAV scan complete — no threats" | mail -s "ClamAV Daily OK" emubantam@gmail.com || echo "ClamAV scan found threats — check /var/log/clamav/daily-scan.log" | mail -s "ClamAV ALERT" emubantam@gmail.com # Simpler version — just log, no email: 0 3 * * * clamscan -r -i --move=/var/quarantine --log=/var/log/clamav/daily-scan.log /var/www/ # Weekly full-system scan (Sundays at 4 AM — longer running, skip virtual filesystems) 0 4 * * 0 clamscan -r -i --move=/var/quarantine --exclude-dir=^/proc --exclude-dir=^/sys --exclude-dir=^/dev --exclude-dir=^/run --log=/var/log/clamav/weekly-scan.log /

Option B — Systemd timer (cleaner, with logging)

# Create the scan script $ sudo nano /usr/local/bin/clamav-daily-scan.sh
#!/bin/bash # /usr/local/bin/clamav-daily-scan.sh LOGFILE="/var/log/clamav/daily-scan-$(date +%Y-%m-%d).log" QUARANTINE="/var/quarantine" WEBROOT="/var/www" echo "ClamAV scan started at $(date)" > "$LOGFILE" clamscan -r -i --move="$QUARANTINE" --log="$LOGFILE" "$WEBROOT" EXIT_CODE=$? if [ $EXIT_CODE -eq 1 ]; then echo "ALERT: Infected files found — check $LOGFILE and $QUARANTINE" | \ mail -s "ClamAV THREAT DETECTED on $(hostname)" emubantam@gmail.com fi # Exit codes: 0 = no threats, 1 = threats found, 2 = error exit $EXIT_CODE
$ sudo chmod +x /usr/local/bin/clamav-daily-scan.sh # Create the systemd service unit $ sudo nano /etc/systemd/system/clamav-daily-scan.service
[Unit] Description=ClamAV Daily Web Root Scan After=clamav-daemon.service [Service] Type=oneshot ExecStart=/usr/local/bin/clamav-daily-scan.sh User=root
# Create the timer unit $ sudo nano /etc/systemd/system/clamav-daily-scan.timer
[Unit] Description=Run ClamAV daily web root scan at 3 AM [Timer] OnCalendar=*-*-* 03:00:00 RandomizedDelaySec=300 Persistent=true [Install] WantedBy=timers.target
$ sudo systemctl daemon-reload $ sudo systemctl enable --now clamav-daily-scan.timer # Verify the timer is scheduled $ sudo systemctl list-timers clamav-daily-scan.timer NEXT LEFT LAST PASSED UNIT ACTIVATES Mon 2026-06-16 03:04:32 BST 14h left - - clamav-daily-scan.timer clamav-daily-scan.service # Test the scan script immediately (without waiting for 3 AM) $ sudo systemctl start clamav-daily-scan.service $ sudo journalctl -u clamav-daily-scan.service --no-pager

Protecting Upload Directories

ClamAV detects malicious files after they've been uploaded. Apache configuration can stop them from being executed even if they slip through. The two defences are complementary — use both.

The most dangerous upload attack is a PHP web shell: an attacker finds a file upload form (contact form, avatar upload, attachment) and uploads a file named shell.php (or shell.php.jpg to bypass naive extension checks). If Apache executes it, the attacker gains full shell access to your server under the web server's user.

# In your VirtualHost config — deny PHP execution in the uploads directory # /etc/apache2/sites-available/osztromok.com.conf <VirtualHost *:443> # ... existing config ... # Uploads directory — allow file serving but block ALL script execution <Directory /var/www/osztromok.com/public_html/uploads> # Disable PHP engine for this directory php_admin_flag engine off # Belt AND braces — also block direct requests to PHP files <FilesMatch "\.php[0-9]?$"> Require all denied </FilesMatch> # Block other executable extensions often used in attacks <FilesMatch "\.(phtml|phar|php3|php4|php5|phps|cgi|pl|py|rb|sh)$"> Require all denied </FilesMatch> # Disable .htaccess overrides in this directory # (attackers can upload a .htaccess that re-enables PHP) AllowOverride None </Directory> </VirtualHost>
$ sudo apache2ctl configtest && sudo systemctl reload apache2 # Test that PHP is blocked in the uploads directory $ echo "<?php echo 'shell'; ?>" | sudo tee /var/www/osztromok.com/public_html/uploads/test.php $ curl https://osztromok.com/uploads/test.php 403 Forbidden ← PHP execution blocked — correct $ sudo rm /var/www/osztromok.com/public_html/uploads/test.php
AllowOverride None in the uploads directory is critical. Without it, an attacker can upload a .htaccess file that re-enables PHP execution with AddType application/x-httpd-php .jpg — turning every uploaded image into an executable PHP file. AllowOverride None prevents Apache from reading any .htaccess files in that directory.

On-Access Scanning for Upload Directories

On-access scanning tells clamd to automatically scan files when they appear or are modified in a specific directory — catching malicious uploads the moment they land, before any web request can trigger them.

# Edit clamd configuration to enable on-access scanning $ sudo nano /etc/clamav/clamd.conf
# Add these lines to /etc/clamav/clamd.conf OnAccessIncludePath /var/www/osztromok.com/public_html/uploads OnAccessExcludeUname clamav # don't scan files created by clamav itself OnAccessPrevention yes # block access to infected files (not just log) OnAccessMaxFileSize 20M # skip files larger than 20 MB
# On-access scanning requires clamd to run as root (check User= in clamd.conf) $ grep "^User" /etc/clamav/clamd.conf User clamav ← change to root for on-access to work $ sudo nano /etc/clamav/clamd.conf # Change: User clamav → User root $ sudo systemctl restart clamav-daemon # Verify on-access is active in the logs $ sudo journalctl -u clamav-daemon -n 20 | grep -i "access" clamd[1234]: On-access module loaded successfully. clamd[1234]: Protecting directory: /var/www/osztromok.com/public_html/uploads
On-access scanning adds CPU overhead — every write to the watched directory triggers a scan. For a busy upload directory on a home server, this can be noticeable. If performance degrades, disable on-access and rely on scheduled scans instead. On-access also requires kernel support for fanotify, present in all modern Ubuntu/Debian kernels.

When a Scan Finds Something — Incident Response

Phase 1Contain — isolate the threat before investigating
Move the infected file to quarantine (if not done automatically by the scan): sudo mv /path/to/threat.php /var/quarantine/. Do not delete it yet — you need to examine it. If the site is actively serving malicious content, consider temporarily taking the vhost offline: sudo a2dissite osztromok.com.conf && sudo systemctl reload apache2.
Phase 2Investigate — how did it get there?
Check Apache access logs for requests to the infected file: sudo grep "threat.php" /var/log/apache2/access.log. Find when it was created: sudo stat /var/quarantine/threat.php — use the mtime (modification time). Check Apache logs around that time for POST requests (uploads): sudo grep "POST" /var/log/apache2/access.log | grep "Jun 15". Look at the file contents: sudo cat /var/quarantine/threat.php — this tells you what the attacker intended and sometimes reveals the upload method. Check for other suspicious files uploaded at the same time: sudo find /var/www/ -newer /var/quarantine/threat.php -type f | head -20.
Phase 3Close the entry point
Identify the upload mechanism the attacker used and close it. Common entry points: unpatched CMS plugin (run sudo -u www-data wp core update && wp plugin update --all), weak file type validation in custom upload code, world-writable directories, compromised credentials. Add PHP execution blocking to the upload directory (see the Apache config above) if not already in place. Add the uploading IP to fail2ban: sudo fail2ban-client set apache-auth banip ATTACKER-IP.
Phase 4Assess damage — was anything exfiltrated or modified?
Check if the web shell was ever executed (not just uploaded): sudo grep "threat.php" /var/log/apache2/access.log — a GET request to it means it was accessed. If it was executed, check for: new user accounts (cat /etc/passwd | grep -v nologin), new cron jobs (sudo crontab -l -u www-data), modified system files (see AIDE below), outbound connections (sudo ss -tnp | grep www-data). Run ClamAV again on the whole system: sudo clamscan -r -i /.
Phase 5Clean up and recover
Delete the quarantined file: sudo rm /var/quarantine/threat.php. Restore any modified legitimate files from git (if your web content is version-controlled) or from backup. Re-enable the site if it was taken offline: sudo a2ensite osztromok.com.conf && sudo systemctl reload apache2. Update all software: sudo apt upgrade && sudo apt autoremove. Run one final clean scan to confirm all threats are gone.

Complementary Tools

ClamAV catches known malware by signature. These tools cover different aspects of the threat landscape:

rkhunter
Rootkit / backdoor scanner
Checks for rootkits — malware that hides from the OS itself. Looks for suspicious files in system directories, backdoored system binaries, and known rootkit signatures. Install: sudo apt install rkhunter. Run: sudo rkhunter --check.
AIDE
File integrity monitor
Advanced Intrusion Detection Environment. Takes a cryptographic snapshot of your filesystem (hashes, permissions, ownership). Later runs compare the current state to the baseline — any changed system file is flagged. Install: sudo apt install aide. Init: sudo aideinit. Check: sudo aide --check.
chkrootkit
Rootkit scanner
Simpler alternative to rkhunter. Checks for signs of rootkits in running processes, network interfaces, and filesystem. Good as a second opinion alongside rkhunter. Install: sudo apt install chkrootkit. Run: sudo chkrootkit.
lynis
System security audit
Comprehensive system hardening audit — checks hundreds of configuration settings across SSH, Apache, filesystem permissions, logging, and more. Produces a scored report with actionable suggestions. Install: sudo apt install lynis. Run: sudo lynis audit system.
# Quick setup for rkhunter — run after installing $ sudo apt install rkhunter -y $ sudo rkhunter --update # update rkhunter database $ sudo rkhunter --propupd # baseline current system (run after trusted system changes) $ sudo rkhunter --check --skip-keypress [ Rootkits ] Checking for known rootkit files and directories [ None found ] Checking for Anonoxymoron Rootkit [ Not found ] ... (many checks) ... [ Application checks ] Checking Apache2 configuration... [ OK ] System checks summary: 0 warnings # Quick AIDE setup $ sudo apt install aide -y $ sudo aideinit # creates the initial database baseline (takes a few minutes) $ sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db $ sudo aide --check # compare current filesystem against baseline AIDE found differences between database and filesystem! Changed: /var/log/apache2/access.log ← expected: log files change constantly # Exclude log directories from AIDE monitoring in /etc/aide/aide.conf

Troubleshooting

freshclam fails — "ERROR: /var/log/clamav/freshclam.log: Permission denied"
The freshclam service is already running and holds the log file, or the log file permissions are wrong. Solution: stop the service before running freshclam manually — sudo systemctl stop clamav-freshclam, then sudo freshclam, then sudo systemctl start clamav-freshclam. Never run sudo freshclam while the service is running — they'll conflict trying to write to the same log file.
clamd fails to start — "ERROR: Can't open/parse the config file /etc/clamav/clamd.conf"
Syntax error in clamd.conf. Check: sudo clamd --config-file=/etc/clamav/clamd.conf --check-config. Common causes: typo in a directive name, a value missing where one is expected, or an OnAccessIncludePath pointing to a non-existent directory. Also check ownership: ls -la /etc/clamav/clamd.conf — it should be owned by clamav:clamav.
clamscan is very slow — taking 10+ minutes for the web root
clamscan loads all signatures into memory on each run — that's ~150 MB of database reading before scanning starts. Switch to clamdscan if the daemon is running (signatures stay loaded in RAM, subsequent scans start immediately). Alternatively, accept the slowness and schedule scans during off-peak hours (3–4 AM). Also check: is /var/www on a network mount? Network filesystem scans are much slower than local disk.
ClamAV flagging a legitimate file as infected (false positive)
False positives happen — ClamAV's signatures are broad patterns that sometimes match benign files. Confirm it's a false positive: look at the file content and the signature name. Report it at https://www.clamav.net/reports/fp. Whitelist the specific file with --exclude=FILENAME or by adding a whitelist to the ClamAV database. Never blindly auto-delete files without a review step — this is why the --move flag (quarantine) is safer than --remove.
clamav-daemon not starting — "ERROR: SelfCheck: Database modification time has changed"
The signature database was updated by freshclam while clamd was running. clamd detected the change and logged an error, or may have stopped. This is normal — clamd should automatically reload the database. If it's stopped: sudo systemctl restart clamav-daemon. To prevent this: configure freshclam to signal clamd when updating (NotifyClamd /etc/clamav/clamd.conf in freshclam.conf).

Quick Reference — Chapter 7

CommandPurpose
sudo clamscan -r -i /var/www/Scan web root recursively, show only infected files
sudo clamdscan -i /var/www/Same scan via daemon — much faster (daemon must be running)
sudo clamscan -r -i --move=/var/quarantine /var/www/Scan and quarantine infected files — preferred over --remove
sudo freshclamManually update virus definitions (stop clamav-freshclam service first)
clamscan --versionCheck current signature database version and date
sudo systemctl status clamav-freshclam clamav-daemonCheck both ClamAV services are running
sudo rkhunter --check --skip-keypressScan for rootkits and backdoors
sudo aide --checkCompare current filesystem against the AIDE baseline
sudo lynis audit systemFull system security audit with scored report
sudo find /var/www/ -name "*.php" -newer /var/www/index.phpFind PHP files added after a reference date (incident investigation)
ToolWhat it catchesWhen to run
ClamAVKnown malware, web shells, trojans in filesDaily scheduled scan of web root
rkhunterRootkits, hidden processes, backdoored binariesWeekly, and after any suspicious activity
AIDEAny file that has changed since the baselineDaily or after software updates
lynisConfiguration weaknesses across the whole systemMonthly, and after major config changes