Build a production-grade ClamAV cluster with HAProxy load balancing, shared virus definitions, and comprehensive monitoring for enterprise-scale threat detection and high availability.
Prerequisites
- Root or sudo access
- Multiple servers (minimum 3 nodes)
- NFS or shared storage system
- Basic understanding of load balancing
- Network connectivity between cluster nodes
What this solves
Enterprise environments require robust antivirus scanning that can handle high volumes of files without creating single points of failure. A ClamAV cluster provides distributed scanning capabilities, automatic failover, and scalable threat detection across multiple nodes.
This setup ensures continuous malware protection even when individual scanner nodes fail, while distributing the computational load across multiple servers for improved performance and reliability.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you get the latest versions of ClamAV and dependencies.
sudo apt update && sudo apt upgrade -y
Install ClamAV and HAProxy
Install ClamAV daemon, scanner tools, and HAProxy for load balancing across all cluster nodes.
sudo apt install -y clamav clamav-daemon clamav-freshclam haproxy rsync nfs-common
Create ClamAV system users and directories
Set up dedicated users and secure directory structure for the ClamAV cluster. Each node needs consistent user permissions for shared storage access.
sudo useradd -r -s /bin/false -d /var/lib/clamav clamav
sudo mkdir -p /var/lib/clamav/shared
sudo mkdir -p /var/log/clamav
sudo mkdir -p /run/clamav
sudo chown -R clamav:clamav /var/lib/clamav /var/log/clamav /run/clamav
sudo chmod 755 /var/lib/clamav /var/log/clamav
sudo chmod 750 /run/clamav
Configure shared virus definition storage
Set up NFS or shared storage for virus definitions. This ensures all cluster nodes use the same signature database and reduces update overhead.
sudo mkdir -p /mnt/clamav-shared
sudo chown clamav:clamav /mnt/clamav-shared
sudo chmod 755 /mnt/clamav-shared
Add the shared storage mount to fstab for automatic mounting:
203.0.113.10:/shared/clamav /mnt/clamav-shared nfs rw,hard,intr 0 0
sudo mount /mnt/clamav-shared
Configure ClamAV daemon for cluster operation
Configure each ClamAV node to use shared virus definitions and accept network connections for distributed scanning.
# Network configuration
TCPSocket 3310
TCPAddr 0.0.0.0
MaxConnectionQueueLength 100
MaxThreads 20
ReadTimeout 300
CommandReadTimeout 30
SendBufTimeout 500
MaxQueue 500
Database configuration
DatabaseDirectory /mnt/clamav-shared/db
TemporaryDirectory /tmp
DatabaseOwner clamav
Logging
LogFile /var/log/clamav/clamav.log
LogTime yes
LogClean no
LogSyslog yes
LogFacility LOG_DAEMON
LogVerbose yes
Security settings
User clamav
AllowAllMatchScan yes
Foreground no
PidFile /run/clamav/clamd.pid
LocalSocket /run/clamav/clamd.ctl
LocalSocketGroup clamav
LocalSocketMode 666
Performance tuning
MaxScanSize 500M
MaxFileSize 100M
MaxRecursion 20
MaxFiles 15000
MaxEmbeddedPE 40M
MaxHTMLNormalize 40M
MaxHTMLNoTags 8M
MaxScriptNormalize 5M
MaxZipTypeRcg 1M
Archive scanning
ScanArchive yes
ScanPDF yes
ScanSWF yes
ScanXMLDOCS yes
ScanHWP3 yes
ScanOLE2 yes
ScanPE yes
ScanELF yes
ScanMail yes
ScanPartialMessages yes
ScanHTML yes
Exclude common false positives
ExcludePath ^/proc/
ExcludePath ^/sys/
ExcludePath ^/dev/
ExcludePath ^/run/user/
Configure FreshClam for shared updates
Set up virus definition updates to use shared storage. Only one node should update definitions to prevent conflicts.
# Database directory
DatabaseDirectory /mnt/clamav-shared/db
DatabaseOwner clamav
Update settings
DNSDatabaseInfo current.cvd.clamav.net
DatabaseMirror db.local.clamav.net
DatabaseMirror database.clamav.net
Logging
UpdateLogFile /var/log/clamav/freshclam.log
LogFileMaxSize 20M
LogTime yes
LogVerbose yes
LogSyslog yes
LogFacility LOG_DAEMON
Network settings
ReceiveTimeout 60
TestDatabases yes
ScriptedUpdates yes
CompressLocalDatabase no
Security
Bytecode yes
Primary update node configuration (only on one node)
OnUpdateExecute /usr/bin/systemctl reload clamav-daemon
Secondary nodes should not update
Comment out the update mirrors on secondary nodes
and rely on shared storage synchronization
Create database synchronization script
Create a script for secondary nodes to synchronize with the primary update node and reload the daemon when definitions change.
#!/bin/bash
ClamAV Database Synchronization Script
Run on secondary nodes to sync with primary
PRIMARY_NODE="203.0.113.10"
SHARED_DB_PATH="/mnt/clamav-shared/db"
LOCAL_DB_PATH="/var/lib/clamav"
LOG_FILE="/var/log/clamav/sync.log"
Create log entry
echo "$(date): Starting ClamAV database sync" >> $LOG_FILE
Check if shared storage is mounted
if ! mountpoint -q /mnt/clamav-shared; then
echo "$(date): ERROR - Shared storage not mounted" >> $LOG_FILE
exit 1
fi
Check for database updates
if [ -f "$SHARED_DB_PATH/main.cvd" ] && [ -f "$SHARED_DB_PATH/daily.cvd" ]; then
# Compare timestamps
SHARED_TIME=$(stat -c %Y "$SHARED_DB_PATH/main.cvd")
LOCAL_TIME=0
if [ -f "$LOCAL_DB_PATH/main.cvd" ]; then
LOCAL_TIME=$(stat -c %Y "$LOCAL_DB_PATH/main.cvd")
fi
if [ $SHARED_TIME -gt $LOCAL_TIME ]; then
echo "$(date): Updating local database from shared storage" >> $LOG_FILE
# Stop daemon during update
systemctl stop clamav-daemon
# Sync databases
rsync -av --delete "$SHARED_DB_PATH/" "$LOCAL_DB_PATH/"
chown -R clamav:clamav "$LOCAL_DB_PATH"
# Restart daemon
systemctl start clamav-daemon
echo "$(date): Database sync completed successfully" >> $LOG_FILE
else
echo "$(date): Local database is up to date" >> $LOG_FILE
fi
else
echo "$(date): ERROR - Shared databases not found" >> $LOG_FILE
fi
sudo chmod 755 /usr/local/bin/clamav-sync.sh
sudo chown root:root /usr/local/bin/clamav-sync.sh
Configure HAProxy load balancer
Set up HAProxy to distribute scanning requests across cluster nodes with health checks and automatic failover.
global
daemon
user haproxy
group haproxy
pidfile /var/run/haproxy.pid
log stdout local0 info
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
# SSL/TLS settings
ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
defaults
mode tcp
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
log global
option tcplog
option dontlognull
retries 3
option redispatch
maxconn 2000
Statistics interface
frontend stats
bind *:8404
mode http
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
stats auth admin:clamav-cluster-2024
ClamAV cluster frontend
frontend clamav-cluster
bind *:3310
mode tcp
option tcplog
timeout client 300000ms
default_backend clamav-nodes
ClamAV backend nodes
backend clamav-nodes
mode tcp
balance roundrobin
timeout server 300000ms
timeout connect 10000ms
# Health check configuration
option tcp-check
tcp-check send "PING\n"
tcp-check expect string "PONG"
# Cluster nodes
server clamav-node1 203.0.113.11:3310 check inter 10s rise 3 fall 3 maxconn 50
server clamav-node2 203.0.113.12:3310 check inter 10s rise 3 fall 3 maxconn 50
server clamav-node3 203.0.113.13:3310 check inter 10s rise 3 fall 3 maxconn 50
Create systemd services and timers
Set up systemd services for automatic database synchronization and cluster health monitoring.
[Unit]
Description=ClamAV Database Synchronization
After=network.target
Requires=network.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/clamav-sync.sh
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run ClamAV database sync every 30 minutes
Requires=clamav-sync.service
[Timer]
OnCalendar=*:0/30
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable clamav-sync.timer
sudo systemctl start clamav-sync.timer
Configure firewall rules
Open necessary ports for ClamAV cluster communication and HAProxy management interface.
sudo ufw allow 3310/tcp comment "ClamAV daemon"
sudo ufw allow 8404/tcp comment "HAProxy stats"
sudo ufw allow from 203.0.113.0/24 to any port 2049 comment "NFS for cluster"
sudo ufw reload
Initialize virus definitions
Download initial virus definitions on the primary node and set up the shared database structure.
sudo mkdir -p /mnt/clamav-shared/db
sudo chown -R clamav:clamav /mnt/clamav-shared/db
sudo -u clamav freshclam --config-file=/etc/clamav/freshclam.conf
Start and enable services
Enable and start ClamAV daemon and HAProxy on all cluster nodes.
sudo systemctl enable clamav-daemon clamav-freshclam haproxy
sudo systemctl start clamav-daemon clamav-freshclam haproxy
sudo systemctl status clamav-daemon haproxy
Configure monitoring and alerting
Create cluster health check script
Set up monitoring script to check cluster node health and send alerts when nodes become unavailable.
#!/bin/bash
ClamAV Cluster Health Monitoring Script
CLUSTER_NODES=("203.0.113.11" "203.0.113.12" "203.0.113.13")
LOG_FILE="/var/log/clamav/cluster-health.log"
ALERT_EMAIL="admin@example.com"
MIN_HEALTHY_NODES=2
healthy_nodes=0
failed_nodes=()
echo "$(date): Starting cluster health check" >> $LOG_FILE
for node in "${CLUSTER_NODES[@]}"; do
if timeout 10 echo "PING" | nc -w 5 $node 3310 | grep -q "PONG"; then
echo "$(date): Node $node is healthy" >> $LOG_FILE
((healthy_nodes++))
else
echo "$(date): Node $node is DOWN" >> $LOG_FILE
failed_nodes+=("$node")
fi
done
echo "$(date): Healthy nodes: $healthy_nodes/${#CLUSTER_NODES[@]}" >> $LOG_FILE
if [ $healthy_nodes -lt $MIN_HEALTHY_NODES ]; then
ALERT_MSG="CRITICAL: ClamAV cluster has only $healthy_nodes healthy nodes out of ${#CLUSTER_NODES[@]}. Failed nodes: ${failed_nodes[*]}"
echo "$(date): $ALERT_MSG" >> $LOG_FILE
echo "$ALERT_MSG" | mail -s "ClamAV Cluster Alert" $ALERT_EMAIL
fi
sudo chmod 755 /usr/local/bin/clamav-monitor.sh
Set up log rotation
Configure log rotation to prevent cluster logs from consuming excessive disk space.
/var/log/clamav/*.log {
weekly
rotate 12
compress
delaycompress
missingok
notifempty
postrotate
/bin/systemctl reload clamav-daemon > /dev/null 2>&1 || true
endscript
}
Verify your setup
Test the ClamAV cluster configuration and verify load balancing is working correctly.
# Check cluster node status
sudo systemctl status clamav-daemon haproxy
Test direct node connectivity
echo "PING" | nc -w 5 203.0.113.11 3310
echo "PING" | nc -w 5 203.0.113.12 3310
Test load balancer
echo "PING" | nc -w 5 localhost 3310
Check HAProxy statistics
curl -u admin:clamav-cluster-2024 http://localhost:8404/stats
Test file scanning through cluster
echo "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" > /tmp/eicar.txt
clamdscan --fdpass /tmp/eicar.txt
Monitor cluster health
sudo /usr/local/bin/clamav-monitor.sh
Check shared database synchronization
ls -la /mnt/clamav-shared/db/
sudo systemctl status clamav-sync.timer
You can also integrate this ClamAV cluster with your existing monitoring infrastructure using techniques from our Linux system monitoring tutorial or set up centralized logging with ELK Stack configuration.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Nodes can't access shared storage | NFS mount permission issues | sudo mount -t nfs -o rw,hard,intr server:/path /mnt/clamav-shared |
| ClamAV daemon fails to start | Database directory permissions | sudo chown -R clamav:clamav /var/lib/clamav and sudo chmod 755 /var/lib/clamav |
| HAProxy shows nodes as down | Health check timeout | Increase timeout values in haproxy.cfg backend section |
| Database sync fails | Network connectivity issues | Check NFS connectivity and firewall rules for port 2049 |
| High memory usage on nodes | Too many concurrent scans | Reduce MaxThreads in clamd.conf and adjust HAProxy maxconn |
| Virus definitions out of date | FreshClam update failures | Check network connectivity and DNS resolution for clamav.net |
Next steps
- Configure ClamAV antivirus scanning with automated threat detection
- Monitor HAProxy and Consul with Prometheus and Grafana
- Deploy ClamAV cluster on Kubernetes with persistent volumes
- Integrate ClamAV cluster with web application file uploads
- Configure SSL encryption and authentication for ClamAV cluster
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
SCRIPT_NAME=$(basename "$0")
NFS_SERVER=""
NODE_TYPE=""
TOTAL_STEPS=12
usage() {
echo "Usage: $SCRIPT_NAME --nfs-server <IP> --node-type <primary|secondary>"
echo " --nfs-server: IP address of NFS server for shared virus definitions"
echo " --node-type: primary (handles updates) or secondary (read-only)"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
cleanup() {
if [[ $? -ne 0 ]]; then
error "Installation failed. Cleaning up..."
systemctl stop clamav-daemon 2>/dev/null || true
systemctl stop haproxy 2>/dev/null || true
umount /mnt/clamav-shared 2>/dev/null || true
fi
}
trap cleanup ERR
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--nfs-server)
NFS_SERVER="$2"
shift 2
;;
--node-type)
NODE_TYPE="$2"
shift 2
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
usage
;;
esac
done
if [[ -z "$NFS_SERVER" || -z "$NODE_TYPE" ]]; then
usage
fi
if [[ "$NODE_TYPE" != "primary" && "$NODE_TYPE" != "secondary" ]]; then
error "Node type must be 'primary' or 'secondary'"
exit 1
fi
# Check prerequisites
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
exit 1
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
CLAMAV_SERVICE="clamav-daemon"
FRESHCLAM_SERVICE="clamav-freshclam"
CLAMD_CONF="/etc/clamav/clamd.conf"
FRESHCLAM_CONF="/etc/clamav/freshclam.conf"
NFS_PKG="nfs-common"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
CLAMAV_SERVICE="clamd@scan"
FRESHCLAM_SERVICE="clamav-freshclam"
CLAMD_CONF="/etc/clamd.d/scan.conf"
FRESHCLAM_CONF="/etc/freshclam.conf"
NFS_PKG="nfs-utils"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
CLAMAV_SERVICE="clamd@scan"
FRESHCLAM_SERVICE="clamav-freshclam"
CLAMD_CONF="/etc/clamd.d/scan.conf"
FRESHCLAM_CONF="/etc/freshclam.conf"
NFS_PKG="nfs-utils"
;;
*)
error "Unsupported distribution: $ID"
exit 1
;;
esac
else
error "Cannot detect distribution"
exit 1
fi
log "[1/$TOTAL_STEPS] Updating system packages..."
$PKG_UPDATE
log "[2/$TOTAL_STEPS] Installing ClamAV and dependencies..."
$PKG_INSTALL clamav clamav-server clamav-update haproxy rsync $NFS_PKG
log "[3/$TOTAL_STEPS] Creating ClamAV system user and directories..."
useradd -r -s /bin/false -d /var/lib/clamav clamav 2>/dev/null || true
mkdir -p /var/lib/clamav/shared /var/log/clamav /run/clamav
chown -R clamav:clamav /var/lib/clamav /var/log/clamav /run/clamav
chmod 755 /var/lib/clamav /var/log/clamav
chmod 750 /run/clamav
log "[4/$TOTAL_STEPS] Setting up shared storage mount point..."
mkdir -p /mnt/clamav-shared
chown clamav:clamav /mnt/clamav-shared
chmod 755 /mnt/clamav-shared
echo "$NFS_SERVER:/shared/clamav /mnt/clamav-shared nfs rw,hard,intr 0 0" >> /etc/fstab
mount /mnt/clamav-shared
log "[5/$TOTAL_STEPS] Creating virus definition directory..."
mkdir -p /mnt/clamav-shared/db
chown -R clamav:clamav /mnt/clamav-shared/db
chmod 755 /mnt/clamav-shared/db
log "[6/$TOTAL_STEPS] Configuring ClamAV daemon..."
cat > "$CLAMD_CONF" << 'EOF'
TCPSocket 3310
TCPAddr 0.0.0.0
MaxConnectionQueueLength 100
MaxThreads 20
ReadTimeout 300
CommandReadTimeout 30
SendBufTimeout 500
MaxQueue 500
DatabaseDirectory /mnt/clamav-shared/db
TemporaryDirectory /tmp
DatabaseOwner clamav
LogFile /var/log/clamav/clamav.log
LogTime yes
LogClean no
LogSyslog yes
LogFacility LOG_DAEMON
LogVerbose yes
User clamav
AllowAllMatchScan yes
Foreground no
PidFile /run/clamav/clamd.pid
LocalSocket /run/clamav/clamd.ctl
LocalSocketGroup clamav
LocalSocketMode 644
MaxScanSize 500M
MaxFileSize 100M
MaxRecursion 20
MaxFiles 15000
MaxEmbeddedPE 40M
MaxHTMLNormalize 40M
MaxHTMLNoTags 8M
MaxScriptNormalize 5M
MaxZipTypeRcg 1M
ScanArchive yes
ScanPDF yes
ScanSWF yes
ScanXMLDOCS yes
ScanHWP3 yes
ScanOLE2 yes
ScanPE yes
ScanELF yes
ScanMail yes
ScanPartialMessages yes
ScanHTML yes
ExcludePath ^/proc/
ExcludePath ^/sys/
ExcludePath ^/dev/
ExcludePath ^/run/user/
EOF
log "[7/$TOTAL_STEPS] Configuring FreshClam..."
cat > "$FRESHCLAM_CONF" << EOF
DatabaseDirectory /mnt/clamav-shared/db
DatabaseOwner clamav
DNSDatabaseInfo current.cvd.clamav.net
DatabaseMirror database.clamav.net
UpdateLogFile /var/log/clamav/freshclam.log
LogFileMaxSize 20M
LogTime yes
LogVerbose yes
LogSyslog yes
LogFacility LOG_DAEMON
ReceiveTimeout 60
TestDatabases yes
ScriptedUpdates yes
CompressLocalDatabase no
Bytecode yes
EOF
if [[ "$NODE_TYPE" == "primary" ]]; then
echo "OnUpdateExecute /usr/bin/systemctl reload $CLAMAV_SERVICE" >> "$FRESHCLAM_CONF"
fi
log "[8/$TOTAL_STEPS] Configuring HAProxy load balancer..."
cat > /etc/haproxy/haproxy.cfg << 'EOF'
global
daemon
chroot /var/lib/haproxy
user haproxy
group haproxy
stats socket /var/run/haproxy.sock mode 644 level admin
defaults
mode tcp
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend clamav_frontend
bind *:3310
default_backend clamav_backend
backend clamav_backend
balance roundrobin
option tcp-check
tcp-check connect port 3310
server clamav_local 127.0.0.1:3310 check
EOF
log "[9/$TOTAL_STEPS] Setting file permissions..."
chmod 644 "$CLAMD_CONF" "$FRESHCLAM_CONF" /etc/haproxy/haproxy.cfg
log "[10/$TOTAL_STEPS] Configuring firewall..."
if command -v ufw >/dev/null 2>&1; then
ufw allow 3310/tcp
ufw --force enable
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-port=3310/tcp
firewall-cmd --reload
fi
log "[11/$TOTAL_STEPS] Starting services..."
systemctl enable $CLAMAV_SERVICE haproxy
if [[ "$NODE_TYPE" == "primary" ]]; then
systemctl enable $FRESHCLAM_SERVICE
systemctl start $FRESHCLAM_SERVICE
fi
systemctl start $CLAMAV_SERVICE haproxy
log "[12/$TOTAL_STEPS] Verifying installation..."
sleep 5
if systemctl is-active --quiet $CLAMAV_SERVICE; then
log "✓ ClamAV daemon is running"
else
error "✗ ClamAV daemon failed to start"
exit 1
fi
if systemctl is-active --quiet haproxy; then
log "✓ HAProxy is running"
else
error "✗ HAProxy failed to start"
exit 1
fi
if [[ "$NODE_TYPE" == "primary" ]] && systemctl is-active --quiet $FRESHCLAM_SERVICE; then
log "✓ FreshClam service is running (primary node)"
fi
if netstat -ln | grep -q ":3310"; then
log "✓ ClamAV is listening on port 3310"
else
error "✗ ClamAV is not listening on port 3310"
exit 1
fi
log "ClamAV cluster node ($NODE_TYPE) installation completed successfully!"
log "You can test the installation with: echo 'test' | nc localhost 3310"
Review the script before running. Execute with: bash install.sh