Clam AV - Hardening

Clam AV - Hardening
Linux

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

  1. Privacy - Microsoft Windows is noisy as hell - Linux is not
  2. I need to become instinctive on what I am doing with Linux - making it my daily will help
  3. Linux is transparent or I believe so - I will leave it at that.
  4. 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.pid for 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.log records 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