Clam AV - Hardening
I am in the process of ditching Microsoft Windows, it's not that I don't like it - I'm neutral on that front but I am moving to Linux. There are a number of reasons
- Privacy - Microsoft Windows is noisy as hell - Linux is not
- I need to become instinctive on what I am doing with Linux - making it my daily will help
- Linux is transparent or I believe so - I will leave it at that.
- Hardware longevity - replacing a laptop every 5 to 7 years is bloody wasteful
As such I don't believe the "why do you need antivirus on Linux" line. The fact that it runs nearly all the worlds' servers and has a growing footprint in desktops and laptops means it will eventually gain somebodies attention with malicious intent - if it hasn't already done so. I mean derivatives of it run routers, just about every IOT device, you name it.
So, I went with the standard solution for Linux antivirus protection.
I have gotten around to hardening it iaw the recommendations of the AI (the smarter of the 5 I have access to - yes it costs - but I learn heaps - yes, it's smarter than me). Yes, I have been writing a lot of code lately in addition to fixing my servers. Learn something new, code rewrite, find out your assumptions are wrong, code rewrite. It's never ending but I love it.
The following script is its recommendations. I have shared it to give anyone who reads this some ideas. I DONT say it's right but it's better than the base configuration.
So here it is - Updated 17-12-2025 - complete overhaul
#!/bin/bash
# setup-clamav.sh
# Harden ClamAV configuration for Debian/Mint systems
# Idempotent: safely re-runnable, only applies changes if missing
# Audit-style comments included for compliance traceability
# Root check included: must be run as root
# Supports --dry-run mode for safe validation
# ./setup-clamav.sh --dry-run
set -euo pipefail
# --- Output helpers ---
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[0;33m[WARN]\033[0m $*" >&2; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
# --- Root privilege check ---
if [[ "${EUID}" -ne 0 ]]; then
error "This script must be run as root."
exit 1
fi
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
info "Dry-run mode enabled: no changes will be applied."
fi
CLAMD_CONF="/etc/clamav/clamd.conf"
FRESHCLAM_CONF="/etc/clamav/freshclam.conf"
LOGFILE="/var/log/clamav/harden_clamav.log"
CRON_FILE="/etc/cron.d/clamav-freshclam"
# Ensure logfile exists
mkdir -p "$(dirname "$LOGFILE")"
touch "$LOGFILE"
# Function to ensure a config directive exists and is set correctly
ensure_config() {
local file="$1"
local key="$2"
local value="$3"
if grep -qE "^[[:space:]]*$key[[:space:]]" "$file"; then
if $DRY_RUN; then
info "Would update $key in $file to $value"
else
sed -i "s|^[[:space:]]*$key[[:space:]].*|$key $value|" "$file"
info "Updated $key in $file to $value"
fi
else
if $DRY_RUN; then
info "Would add $key $value to $file"
else
echo "$key $value" >> "$file"
info "Added $key $value to $file"
fi
fi
}
info "=== Hardening ClamAV configuration ==="
# --- Socket permissions ---
ensure_config "$CLAMD_CONF" "LocalSocketMode" "660"
ensure_config "$CLAMD_CONF" "LocalSocketGroup" "clamav"
ensure_config "$CLAMD_CONF" "PidFile" "/var/run/clamav/clamd.pid"
# --- Logging ---
ensure_config "$CLAMD_CONF" "LogFile" "/var/log/clamav/clamav.log"
ensure_config "$CLAMD_CONF" "LogFileMaxSize" "52428800" # 50 MB
ensure_config "$CLAMD_CONF" "LogTime" "yes"
ensure_config "$CLAMD_CONF" "LogVerbose" "yes"
ensure_config "$CLAMD_CONF" "LogSyslog" "yes"
ensure_config "$CLAMD_CONF" "LogFacility" "LOG_LOCAL6"
ensure_config "$CLAMD_CONF" "LogRotate" "yes"
# --- Exclusions ---
for path in "/proc/" "/sys/" "/dev/" "/run/" "/var/lib/docker/" "/var/lib/containers/" "/var/lib/mysql/" "/var/lib/postgresql/" "/var/cache/apt/archives/" "/var/infected/"; do
if ! grep -q "ExcludePath $path" "$CLAMD_CONF"; then
if $DRY_RUN; then
info "Would add ExcludePath $path"
else
echo "ExcludePath $path" >> "$CLAMD_CONF"
info "Added ExcludePath $path"
fi
fi
done
# --- Alerts ---
for key in AlertEncryptedArchive AlertEncryptedDoc AlertOLE2Macros AlertPhishingSSLMismatch AlertPhishingCloak; do
ensure_config "$CLAMD_CONF" "$key" "yes"
done
# --- Database hygiene ---
ensure_config "$CLAMD_CONF" "OfficialDatabaseOnly" "yes"
ensure_config "$CLAMD_CONF" "FailIfCvdOlderThan" "7"
ensure_config "$CLAMD_CONF" "ConcurrentDatabaseReload" "yes"
ensure_config "$CLAMD_CONF" "SelfCheck" "3600"
# --- Detection refinements ---
ensure_config "$CLAMD_CONF" "AlgorithmicDetection" "yes"
ensure_config "$CLAMD_CONF" "DetectPUA" "yes"
# --- File & Scan Limits ---
ensure_config "$CLAMD_CONF" "MaxScanSize" "104857600" # 100 MB
ensure_config "$CLAMD_CONF" "MaxFileSize" "26214400" # 25 MB
ensure_config "$CLAMD_CONF" "MaxRecursion" "16"
ensure_config "$CLAMD_CONF" "MaxFiles" "10000"
ensure_config "$CLAMD_CONF" "OnAccessMaxFileSize" "10485760" # 10 MB
# --- On-Access Scanning (workstation protection) ---
ensure_config "$CLAMD_CONF" "OnAccessIncludePath" "/home"
for path in "/proc" "/sys" "/dev" "/run"; do
if ! grep -q "OnAccessExcludePath $path" "$CLAMD_CONF"; then
if $DRY_RUN; then
info "Would add OnAccessExcludePath $path"
else
echo "OnAccessExcludePath $path" >> "$CLAMD_CONF"
info "Added OnAccessExcludePath $path"
fi
fi
done
# --- Freshclam checks ---
ensure_config "$FRESHCLAM_CONF" "Checks" "12" # every ~2 hours; cron triggers at 6-hour cadence
# --- Cron job for signature updates (atomic cron.d drop-in) ---
CRON_CONTENT="# Managed by setup-clamav.sh
SHELL=/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin
# ClamAV signature updates (every 6 hours)
0 */6 * * * root /usr/bin/freshclam --quiet
"
if $DRY_RUN; then
info "Would write cron file at $CRON_FILE with 6-hour schedule"
else
printf "%s" "$CRON_CONTENT" > "$CRON_FILE"
chmod 0644 "$CRON_FILE"
chown root:root "$CRON_FILE"
info "Installed cron schedule in $CRON_FILE (every 6 hours)"
fi
# --- Resolve systemd vs cron conflict for freshclam ---
FRESHCLAM_UNIT="clamav-freshclam.service"
if [[ -f "$CRON_FILE" ]]; then
if $DRY_RUN; then
info "Would disable and stop $FRESHCLAM_UNIT because cron file exists ($CRON_FILE)"
else
if systemctl is-enabled --quiet "$FRESHCLAM_UNIT"; then
info "Disabling $FRESHCLAM_UNIT (cron manages updates)..."
systemctl disable --now "$FRESHCLAM_UNIT" || warn "Could not disable/stop $FRESHCLAM_UNIT (continuing)"
else
info "$FRESHCLAM_UNIT is already disabled; cron will manage updates."
# ensure it is not running
systemctl stop "$FRESHCLAM_UNIT" 2>/dev/null || true
fi
fi
fi
# --- Restart clamd only (freshclam handled by cron) ---
if $DRY_RUN; then
info "Dry-run complete: no services restarted."
else
info "=== Restarting clamd service ==="
if systemctl restart clamav-daemon.service; then
if systemctl is-active --quiet clamav-daemon.service; then
info "SUCCESS: clamd restarted and active"
else
error "clamd failed to start"
systemctl status clamav-daemon.service
exit 1
fi
else
error "Failed to restart clamd"
exit 1
fi
fi
info "=== Harden ClamAV script completed successfully ==="As you can see - it didn't change too much.
What’s Included
- Root check: prevents non‑root execution.
- PidFile enabled:
/var/run/clamav/clamd.pidfor monitoring. - LogFileMaxSize set to 50M: prevents runaway logs.
- DetectPUA enabled: catches adware/toolbars.
- OnAccessMaxFileSize bumped to 10 MB: broader coverage for downloads.
- OnAccessIncludePath
/home: protects user data. - Exclusions: system dirs, DBs, container storage, apt cache, quarantine.
- Alerts: encrypted archives/docs, macros, phishing mismatches/cloaks.
- Database hygiene: official DB only, fail if older than 7 days.
- Service restart & verification: ensures daemon and updater are active.
- Audit log:
/var/log/clamav/harden_clamav.logrecords all changes.
Unlike Windows Professional and above you don't have to turn ASR on and a lot of services off with or without caveats. Like I said transparent.
Next job testing the SSH key reset/rotation script that the AI and I rewrote last night whilst I should have been asleep. It's much better to break your laptop than your server 😄
One massive learning on moving to Linux is none of the major cloud storage players provide Linux code to access their storage servers - which includes Microsoft, Google, Proton to name 3 - I mean what the hell.
To be honest for most people Linux would be a much safer option.
On a side note - the docker version 29 saga continues - I cut my losses but...
https://github.com/portainer/portainer/issues/12925#issuecomment-3540552277
But what the hell do I know.
#enoughsaid