Installing Linux Malware Detect (LMD) with ClamAV on CentOS Stream 9: Protecting Your Web Server from Malware

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

When a Shell Gets Uploaded to Your Web Server Without You Knowing

Last year, a client asked me to check a CentOS server running WordPress. Traffic looked normal, the site was up, nothing seemed off — until I spotted some suspicious PHP files sitting in /var/www/html/wp-content/uploads/. They had image-looking names but .php.jpg extensions. Actual webshells. The attacker had been using them for nearly three weeks to mine crypto on the server without anyone noticing.

That was the moment I got serious about setting up a malware scanner, instead of just relying on the firewall and SELinux. In this post, I’ll share how to use LMD (Linux Malware Detect) with ClamAV on CentOS Stream 9 — a combination well-suited for VPS and shared hosting environments running a web server.

A bit of context: when CentOS 8 reached EOL, I had to urgently migrate 5 servers to Rocky Linux within a week. That experience taught me one thing — tools with fewer distro-specific package dependencies are far easier to port. LMD installs from source, ClamAV is available in EPEL, and both have run smoothly from CentOS 7 through Stream 9 with almost no changes needed.

Why Use Both Instead of Just One?

LMD and ClamAV cover different angles — you need both together for adequate coverage:

  • LMD — specializes in web malware detection: PHP webshells, backdoors, script injection. LMD’s signature database is built from real-world honeypots, so it closely matches the types of malware targeting Linux web servers.
  • ClamAV — a fast, widely-used antivirus engine. When LMD uses ClamAV as its backend, scan speed increases 3–4x compared to LMD’s built-in engine.

Using ClamAV alone: its signatures don’t cover newer PHP webshells well enough. Using LMD alone: scans are slow and resource-hungry. Combined, you get a practical setup that’s production-ready.

Installing LMD and ClamAV on CentOS Stream 9

Step 1: Install ClamAV via EPEL

CentOS Stream 9 requires the EPEL repo to get ClamAV. If you don’t have EPEL yet:

dnf install -y epel-release
dnf install -y clamav clamd clamav-update

Update the signature database immediately after installation:

freshclam

If you encounter a lock file error, remove it and try again:

rm -f /var/lock/subsys/clamd.scan
freshclam

Step 2: Install Linux Malware Detect from Source

LMD isn’t in the official repos — you need to download it directly from rfxn.com. Their install script is straightforward:

cd /tmp
curl -LO https://www.rfxn.com/downloads/maldetect-current.tar.gz
tar xzf maldetect-current.tar.gz
cd maldetect-*/
bash install.sh

The script installs LMD into /usr/local/maldetect/ and creates a symlink at /usr/local/sbin/maldet. Verify the version:

maldet -v

Detailed Configuration

Configuring conf.maldet

The main configuration file is /usr/local/maldetect/conf.maldet. Here are the important options to change:

vi /usr/local/maldetect/conf.maldet
# Enable email alerts when malware is detected
email_alert="1"
email_addr="[email protected]"

# Automatically quarantine detected files
quarantine_hits="1"

# Auto-clean injected scripts (removes injected code, keeps original file)
quarantine_clean="1"

# Use ClamAV engine — most important, enable this for faster scans
scan_clamscan="1"
clamscan_path="/usr/bin/clamscan"

# Limit CPU/IO to avoid impacting production
scan_cpunice="19"
scan_ionice="6"

The scan_clamscan="1" setting is something I often forget when building a new server. Without it, LMD falls back to its own engine — about 3–4x slower than the ClamAV backend.

Adding the Web Root to Automated Scans

LMD creates a daily cron at /etc/cron.daily/maldet, which by default scans /tmp and /dev/shm. Create a custom cron entry to control the schedule — I usually set it at 2 AM, when traffic is lowest:

crontab -e
# Scan web root at 2:00 AM every night
0 2 * * * /usr/local/sbin/maldet --scan-all /var/www/html >> /var/log/maldet-cron.log 2>&1

# Update LMD signatures daily at 1:00 AM
0 1 * * * /usr/local/sbin/maldet -u >> /var/log/maldet-update.log 2>&1

ClamAV also needs periodic signature updates:

echo "0 */12 * * * root /usr/bin/freshclam --quiet" > /etc/cron.d/freshclam

Whitelisting Directories That Don’t Need Scanning

Some WordPress plugins use base64_decode, eval, or encoded strings — these can look like shells, causing LMD to generate false positives. Add paths to LMD’s ignore file (one path per line):

echo "/var/www/html/wp-content/plugins/wordfence" >> /usr/local/maldetect/ignore_paths
echo "/var/www/html/wp-content/plugins/your-plugin" >> /usr/local/maldetect/ignore_paths

Rescan that path to verify LMD is correctly ignoring it:

maldet -a /var/www/html/wp-content/plugins/wordfence

Testing and Monitoring

Testing with the EICAR File

EICAR is an international standard test file — not actual malware, but every antivirus recognizes it. Use it to verify your setup works correctly before letting automation run on its own:

echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar-test.com
maldet -a /tmp/eicar-test.com

If the setup is correct, the output will report a hit and the file will be moved to quarantine:

maldet(12345): {hit} {eicar} /tmp/eicar-test.com [MD5: 44d88612...]
maldet(12345): {scan} 1 hits found
maldet(12345): {quarantine} /tmp/eicar-test.com moved to quarantine

Manual Scanning When Something Seems Off

CPU suddenly spikes to 90% at 3 AM, traffic is coming from unfamiliar IPs, or something just feels “off” — this is when you need to scan immediately rather than wait for the cron job:

# Scan the entire web root
maldet -a /var/www/html

# Scan only files modified in the last 3 days
maldet -r /var/www/html 3

# View the scan report
maldet --report

Managing Quarantine

Quarantined files aren’t deleted — they’re just moved to a separate directory. Your site won’t go down immediately, giving you time to review before making a permanent deletion. To list quarantined files:

ls -la /usr/local/maldetect/quarantine/

Restore a file that was incorrectly quarantined (false positive):

maldet --restore /usr/local/maldetect/quarantine/filename

Real-Time Log Monitoring

# Event log — all detection and quarantine activity
tail -f /usr/local/maldetect/logs/event_log

# Scan log — detailed output for each scan
tail -f /usr/local/maldetect/logs/scan_log

Lessons Learned from Running This in Production

After about six months running this setup in production, here are a few takeaways:

  • Quarantine isn’t a substitute for prevention: LMD detects files after they’ve already landed on the server. Pair it with ModSecurity to block malicious uploads at the source — that’s where real defense-in-depth starts.
  • Enable inotify monitoring for real-time detection: LMD supports inotifywait for instant file change tracking — set inotify_monitor="1" in the config. It uses more resources, but catches shells the moment they’re uploaded.
  • Set up log rotation for maldet-cron.log: Nightly scans accumulate log data faster than you’d expect — especially when scanning directories with many small files. Add an entry to /etc/logrotate.d/ to rotate it automatically.
  • Test email alerts right after setup: Don’t wait for an incident to discover your email isn’t working. Run a test scan with the EICAR file and confirm the alert arrives before you hand things off to automation.

This whole setup takes about 20 minutes — including the initial freshclam signature update. For any server that accepts user file uploads, this isn’t optional, it’s mandatory. I learned that the hard way.

Share: