ClamAV - Hardening version 4
Update the ClamAV file to make it work for the following:
- Debian, MintOS LMDE and Bazzite
- Fix the email notification on service failure problem
- Fixed the BazziteOS ClamAV Problem, that was seriously tough.
Tests run on script
- Script on MintOS LMDE 7 - no errors
- Script run on Bazzite OS - no errors
- Script run on stagnant MintOS LMDE 7 - no errors
ClamAV Hardening Script
Find below the hardening script. Please read the script summary before running.
Please note if you installed ClamAV on the Bazzite system using the old method, remove it as it was ugly and failed. This configuration script will work with the adjusted installation script in the other post.
See here
BazziteOS - Post Script
The folllowing script is now operational. IAW our ditching of the Microsoft eco system and the problems with MintOS in gaming we are moving various systems to BazziteOS. So far we have not been disappointed The script is now production ie.. live. I have found now major issues with it.

#!/bin/bash
# -----------------------------------------------------------------------------
# setup-clamav.sh
# Harden ClamAV configuration for Debian/LMDE and Bazzite OS systems
# -----------------------------------------------------------------------------
#
# PURPOSE
# Applies a hardened, best-practice ClamAV configuration focused on
# real-time workstation protection, home directory defence, and
# scheduled full-system sweeps.
#
# Supports two distinct operating system families:
#
# DEBIAN / LMDE (debian)
# Full daemon-based setup: clamd + freshclam as systemd services,
# on-access scanning via fanotify, quarantine, Sanesecurity +
# URLhaus third-party signature databases.
# Packages: clamav-daemon, clamav-freshclam (via apt)
# Config: /etc/clamav/clamd.conf, /etc/clamav/freshclam.conf
#
# BAZZITE OS (bazzite)
# Immutable Fedora Atomic base -- packages installed via rpm-ostree
# layering before this script is run (requires reboot after layering).
# Packages: clamav, clamav-freshclam, clamd (via rpm-ostree)
# Config: /etc/clamd.d/scan.conf, /etc/freshclam.conf
# Users: clamd daemon runs as 'clamscan' (group: clamscan, virusgroup)
# freshclam runs as 'clamupdate'
#
# BEHAVIOUR
# - Auto-detects OS from /etc/os-release (ID and ID_LIKE fields)
# - Self-contained: resets config files to a known-good baseline before
# applying settings -- safe for first run and re-runs alike
# - Idempotent: safely re-runnable
# - Supports --dry-run mode: validates what would change without touching
# any files or services -> ./setup-clamav.sh --dry-run
# - Both modes must be run as root
#
# ON-ACCESS MODE (Bazzite)
# DETECTION + QUARANTINE, not blocking. OnAccessPrevention is set to 'no'
# so file opens are NOT intercepted/denied at the kernel level. Instead
# clamonacc scans on access and moves any detected file to quarantine via
# its --move flag (installed as a systemd drop-in). This deliberately
# avoids fanotify permission-event (blocking) mode, which is the
# SELinux-heavy path on Fedora Atomic.
#
# UPDATE STRATEGY (Debian)
# Signature updates managed by clamav-freshclam systemd service -- NOT cron.
# This avoids log-lock conflicts caused by multiple freshclam processes.
#
# UPDATE STRATEGY (Bazzite)
# clamav-freshclam.service manages signature updates (6 checks/day = every
# 4 hours). The service is stopped before any manual freshclam run to
# prevent log-lock conflicts on re-runs.
#
# PRE-REQUISITES (Debian)
# - clamav-daemon, clamav-freshclam installed
# - Kernel with fanotify support (Linux Mint 20+ / kernel 5.4+)
# - Local MTA (Postfix) installed and configured for email notifications
#
# PRE-REQUISITES (Bazzite)
# - Packages layered via rpm-ostree in setup-laptop-bazzite.sh:
# clamav clamav-freshclam clamd
# - System REBOOTED after rpm-ostree layering before running this script
# - Run as root
#
# PROTECTION COVERAGE (Debian)
# Real-time (on-access, BLOCKING): /home /tmp /var/tmp /root
# Quarantine destination: /var/quarantine/clamav (mode 0700)
# Signature updates: Every 4 hours via freshclam systemd service
# Install log: /var/log/clamav/harden_clamav.log
#
# PROTECTION COVERAGE (Bazzite)
# On-access (DETECTION + quarantine): /home /tmp /var/tmp /root
# Quarantine destination: /var/quarantine/clamav (mode 0700)
# On-access scanner: clamav-clamonacc.service (fanotify via clamd)
# On-access action: clamonacc --move (scan + quarantine, no block)
# Signature updates: Every 4 hours via clamav-freshclam.service
#
# FEDORA/BAZZITE USER MODEL
# clamd daemon: runs as user 'clamscan', group 'clamscan' (also 'virusgroup')
# freshclam: runs as user 'clamupdate', group 'clamupdate'
# clamonacc: runs as 'root' (requires CAP_SYS_ADMIN for fanotify)
# /var/log/clamav/: clamscan:clamscan 0750
# /var/log/clamav/clamd.log: clamscan:clamscan 0640
# /var/log/clamav/freshclam.log: clamupdate:clamupdate 0640
# /var/log/clamav/clamonacc.log: root:root 0640
# /var/quarantine/clamav/: root:root 0700
#
# SELINUX (Bazzite)
# Bazzite runs SELinux in enforcing mode. Hand-created files under
# /var/log/clamav get an incorrect SELinux context, which causes clamd to
# fail with "permission denied" on its log file DESPITE correct Unix
# ownership. This script runs restorecon on all created paths and sets the
# antivirus_can_scan_system boolean. If clamonacc cannot move files into
# /var/quarantine/clamav (a non-standard labelled path), check the denial
# with: sudo ausearch -m AVC -ts recent
#
# Version: 4.6.1
# Updated: 17-06-2026
#
# Change Log:
# v4.6.1 -- 17-06-2026 (Bazzite -- all fixes verified live on Bazzite/F44)
# - FIXED (Bazzite): freshclam "Permission denied" on freshclam.log. The log
# dir was clamscan:clamscan 0750, which the clamupdate user (freshclam's
# DatabaseOwner) could not traverse. Now set to clamscan:virusgroup with
# mode 2770 (setgid) so all three ClamAV users share the dir and new logs
# inherit virusgroup. Both clamscan and clamupdate are already members of
# virusgroup on Fedora.
# - FIXED (Bazzite): clamd@scan.service "Start request repeated too quickly".
# clamd itself was fine; systemd's restart rate-limiter latched after rapid
# retries and masked the real (already-resolved) state. Added
# 'systemctl reset-failed clamd@scan.service' before starting it.
# - FIXED (Bazzite): On-access watched /home, but Bazzite's real home is
# /var/home (/home is a symlink fanotify does not follow). Watch path is
# now /var/home.
# - FIXED (Bazzite): clamonacc refused to start -- it requires an
# OnAccessExclude* directive and reads on-access config via clamd, where
# OnAccessExcludeUID alone did not satisfy it. Added
# 'OnAccessExcludeUname clamscan' (the clamd daemon user on Fedora) to
# scan.conf, which clamonacc accepts.
# - FIXED (Bazzite): on-access detected files but could not scan them --
# "File path check failure: Permission denied" -- because clamonacc handed
# the daemon a path the clamscan user could not open (e.g. private Insync
# dirs). Added --fdpass to the clamonacc ExecStart so the open file
# descriptor is passed to the daemon instead. This is the fix that made
# on-access quarantine actually work (verified: EICAR dropped as a normal
# user was moved to /var/quarantine/clamav within seconds).
# - NOTE (Bazzite): clamonacc.log is noisy with transient sync-tool temp
# files (Insync .insyncdl, etc.). These are harmless scan races on files
# that vanish before scanning; --fdpass resolves the permission class of
# them. No action required.
#
# v4.6.0 -- 16-06-2026
# - CHANGED (Bazzite): On-access mode switched from BLOCKING to DETECTION +
# QUARANTINE. OnAccessPrevention is now 'no'. File opens are no longer
# intercepted; clamonacc scans on access and quarantines detections. This
# was a deliberate architecture decision to avoid fanotify permission-event
# (blocking) mode, which is the SELinux-heavy path on Fedora Atomic.
# - FIXED (Bazzite): Added SELinux remediation. After creating the log dir
# and files, the script now runs restorecon on /var/log/clamav,
# /run/clamd.scan and /var/quarantine/clamav, and sets the
# antivirus_can_scan_system boolean. This addresses the historical
# clamd@scan.service "permission denied on clamd.log despite correct
# ownership" failure, which was an SELinux label problem, not a Unix one.
# - FIXED (Bazzite): Quarantine for on-access is now actually wired up. The
# previous "MoveFileTo directive" comment referenced a non-existent clamd
# directive. Quarantine is performed by clamonacc's --move flag, installed
# via a systemd drop-in at
# /etc/systemd/system/clamav-clamonacc.service.d/override.conf. clamonacc
# also logs to /var/log/clamav/clamonacc.log.
# - CHANGED (Bazzite): Corrected the OnAccessExcludeUID comment (clamd runs
# as 'clamscan' on Fedora, not root). Updated all "BLOCKING" wording in
# comments and the summary box to "DETECTION + quarantine".
# - NOTE (Bazzite): On-access scanning excludes UID 0. Because this script
# runs as root, an EICAR file it creates is NOT scanned on access by
# design. Verify on-access quarantine as a normal user (UID 1000), per the
# guidance printed in the summary.
#
# v4.5.0 -- 06-06-2026
# - FIXED (Debian): Added 'User root' directive to clamd.conf. Required for
# fanotify/OnAccessPrevention to function. Debian ships clamav-daemon as
# User clamav; without the override on-access scanning silently failed to
# mount the fanotify watch, leaving real-time protection inactive.
# - FIXED (Debian): clamav-freshclam.service is now stopped before the manual
# freshclam run in Section 11 to release the log-file lock. On re-runs where
# the service was already active, freshclam failed with "Failed to lock the
# log file ... Resource temporarily unavailable" which blocked DB updates.
# Removed --quiet so initial download progress is visible.
# - FIXED (Debian): Added exec tee redirect so harden_clamav.log is actually
# written during install/re-runs. Log file ownership changed to root:adm
# (script runs as root; clamav:clamav ownership blocked the tee write).
# - REMOVED (Debian): unused SCAN_LOG / daily_scan.log definition -- there is
# no scheduled scan in this laptop script (on-access logs go to clamav.log).
# - CHANGED: Replaced all Unicode box-drawing and arrow characters with ASCII
# equivalents throughout both OS paths for clean terminal cut-and-paste.
# -----------------------------------------------------------------------------
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; }
section() { echo -e "\n\033[1;34m>>> $* \033[0m"; }
# -----------------------------------------------------------------------------
# OS DETECTION
# -----------------------------------------------------------------------------
detect_os() {
if [[ ! -f /etc/os-release ]]; then
error "/etc/os-release not found -- cannot detect OS."
exit 1
fi
local os_id os_id_like
os_id=$(grep -oP '(?<=^ID=)[^\n]+' /etc/os-release | tr -d '"' | tr '[:upper:]' '[:lower:]')
os_id_like=$(grep -oP '(?<=^ID_LIKE=)[^\n]+' /etc/os-release | tr -d '"' | tr '[:upper:]' '[:lower:]' || true)
if [[ "$os_id" == "bazzite" ]] || echo "$os_id_like" | grep -q "bazzite"; then
OS_FAMILY="bazzite"
elif [[ "$os_id" == "debian" || "$os_id" == "linuxmint" ]] || \
echo "$os_id_like" | grep -qE "debian|ubuntu"; then
OS_FAMILY="debian"
else
error "Unsupported OS: ID='$os_id' ID_LIKE='$os_id_like'"
error "This script supports Debian, Linux Mint (LMDE), and Bazzite OS only."
exit 1
fi
info "Detected OS family: $OS_FAMILY (ID=$os_id)"
}
detect_os
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
info "Dry-run mode enabled: no changes will be applied."
fi
# -----------------------------------------------------------------------------
# PRIVILEGE CHECK
# Both modes must be run as root.
# -----------------------------------------------------------------------------
if [[ "${EUID}" -ne 0 ]]; then
error "This script must be run as root."
exit 1
fi
# -----------------------------------------------------------------------------
# SHARED HELPERS
# -----------------------------------------------------------------------------
ensure_config() {
local file="$1" key="$2" value="$3"
if grep -qE "^[[:space:]]*${key}[[:space:]]" "$file"; then
if $DRY_RUN; then
info "[DRY] Would update: $key -> $value ($file)"
else
sed -i "s|^[[:space:]]*${key}[[:space:]].*|${key} ${value}|" "$file"
info "Updated: $key = $value"
fi
else
if $DRY_RUN; then
info "[DRY] Would add: $key $value ($file)"
else
echo "${key} ${value}" >> "$file"
info "Added: $key = $value"
fi
fi
}
ensure_multivalue() {
local file="$1" directive="$2" value="$3"
if ! grep -qF "${directive} ${value}" "$file"; then
if $DRY_RUN; then
info "[DRY] Would add: ${directive} ${value}"
else
echo "${directive} ${value}" >> "$file"
info "Added: ${directive} ${value}"
fi
else
info "Already present: ${directive} ${value}"
fi
}
# =============================================================================
# BAZZITE PATH
#
# ClamAV installed natively via rpm-ostree (clamav + clamav-freshclam + clamd).
# This script assumes packages are installed and system rebooted.
# Package installation is handled by setup-laptop-bazzite.sh.
#
# On-access mode: DETECTION + QUARANTINE (OnAccessPrevention no). clamonacc
# scans on access and moves detections to quarantine via --move. File opens
# are NOT blocked at the kernel level.
#
# Differences from Debian/LMDE:
# Packages: clamav clamav-freshclam clamd
# Config: /etc/clamd.d/scan.conf (not /etc/clamav/clamd.conf)
# /etc/freshclam.conf (not /etc/clamav/freshclam.conf)
# Socket: /run/clamd.scan/clamd.sock (tmpfiles.d managed)
# Users: clamd -> user 'clamscan', group 'clamscan' + 'virusgroup'
# freshclam -> user 'clamupdate', group 'clamupdate'
# clamonacc -> user 'root' (fanotify needs CAP_SYS_ADMIN)
# Units: clamav-freshclam.service -- signature updates
# clamd@scan.service -- clamd daemon (template unit)
# clamav-clamonacc.service -- on-access scanner (connects to clamd)
# All ship with the packages -- no custom units needed; we add a
# drop-in to clamav-clamonacc.service to enable --move quarantine.
# On-access: clamonacc connects to clamd via LocalSocket -- clamd must be
# running before clamonacc starts.
# SELinux: enforcing. restorecon is run on all created paths; the
# antivirus_can_scan_system boolean is set.
# Reset: No dpkg-reconfigure -- sed removes the Example line directly.
# =============================================================================
if [[ "$OS_FAMILY" == "bazzite" ]]; then
section "Bazzite OS -- Native ClamAV Setup (rpm-ostree)"
# -------------------------------------------------------------------------
# SECTION B0 -- Verify packages are installed
#
# All three packages must be present and active (i.e. the system has been
# rebooted after rpm-ostree layering in setup-laptop-bazzite.sh).
# -------------------------------------------------------------------------
section "Verifying ClamAV packages"
for pkg in clamav clamav-freshclam clamd; do
if ! rpm -q "$pkg" &>/dev/null; then
error "Package not installed: $pkg"
error "Run setup-laptop-bazzite.sh, reboot, then re-run this script."
error "Expected packages: clamav clamav-freshclam clamd"
exit 1
fi
info "Installed: $(rpm -q "$pkg")"
done
CLAMD_CONF="/etc/clamd.d/scan.conf"
FRESHCLAM_CONF="/etc/freshclam.conf"
QUARANTINE_DIR="/var/quarantine/clamav"
BACKUP_DIR="/etc/clamd.d/backups/$(date +%Y%m%d_%H%M%S)"
CLAMONACC_DROPIN_DIR="/etc/systemd/system/clamav-clamonacc.service.d"
CLAMONACC_DROPIN="${CLAMONACC_DROPIN_DIR}/override.conf"
# -------------------------------------------------------------------------
# SECTION B1 -- Quarantine directory
#
# Owned by root:root -- clamonacc (running as root) moves detected files
# here via its --move flag (configured in the drop-in in Section B9b).
# Mode 0700: only root can list/read quarantined files.
# -------------------------------------------------------------------------
section "Quarantine Directory"
if $DRY_RUN; then
info "[DRY] Would create $QUARANTINE_DIR (root:root, mode 0700)"
else
mkdir -p "$QUARANTINE_DIR"
chown root:root "$QUARANTINE_DIR"
chmod 0700 "$QUARANTINE_DIR"
info "Quarantine dir ready: $QUARANTINE_DIR"
fi
# -------------------------------------------------------------------------
# SECTION B2 -- Back up and reset config files
#
# Both files ship with an 'Example' line that disables all settings until
# removed. We back up originals then strip it for a clean baseline.
# Safe on re-runs: the Example line will already be absent, sed is a no-op.
# -------------------------------------------------------------------------
section "Backing up and resetting config files"
if $DRY_RUN; then
info "[DRY] Would back up $CLAMD_CONF and $FRESHCLAM_CONF to $BACKUP_DIR"
info "[DRY] Would remove Example line from both config files"
else
mkdir -p "$BACKUP_DIR"
[[ -f "$CLAMD_CONF" ]] && cp "$CLAMD_CONF" "${BACKUP_DIR}/scan.conf.bak" && info "Backed up: $CLAMD_CONF"
[[ -f "$FRESHCLAM_CONF" ]] && cp "$FRESHCLAM_CONF" "${BACKUP_DIR}/freshclam.conf.bak" && info "Backed up: $FRESHCLAM_CONF"
sed -i '/^Example/d' "$CLAMD_CONF" && info "Example line removed: $CLAMD_CONF"
sed -i '/^Example/d' "$FRESHCLAM_CONF" && info "Example line removed: $FRESHCLAM_CONF"
fi
# -------------------------------------------------------------------------
# SECTION B3 -- Log directory and files (+ SELinux remediation)
#
# Fedora user model:
# clamd runs as 'clamscan' -- owns clamd.log and the log directory.
# freshclam runs as 'clamupdate' -- owns freshclam.log.
# clamonacc runs as 'root' -- owns clamonacc.log.
#
# The directory must be owned and writable by 'clamscan' so that clamd
# can create and rotate clamd.log. Mode 0750 prevents world-access to
# potentially sensitive scan output.
#
# SELINUX: Bazzite is enforcing. Hand-created files get the wrong SELinux
# context, which makes clamd fail with "permission denied" on clamd.log
# DESPITE correct Unix ownership (the historical failure mode). After
# creating the files we run restorecon to apply the correct contexts and
# set the antivirus_can_scan_system boolean.
# -------------------------------------------------------------------------
section "Log directory and files (+ SELinux)"
ensure_config "$CLAMD_CONF" "LogFile" "/var/log/clamav/clamd.log"
ensure_config "$CLAMD_CONF" "LogFileMaxSize" "52428800"
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"
if ! $DRY_RUN; then
# Directory: owned clamscan:virusgroup, mode 2770 (setgid). All three
# ClamAV users (clamscan/clamd, clamupdate/freshclam, and clamonacc via
# root) need access here. The shared virusgroup + setgid bit means new
# log files (clamd.log rotation, freshclam.log) inherit virusgroup so
# freshclam (running as clamupdate) can write its log -- the 0750
# clamscan-only layout in 4.6.0 caused "Permission denied" on
# freshclam.log because clamupdate could not traverse the directory.
mkdir -p /var/log/clamav
chown clamscan:virusgroup /var/log/clamav
chmod 2770 /var/log/clamav
# clamd.log -- owned by clamscan (the clamd daemon user)
touch /var/log/clamav/clamd.log
chown clamscan:clamscan /var/log/clamav/clamd.log
chmod 0640 /var/log/clamav/clamd.log
info "Log dir and clamd.log ready: clamscan:clamscan"
# freshclam.log -- owned by clamupdate (the freshclam process user)
touch /var/log/clamav/freshclam.log
chown clamupdate:clamupdate /var/log/clamav/freshclam.log
chmod 0640 /var/log/clamav/freshclam.log
info "freshclam.log ready: clamupdate:clamupdate"
# clamonacc.log -- owned by root (clamonacc runs as root)
touch /var/log/clamav/clamonacc.log
chown root:root /var/log/clamav/clamonacc.log
chmod 0640 /var/log/clamav/clamonacc.log
info "clamonacc.log ready: root:root"
# --- SELinux remediation ---
# This is the fix for the historical clamd@scan "permission denied on
# clamd.log despite correct ownership" failure. Without correct
# contexts, clamd (confined) cannot write its log even as the right
# user. restorecon applies the policy-defined labels.
if command -v restorecon >/dev/null 2>&1; then
restorecon -Rv /var/log/clamav 2>/dev/null || true
restorecon -Rv "$QUARANTINE_DIR" 2>/dev/null || true
# /run/clamd.scan is tmpfiles-managed and may not exist until the
# socket is created; relabel if present, ignore otherwise.
[[ -d /run/clamd.scan ]] && restorecon -Rv /run/clamd.scan 2>/dev/null || true
info "SELinux contexts restored on log + quarantine paths."
else
warn "restorecon not found -- skipping SELinux relabel (unexpected on Bazzite)."
fi
if command -v setsebool >/dev/null 2>&1; then
setsebool -P antivirus_can_scan_system 1 2>/dev/null \
&& info "SELinux boolean antivirus_can_scan_system = on" \
|| warn "Could not set antivirus_can_scan_system (may not exist on this policy)."
fi
else
info "[DRY] Would create /var/log/clamav/ (clamscan:virusgroup, 2770 setgid)"
info "[DRY] Would create clamd.log (clamscan:clamscan, 0640)"
info "[DRY] Would create freshclam.log (clamupdate:clamupdate, 0640)"
info "[DRY] Would create clamonacc.log (root:root, 0640)"
info "[DRY] Would run restorecon on /var/log/clamav, $QUARANTINE_DIR, /run/clamd.scan"
info "[DRY] Would set SELinux boolean antivirus_can_scan_system = on"
fi
# LocalSocket: required by clamonacc to connect to clamd.
# Path matches /usr/lib/tmpfiles.d/clamd.scan.conf (d /run/clamd.scan ...)
ensure_config "$CLAMD_CONF" "LocalSocket" "/run/clamd.scan/clamd.sock"
# -------------------------------------------------------------------------
# SECTION B4 -- ExcludePath
# -------------------------------------------------------------------------
section "ExcludePath Directives"
for path in \
"/proc/" \
"/sys/" \
"/dev/" \
"/run/" \
"/var/lib/docker/" \
"/var/lib/containers/" \
"/var/cache/dnf/" \
"${QUARANTINE_DIR}/"; do
ensure_multivalue "$CLAMD_CONF" "ExcludePath" "$path"
done
# -------------------------------------------------------------------------
# SECTION B5 -- Alert Settings
# -------------------------------------------------------------------------
section "Alert Settings"
for key in \
AlertEncryptedArchive \
AlertEncryptedDoc \
AlertOLE2Macros \
AlertPhishingSSLMismatch \
AlertPhishingCloak \
AlertBrokenExecutables \
AlertBrokenMedia \
AlertPartitionIntersection; do
ensure_config "$CLAMD_CONF" "$key" "yes"
done
# -------------------------------------------------------------------------
# SECTION B6 -- Database Hygiene
# -------------------------------------------------------------------------
section "Database Hygiene"
ensure_config "$CLAMD_CONF" "OfficialDatabaseOnly" "no"
ensure_config "$CLAMD_CONF" "FailIfCvdOlderThan" "7"
ensure_config "$CLAMD_CONF" "ConcurrentDatabaseReload" "yes"
ensure_config "$CLAMD_CONF" "SelfCheck" "1800"
# -------------------------------------------------------------------------
# SECTION B7 -- Detection Refinements
# -------------------------------------------------------------------------
section "Detection Refinements"
ensure_config "$CLAMD_CONF" "DetectPUA" "yes"
ensure_config "$CLAMD_CONF" "HeuristicAlerts" "yes"
ensure_config "$CLAMD_CONF" "HeuristicScanPrecedence" "yes"
# -------------------------------------------------------------------------
# SECTION B8 -- File & Scan Limits
# -------------------------------------------------------------------------
section "File and Scan Limits"
ensure_config "$CLAMD_CONF" "MaxScanSize" "209715200"
ensure_config "$CLAMD_CONF" "MaxFileSize" "52428800"
ensure_config "$CLAMD_CONF" "MaxRecursion" "16"
ensure_config "$CLAMD_CONF" "MaxFiles" "15000"
ensure_config "$CLAMD_CONF" "MaxEmbeddedPE" "10485760"
ensure_config "$CLAMD_CONF" "MaxHTMLNormalize" "10485760"
ensure_config "$CLAMD_CONF" "MaxHTMLNoTags" "2097152"
ensure_config "$CLAMD_CONF" "MaxScriptNormalize" "5242880"
ensure_config "$CLAMD_CONF" "MaxZipTypeRcg" "1048576"
ensure_config "$CLAMD_CONF" "OnAccessMaxFileSize" "26214400"
# -------------------------------------------------------------------------
# SECTION B9 -- On-Access Scanning (DETECTION mode)
#
# On Fedora/Bazzite, clamav-clamonacc.service handles on-access scanning
# via fanotify. OnAccessIncludePath and OnAccessPrevention in scan.conf
# are read by clamonacc -- clamd itself does not enforce them directly.
#
# OnAccessPrevention 'no' = DETECTION mode: file opens are NOT blocked.
# clamonacc scans on access and quarantines detections via --move (set in
# Section B9b). This is the agreed architecture -- not blocking.
#
# OnAccessExcludeUID 0: prevents clamonacc from scanning file events
# generated by UID 0 processes, which avoids recursive scan loops from
# system/root writes. NOTE: clamd runs as 'clamscan' on Fedora (not root);
# this exclusion is about the UID of the process triggering the access.
# Because this exclusion is active, files created BY ROOT (including by
# this script) are not scanned on access -- verify on-access as a normal
# user (UID 1000). See the summary for the verification command.
# -------------------------------------------------------------------------
section "On-Access Scanning (Detection Mode)"
if ! $DRY_RUN; then
if grep -qE "^[[:space:]]*OnAccessIncludePath[[:space:]]" "$CLAMD_CONF"; then
sed -i "/^[[:space:]]*OnAccessIncludePath[[:space:]]/d" "$CLAMD_CONF"
info "Cleared existing OnAccessIncludePath entries for clean rewrite."
fi
fi
# Bazzite real home is /var/home (/home is a symlink fanotify does not
# follow). Watch /var/home, not /home.
for oa_path in "/var/home" "/tmp" "/var/tmp" "/root"; do
ensure_multivalue "$CLAMD_CONF" "OnAccessIncludePath" "$oa_path"
done
# DETECTION mode: do NOT block opens. clamonacc --move handles quarantine.
ensure_config "$CLAMD_CONF" "OnAccessPrevention" "no"
# clamonacc requires at least one OnAccessExclude* directive (loop guard).
# OnAccessExcludeUID 0 alone did not satisfy clamonacc when read via clamd;
# OnAccessExcludeUname clamscan (the clamd daemon user on Fedora) does.
ensure_config "$CLAMD_CONF" "OnAccessExcludeUID" "0"
ensure_config "$CLAMD_CONF" "OnAccessExcludeUname" "clamscan"
for path in "/proc" "/sys" "/dev" "/run" "$QUARANTINE_DIR"; do
ensure_multivalue "$CLAMD_CONF" "OnAccessExcludePath" "$path"
done
# -------------------------------------------------------------------------
# SECTION B9b -- On-access quarantine drop-in (clamonacc --move)
#
# Quarantine on access is performed by clamonacc, NOT by a clamd.conf
# directive (there is no "MoveFileTo" directive). We install a systemd
# drop-in that overrides the packaged ExecStart to add:
# --move=/var/quarantine/clamav (quarantine detections)
# --log=/var/log/clamav/clamonacc.log
# --config-file=/etc/clamd.d/scan.conf
# -F (foreground -- required for a Type=simple systemd service)
#
# The empty "ExecStart=" first line is required by systemd to clear the
# packaged ExecStart before setting our own.
#
# Reference: https://docs.clamav.net/manual/OnAccess.html
# clamonacc --move=DIRECTORY moves detected files to quarantine.
# -------------------------------------------------------------------------
# --- On-access quarantine drop-in (clamonacc) ---
# --fdpass is REQUIRED: clamonacc runs as root but the daemon runs as the
# clamscan user. Without --fdpass the daemon reopens each file BY PATH and
# fails with "File path check failure: Permission denied" on anything the
# clamscan user cannot read (e.g. private Insync/sync dirs), so detections
# never get scanned or quarantined. --fdpass passes the open fd to the
# daemon, sidestepping the path-permission problem. This is the fix that
# made on-access quarantine work.
# --move quarantines detected files; --log records scan results.
# The empty "ExecStart=" first line clears the packaged ExecStart.
section "On-Access Quarantine Drop-in (clamonacc --fdpass --move)"
if $DRY_RUN; then
info "[DRY] Would install $CLAMONACC_DROPIN with --fdpass --move=$QUARANTINE_DIR"
else
mkdir -p "$CLAMONACC_DROPIN_DIR"
cat > "$CLAMONACC_DROPIN" << DROPIN_EOF
[Service]
ExecStart=
ExecStart=/usr/sbin/clamonacc -F --fdpass --config-file=/etc/clamd.d/scan.conf --log=/var/log/clamav/clamonacc.log --move=${QUARANTINE_DIR}
DROPIN_EOF
chmod 0644 "$CLAMONACC_DROPIN"
[[ -x "$(command -v restorecon)" ]] && restorecon -v "$CLAMONACC_DROPIN" 2>/dev/null || true
info "Installed clamonacc drop-in: $CLAMONACC_DROPIN"
fi
# -------------------------------------------------------------------------
# SECTION B10 -- Freshclam settings
#
# Checks 6 = every 4 hours. Stays within Sanesecurity mirror and URLhaus
# rate limits. NotifyClamd is not used on Fedora -- clamd reloads updated
# databases automatically (SelfCheck / ConcurrentDatabaseReload).
#
# LogFile in freshclam.conf points to the log file pre-created in B3
# with clamupdate:clamupdate ownership.
# -------------------------------------------------------------------------
section "Freshclam Settings"
ensure_config "$FRESHCLAM_CONF" "UpdateLogFile" "/var/log/clamav/freshclam.log"
ensure_config "$FRESHCLAM_CONF" "Checks" "6"
ensure_config "$FRESHCLAM_CONF" "MaxAttempts" "5"
ensure_config "$FRESHCLAM_CONF" "ConnectTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "ReceiveTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "LogVerbose" "yes"
ensure_config "$FRESHCLAM_CONF" "LogSyslog" "yes"
ensure_config "$FRESHCLAM_CONF" "LogFacility" "LOG_LOCAL6"
# -------------------------------------------------------------------------
# SECTION B11 -- Third-party signatures (Sanesecurity + URLhaus)
# -------------------------------------------------------------------------
section "Third-Party Signatures (DatabaseCustomURL)"
SANESEC_MIRROR="https://mirror.rollernet.us/sanesecurity"
# --- Required Sanesecurity support files ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sanesecurity.ftm"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sigwhitelist.ign2"
# --- Low FP risk: Sanesecurity core databases ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/junk.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/jurlbl.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/phish.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/rogue.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/scam.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/spamimg.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/spamattach.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/blurl.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/malwarehash.hsb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/hackingteam.hsb"
# --- Low FP risk: Foxhole (executable/archive content detection) ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_generic.cdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_filename.cdb"
# --- Low FP risk: OITC winnow databases ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_malware.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_malware_links.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_extended_malware.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow.attachments.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_bad_cw.hdb"
# --- URLhaus: active malware distribution URLs (abuse.ch) -- Low FP risk ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "https://urlhaus.abuse.ch/downloads/urlhaus.ndb"
# --- MEDIUM FP RISK -- disabled by default, uncomment to enable ---
# DatabaseCustomURL ${SANESEC_MIRROR}/jurlbla.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/lott.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/spam.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/badmacro.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/shelter.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_js.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_spam_complete.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete_url.ndb
# --- HIGH FP RISK -- not recommended for workstations ---
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_all.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete.ndb
# -------------------------------------------------------------------------
# SECTION B12 -- Initial freshclam update
#
# Stop clamav-freshclam.service first if it is already running (re-run
# safety). If the service holds the log file lock and freshclam is invoked
# manually simultaneously, both processes will fail with a lock conflict.
# On a first run the service won't be active yet -- the stop is a no-op.
# -------------------------------------------------------------------------
section "Running initial freshclam update"
if $DRY_RUN; then
info "[DRY] Would stop clamav-freshclam.service (if running)"
info "[DRY] Would run: freshclam"
else
info "Stopping clamav-freshclam.service (if running) to avoid log-lock..."
systemctl stop clamav-freshclam.service 2>/dev/null || true
sleep 1
info "Updating virus definitions (this may take a moment on first run)..."
freshclam || warn "freshclam update failed -- check /var/log/clamav/freshclam.log"
fi
# -------------------------------------------------------------------------
# SECTION B13 -- Enable and start services
#
# daemon-reload first: picks up the clamonacc drop-in and any unit changes.
#
# Start order matters:
# 1. clamav-freshclam.service -- signature updates
# 2. clamd@scan.service -- clamd daemon (clamonacc connects to this)
# 3. clamav-clamonacc.service -- on-access scanner (requires clamd socket)
#
# clamd@scan uses the template unit clamd@.service with scan.conf.
# The socket at /run/clamd.scan/clamd.sock is created by tmpfiles.d on boot.
# clamd@scan.service is treated as a hard dependency -- if it fails to start
# clamonacc cannot run and the script exits rather than silently leaving
# the system unprotected. (If it fails on SELinux, run: ausearch -m AVC.)
# -------------------------------------------------------------------------
section "Enabling and starting ClamAV services"
if $DRY_RUN; then
info "[DRY] Would run: systemctl daemon-reload"
info "[DRY] Would enable and start clamav-freshclam.service"
info "[DRY] Would enable and start clamd@scan.service"
info "[DRY] Would enable and start clamav-clamonacc.service"
else
info "Reloading systemd unit files..."
systemctl daemon-reload
info "Enabling clamav-freshclam.service..."
systemctl enable clamav-freshclam.service
systemctl start clamav-freshclam.service
sleep 2
if systemctl is-active --quiet clamav-freshclam.service; then
info "SUCCESS: clamav-freshclam.service is active."
else
warn "clamav-freshclam.service did not start cleanly."
systemctl status clamav-freshclam.service --no-pager || true
fi
info "Enabling clamd@scan.service..."
systemctl enable clamd@scan.service
# Clear any prior restart rate-limit. clamd is fine, but if a previous
# attempt rapid-retried, systemd latches "start request repeated too
# quickly" and refuses to start even when the underlying issue is gone.
systemctl reset-failed clamd@scan.service 2>/dev/null || true
systemctl start clamd@scan.service
sleep 3
if systemctl is-active --quiet clamd@scan.service; then
info "SUCCESS: clamd@scan.service is active."
else
error "clamd@scan.service failed to start -- clamonacc cannot run without it."
systemctl status clamd@scan.service --no-pager || true
error "If this is an SELinux denial, capture it with:"
error " sudo ausearch -m AVC -ts recent"
error " sudo journalctl -u clamd@scan -b | grep -i denied"
exit 1
fi
info "Enabling clamav-clamonacc.service..."
systemctl enable clamav-clamonacc.service
systemctl start clamav-clamonacc.service
sleep 2
if systemctl is-active --quiet clamav-clamonacc.service; then
info "SUCCESS: clamav-clamonacc.service is active."
else
warn "clamav-clamonacc.service did not start cleanly."
systemctl status clamav-clamonacc.service --no-pager || true
warn "If on-access quarantine (--move) is denied by SELinux, check:"
warn " sudo ausearch -m AVC -ts recent"
fi
fi
# -------------------------------------------------------------------------
# SECTION B14 -- EICAR functional test (engine + DB)
#
# Uses clamscan (command-line scanner, runs as current user = root here).
# clamscan loads the database directly so it does not need clamd running.
# This confirms database download succeeded and detection is functional.
#
# NOTE: This does NOT test on-access scanning. On-access excludes UID 0,
# and this script runs as root, so a root-created EICAR file would not be
# scanned on access. Verify on-access as a normal user -- see the summary.
# -------------------------------------------------------------------------
section "EICAR Functional Test (engine + DB)"
if $DRY_RUN; then
info "[DRY] Would run EICAR detection test via clamscan."
else
EICAR_FILE="/tmp/eicar_test_$$.txt"
printf 'X5O!P%%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > "${EICAR_FILE}"
info "Running EICAR test via clamscan..."
SCAN_RESULT=$(clamscan --no-summary "$EICAR_FILE" 2>&1 || true)
if echo "$SCAN_RESULT" | grep -q "FOUND"; then
info "EICAR test: PASSED -- clamscan detected test signature correctly."
else
warn "EICAR test: FAILED or definitions not yet downloaded."
warn "Scan output: $SCAN_RESULT"
fi
rm -f "$EICAR_FILE"
fi
# -------------------------------------------------------------------------
# SECTION B15 -- Bazzite usage summary
# -------------------------------------------------------------------------
section "Bazzite ClamAV Setup Complete"
echo ""
echo " +-------------------------------------------------------------+"
echo " | ClamAV -- Bazzite OS (native rpm-ostree) v4.6.1 |"
echo " +-------------------------------------------------------------+"
echo " | Packages: clamav clamav-freshclam clamd |"
echo " | Config: /etc/clamd.d/scan.conf |"
echo " | /etc/freshclam.conf |"
echo " | Socket: /run/clamd.scan/clamd.sock |"
echo " | Services: clamav-freshclam clamd@scan clamav-clamonacc |"
echo " | On-access: /var/home /tmp /var/tmp /root (DETECT+QUARANTINE)|"
echo " | Quarantine: /var/quarantine/clamav (clamonacc --move) |"
echo " | Signatures: Every 4 hours via clamav-freshclam.service |"
echo " +-------------------------------------------------------------+"
echo " | Log files: |"
echo " | /var/log/clamav/clamd.log (clamscan:clamscan) |"
echo " | /var/log/clamav/freshclam.log (clamupdate:clamupdate) |"
echo " | /var/log/clamav/clamonacc.log (root:root) |"
echo " +-------------------------------------------------------------+"
echo " | Verify on-access (RUN AS NORMAL USER, not root): |"
echo " | cd ~ && curl -sO https://secure.eicar.org/eicar.com.txt |"
echo " | sleep 8 && sudo ls -l /var/quarantine/clamav |"
echo " | (file should be moved to quarantine within a few seconds)|"
echo " +-------------------------------------------------------------+"
echo " | Useful commands: |"
echo " | clamscan -r --infected ~/ |"
echo " | sudo systemctl status clamd@scan |"
echo " | sudo systemctl status clamav-clamonacc |"
echo " | sudo systemctl status clamav-freshclam |"
echo " | sudo freshclam |"
echo " | journalctl -u clamd@scan -f |"
echo " | tail -f /var/log/clamav/clamonacc.log |"
echo " | sudo ausearch -m AVC -ts recent # SELinux denials |"
echo " +-------------------------------------------------------------+"
echo ""
exit 0
fi
# =============================================================================
# DEBIAN / LMDE PATH (everything below is Debian only)
# =============================================================================
# --- Paths ---
CLAMD_CONF="/etc/clamav/clamd.conf"
FRESHCLAM_CONF="/etc/clamav/freshclam.conf"
LOGFILE="/var/log/clamav/harden_clamav.log"
QUARANTINE_DIR="/var/quarantine/clamav"
BACKUP_DIR="/etc/clamav/backups/$(date +%Y%m%d_%H%M%S)"
# --- Ensure log dir and quarantine dir exist, then enable install logging ---
#
# harden_clamav.log is owned root:adm -- this script runs as root and writes
# to it via the tee redirect below. The redirect captures all subsequent
# stdout/stderr to the install log as well as the terminal.
if ! $DRY_RUN; then
mkdir -p "$(dirname "$LOGFILE")" "$QUARANTINE_DIR"
chmod 0700 "$QUARANTINE_DIR"
chown root:root "$QUARANTINE_DIR"
touch "$LOGFILE"
chown root:adm "$LOGFILE"
chmod 0640 "$LOGFILE"
# Redirect all subsequent output to the install log as well as the terminal
exec > >(tee -a "$LOGFILE") 2>&1
info "Log redirect active -- all output now written to $LOGFILE"
fi
# -----------------------------------------------------------------------------
# SECTION 0 -- Stop services and back up existing config
#
# We stop both services before touching config files, then reset each config
# to the vendor-supplied default. This guarantees no stale/conflicting
# directives carry over from previous runs or manual edits.
# -----------------------------------------------------------------------------
section "Stopping ClamAV services"
if ! $DRY_RUN; then
info "Stopping clamav-daemon and clamav-freshclam..."
systemctl stop clamav-daemon.service 2>/dev/null || true
systemctl stop clamav-freshclam.service 2>/dev/null || true
sleep 1
# Kill any stale freshclam processes (prevents log lock on restart)
if pgrep -x freshclam >/dev/null 2>&1; then
warn "Stale freshclam process found -- terminating..."
pkill -x freshclam || true
sleep 1
fi
# Remove any stale lock / pid files
rm -f /var/run/clamav/freshclam.pid
rm -f /var/run/clamav/clamd.pid
info "Stale PID files cleared."
else
info "[DRY] Would stop clamav-daemon and clamav-freshclam services"
info "[DRY] Would kill any stale freshclam processes"
info "[DRY] Would remove stale PID files"
fi
section "Backing up existing configuration"
if ! $DRY_RUN; then
mkdir -p "$BACKUP_DIR"
[[ -f "$CLAMD_CONF" ]] && cp "$CLAMD_CONF" "${BACKUP_DIR}/clamd.conf.bak" && info "Backed up: $CLAMD_CONF"
[[ -f "$FRESHCLAM_CONF" ]] && cp "$FRESHCLAM_CONF" "${BACKUP_DIR}/freshclam.conf.bak" && info "Backed up: $FRESHCLAM_CONF"
else
info "[DRY] Would back up configs to $BACKUP_DIR"
fi
section "Resetting configuration files to vendor baseline"
if ! $DRY_RUN; then
info "Regenerating clamd.conf and freshclam.conf via dpkg-reconfigure..."
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure clamav-daemon 2>/dev/null || true
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure clamav-freshclam 2>/dev/null || true
info "Config files reset to vendor defaults."
else
info "[DRY] Would run dpkg-reconfigure clamav-daemon and clamav-freshclam"
fi
# -----------------------------------------------------------------------------
# SECTION 1 -- Socket Permissions and daemon user
#
# User root is required for fanotify on-access scanning. Debian ships
# clamav-daemon as User clamav; without this override OnAccessPrevention
# silently fails to mount the fanotify watch and real-time protection is
# inactive. dpkg-reconfigure (Section 0) resets the user to clamav, so this
# must run after it on every invocation.
# -----------------------------------------------------------------------------
section "Socket Permissions and daemon user"
ensure_config "$CLAMD_CONF" "User" "root"
ensure_config "$CLAMD_CONF" "LocalSocketMode" "660"
ensure_config "$CLAMD_CONF" "LocalSocketGroup" "clamav"
ensure_config "$CLAMD_CONF" "PidFile" "/var/run/clamav/clamd.pid"
# -----------------------------------------------------------------------------
# SECTION 2 -- Logging
# -----------------------------------------------------------------------------
section "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"
# -----------------------------------------------------------------------------
# SECTION 3 -- ExcludePath (daemon)
# Excludes virtual/kernel filesystems and container storage to avoid false
# positives and unnecessary CPU load.
# -----------------------------------------------------------------------------
section "ExcludePath Directives"
for path in \
"/proc/" \
"/sys/" \
"/dev/" \
"/run/" \
"/var/lib/docker/" \
"/var/lib/containers/" \
"/var/lib/mysql/" \
"/var/lib/postgresql/" \
"/var/cache/apt/archives/" \
"$QUARANTINE_DIR/"; do
ensure_multivalue "$CLAMD_CONF" "ExcludePath" "$path"
done
# -----------------------------------------------------------------------------
# SECTION 4 -- Alert Settings
# -----------------------------------------------------------------------------
section "Alert Settings"
for key in \
AlertEncryptedArchive \
AlertEncryptedDoc \
AlertOLE2Macros \
AlertPhishingSSLMismatch \
AlertPhishingCloak \
AlertBrokenExecutables \
AlertBrokenMedia \
AlertPartitionIntersection; do
ensure_config "$CLAMD_CONF" "$key" "yes"
done
# -----------------------------------------------------------------------------
# SECTION 5 -- Database Hygiene
# OfficialDatabaseOnly is 'no' to allow third-party signature databases
# (e.g. Sanesecurity, URLhaus). Set to 'yes' if using ONLY official DBs.
# -----------------------------------------------------------------------------
section "Database Hygiene"
ensure_config "$CLAMD_CONF" "OfficialDatabaseOnly" "no"
ensure_config "$CLAMD_CONF" "FailIfCvdOlderThan" "7"
ensure_config "$CLAMD_CONF" "ConcurrentDatabaseReload" "yes"
ensure_config "$CLAMD_CONF" "SelfCheck" "1800" # 30-minute self-check interval
# -----------------------------------------------------------------------------
# SECTION 6 -- Detection Refinements
# AlgorithmicDetection was deprecated in ClamAV 0.101 -- do not use it.
# HeuristicAlerts is the correct replacement.
# -----------------------------------------------------------------------------
section "Detection Refinements"
ensure_config "$CLAMD_CONF" "DetectPUA" "yes"
ensure_config "$CLAMD_CONF" "HeuristicAlerts" "yes"
ensure_config "$CLAMD_CONF" "HeuristicScanPrecedence" "yes"
# -----------------------------------------------------------------------------
# SECTION 7 -- File & Scan Limits
# Balanced for typical laptop workloads.
# -----------------------------------------------------------------------------
section "File and Scan Limits"
ensure_config "$CLAMD_CONF" "MaxScanSize" "209715200" # 200 MB
ensure_config "$CLAMD_CONF" "MaxFileSize" "52428800" # 50 MB
ensure_config "$CLAMD_CONF" "MaxRecursion" "16"
ensure_config "$CLAMD_CONF" "MaxFiles" "15000"
ensure_config "$CLAMD_CONF" "MaxEmbeddedPE" "10485760" # 10 MB
ensure_config "$CLAMD_CONF" "MaxHTMLNormalize" "10485760"
ensure_config "$CLAMD_CONF" "MaxHTMLNoTags" "2097152"
ensure_config "$CLAMD_CONF" "MaxScriptNormalize" "5242880"
ensure_config "$CLAMD_CONF" "MaxZipTypeRcg" "1048576"
ensure_config "$CLAMD_CONF" "OnAccessMaxFileSize" "26214400" # 25 MB
# -----------------------------------------------------------------------------
# SECTION 8 -- On-Access Scanning (real-time workstation protection)
#
# OnAccessPrevention BLOCKS access to detected files rather than just alerting.
# Requires kernel fanotify support (Linux Mint 20+ / kernel 5.4+) and clamd
# running as root (User root set in Section 1).
#
# OnAccessExcludeUID 0 prevents clamd scanning its own writes -- correct on a
# workstation where no trusted process legitimately writes as root into the
# scanned paths.
#
# NOTE: OnAccessExtraScanning was permanently disabled in ClamAV 0.100.2 due
# to a kernel resource cleanup bug -- omitted to avoid spurious warnings.
# -----------------------------------------------------------------------------
section "On-Access Scanning (Home Directory Protection)"
# Clear all existing OnAccessIncludePath entries for a clean rewrite
if ! $DRY_RUN; then
if grep -qE "^[[:space:]]*OnAccessIncludePath[[:space:]]" "$CLAMD_CONF"; then
sed -i "/^[[:space:]]*OnAccessIncludePath[[:space:]]/d" "$CLAMD_CONF"
info "Cleared existing OnAccessIncludePath entries for clean rewrite."
fi
fi
for oa_path in "/home" "/tmp" "/var/tmp" "/root"; do
ensure_multivalue "$CLAMD_CONF" "OnAccessIncludePath" "$oa_path"
done
ensure_config "$CLAMD_CONF" "OnAccessPrevention" "yes"
ensure_config "$CLAMD_CONF" "OnAccessExcludeUID" "0" # Prevents clamd scanning its own writes
for path in "/proc" "/sys" "/dev" "/run" "$QUARANTINE_DIR"; do
ensure_multivalue "$CLAMD_CONF" "OnAccessExcludePath" "$path"
done
# -----------------------------------------------------------------------------
# SECTION 9 -- Freshclam (signature updates via systemd service)
#
# UPDATE STRATEGY: clamav-freshclam.service manages all signature updates.
# No cron job is used. This eliminates the log-lock conflict caused by
# multiple freshclam processes competing for freshclam.log simultaneously.
#
# Checks 6 = freshclam checks for new signatures 6 times per day (every 4 hours).
# This matches Sanesecurity's recommended mirror polling frequency and stays within
# URLhaus (abuse.ch) rate limits. Do not increase above 6 in fleet deployments.
# -----------------------------------------------------------------------------
section "Freshclam Settings"
ensure_config "$FRESHCLAM_CONF" "Checks" "6"
ensure_config "$FRESHCLAM_CONF" "MaxAttempts" "5"
ensure_config "$FRESHCLAM_CONF" "ConnectTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "ReceiveTimeout" "30"
ensure_config "$FRESHCLAM_CONF" "LogVerbose" "yes"
ensure_config "$FRESHCLAM_CONF" "LogSyslog" "yes"
ensure_config "$FRESHCLAM_CONF" "LogFacility" "LOG_LOCAL6"
ensure_config "$FRESHCLAM_CONF" "NotifyClamd" "$CLAMD_CONF" # Triggers live DB reload after update
# -----------------------------------------------------------------------------
# SECTION 9b -- Third-Party Signatures via DatabaseCustomURL
#
# Freshclam natively supports pulling third-party signature databases via
# DatabaseCustomURL. OfficialDatabaseOnly must be 'no' (set in Section 5).
#
# SOURCES:
# Sanesecurity -- active since 2006, hourly updates, free. Covers phishing,
# malware, macros, spam and zero-day threats.
# URLhaus -- abuse.ch project, updated frequently. Active malware
# distribution URLs. Free, no account required.
#
# FALSE POSITIVE RISK: Only Low FP risk databases are enabled below.
# Medium/High FP databases are commented out -- enable with caution.
#
# MEMORY: Each database increases clamd RAM usage ~150-250 MB total for
# the selection below. Monitor with: systemctl status clamav-daemon
# -----------------------------------------------------------------------------
section "Third-Party Signatures (DatabaseCustomURL)"
SANESEC_MIRROR="https://mirror.rollernet.us/sanesecurity"
# --- Required Sanesecurity support files ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sanesecurity.ftm"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/sigwhitelist.ign2"
# --- Low FP risk: Sanesecurity core databases ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/junk.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/jurlbl.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/phish.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/rogue.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/scam.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/spamimg.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/spamattach.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/blurl.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/malwarehash.hsb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/hackingteam.hsb"
# --- Low FP risk: Foxhole (executable/archive content detection) ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_generic.cdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/foxhole_filename.cdb"
# --- Low FP risk: OITC winnow databases ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_malware.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_malware_links.ndb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_extended_malware.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow.attachments.hdb"
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "${SANESEC_MIRROR}/winnow_bad_cw.hdb"
# --- URLhaus: active malware distribution URLs (abuse.ch) -- Low FP risk ---
ensure_multivalue "$FRESHCLAM_CONF" "DatabaseCustomURL" "https://urlhaus.abuse.ch/downloads/urlhaus.ndb"
# --- MEDIUM FP RISK -- disabled by default, uncomment to enable ---
# DatabaseCustomURL ${SANESEC_MIRROR}/jurlbla.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/lott.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/spam.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/badmacro.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/shelter.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/spear.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/spearl.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_js.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_js.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_spam_complete.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete_url.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow.complex.patterns.ldb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_extended_malware_links.ndb
# --- HIGH FP RISK -- not recommended for workstations ---
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_all.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_all.ndb
# DatabaseCustomURL ${SANESEC_MIRROR}/foxhole_mail.cdb
# DatabaseCustomURL ${SANESEC_MIRROR}/winnow_phish_complete.ndb
# -----------------------------------------------------------------------------
# SECTION 10 -- Remove legacy cron job (if present from earlier script versions)
#
# Previous versions of this script installed a cron job at
# /etc/cron.d/clamav-scheduled to manage freshclam updates. This caused
# log-lock conflicts when both the cron job and the systemd daemon attempted
# to run freshclam simultaneously. Removed in v3.0.0.
# -----------------------------------------------------------------------------
section "Removing legacy cron job (if present)"
LEGACY_CRON="/etc/cron.d/clamav-scheduled"
if [[ -f "$LEGACY_CRON" ]]; then
if $DRY_RUN; then
warn "[DRY] Would remove legacy cron file: $LEGACY_CRON"
else
rm -f "$LEGACY_CRON"
warn "Removed legacy cron file: $LEGACY_CRON"
fi
else
info "No legacy cron file found -- nothing to remove."
fi
# -----------------------------------------------------------------------------
# SECTION 11 -- Enable and start services
#
# clamav-freshclam.service: manages signature updates (Checks 6 = every 4 hrs)
# clamav-daemon.service: provides on-access scanning and clamdscan socket
#
# freshclam runs first to ensure a current signature database exists before
# the daemon starts. If the daemon starts with a missing/outdated DB and
# FailIfCvdOlderThan is set, it will refuse to start.
#
# The service is stopped before the manual freshclam run to release the log
# file lock. On re-runs where the service was already active, running
# freshclam while the service holds the lock fails with:
# ERROR: Failed to lock the log file ... Resource temporarily unavailable
# -----------------------------------------------------------------------------
section "Enabling and starting ClamAV services"
if $DRY_RUN; then
info "[DRY] Would enable clamav-freshclam.service"
info "[DRY] Would stop clamav-freshclam.service before manual freshclam run"
info "[DRY] Would run an initial freshclam update"
info "[DRY] Would enable and start clamav-daemon.service"
else
info "Enabling clamav-freshclam.service..."
systemctl enable clamav-freshclam.service
info "Stopping clamav-freshclam.service to release log lock..."
systemctl stop clamav-freshclam.service 2>/dev/null || true
sleep 1
info "Running initial freshclam update (may take several minutes on first run)..."
freshclam || warn "Initial freshclam update failed -- daemon may start with existing DB if present."
info "Starting clamav-freshclam.service..."
systemctl start clamav-freshclam.service
sleep 2
if systemctl is-active --quiet clamav-freshclam.service; then
info "SUCCESS: clamav-freshclam.service is active."
else
warn "clamav-freshclam.service did not start cleanly."
systemctl status clamav-freshclam.service || true
fi
info "Enabling clamav-daemon.service..."
systemctl enable clamav-daemon.service
info "Starting clamav-daemon.service..."
if systemctl start clamav-daemon.service; then
sleep 2
if systemctl is-active --quiet clamav-daemon.service; then
info "SUCCESS: clamav-daemon.service is active."
else
error "clamav-daemon failed to start."
systemctl status clamav-daemon.service
exit 1
fi
else
error "Failed to start clamav-daemon."
exit 1
fi
fi
# -----------------------------------------------------------------------------
# SECTION 12 -- EICAR test (functional validation)
#
# printf is used to write the EICAR file -- no Python, no interpreter escaping
# layers. %% -> literal %, \P passes through as-is (single backslash).
# -----------------------------------------------------------------------------
section "EICAR Functional Test"
if $DRY_RUN; then
info "[DRY] Would run EICAR detection test against clamd."
else
EICAR_FILE="/tmp/eicar_test_$$.txt"
printf 'X5O!P%%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > "${EICAR_FILE}"
sleep 1
info "Running EICAR test against clamd..."
SCAN_RESULT=$(clamdscan --fdpass --no-summary "$EICAR_FILE" 2>&1 || true)
if echo "$SCAN_RESULT" | grep -q "FOUND"; then
info "EICAR test: PASSED -- clamd detected test signature correctly."
else
warn "EICAR test: FAILED or clamd not yet responding."
warn "Scan output: $SCAN_RESULT"
warn "Manual test: clamdscan --fdpass /tmp/eicar_test.txt"
fi
rm -f "$EICAR_FILE"
fi
# -----------------------------------------------------------------------------
# SECTION 13 -- Systemd Failure Notification
#
# ExecStart calls a standalone helper script installed at
# /usr/local/lib/clamav-notify-failure.sh. The script runs in a clean bash
# environment with no systemd specifier or printf escaping complications.
# The unit passes the failed service name as $1 via the systemd %i specifier.
# hostname -s is used as a reliable fallback when hostname -f returns empty.
#
# The template is reusable -- any unit can add:
# OnFailure=notify-failure@%n.service
# -----------------------------------------------------------------------------
section "Systemd Failure Notification"
NOTIFY_HELPER="/usr/local/lib/clamav-notify-failure.sh"
NOTIFY_UNIT="/etc/systemd/system/notify-failure@.service"
# --- Install the helper script ---
if $DRY_RUN; then
info "[DRY] Would install notify helper script at $NOTIFY_HELPER"
else
cat > "$NOTIFY_HELPER" << 'HELPER_EOF'
#!/bin/bash
# /usr/local/lib/clamav-notify-failure.sh
# Called by notify-failure@.service with the failed unit name as $1
# Sends a failure email via the local MTA (Postfix/sendmail)
UNIT="${1:-unknown-unit}"
# Use hostname -s as reliable fallback; -f can fail if FQDN is not configured
HOST="$(hostname -f 2>/dev/null || hostname -s 2>/dev/null || echo 'unknown-host')"
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
JOURNAL="$(journalctl -u "${UNIT}" -n 50 --no-pager 2>/dev/null || echo 'Journal unavailable')"
/usr/sbin/sendmail -t <<MAIL_EOF
Subject: [ClamAV FAILURE] ${UNIT} failed on ${HOST}
To: root
Content-Type: text/plain
Service failure alert
======================
Host: ${HOST}
Unit: ${UNIT}
Time: ${TIMESTAMP}
Last 50 journal lines:
${JOURNAL}
MAIL_EOF
HELPER_EOF
chmod 0755 "$NOTIFY_HELPER"
info "Installed: $NOTIFY_HELPER"
fi
# --- Install the systemd unit template ---
if $DRY_RUN; then
info "[DRY] Would install notify-failure@.service at $NOTIFY_UNIT"
else
cat > "$NOTIFY_UNIT" << 'UNIT_EOF'
[Unit]
Description=Failure notification for %i
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/lib/clamav-notify-failure.sh %i
UNIT_EOF
chmod 0644 "$NOTIFY_UNIT"
info "Installed: $NOTIFY_UNIT"
fi
# --- Install OnFailure= drop-in overrides for both ClamAV units ---
for UNIT in clamav-daemon.service clamav-freshclam.service; do
DROPIN_DIR="/etc/systemd/system/${UNIT}.d"
DROPIN_FILE="${DROPIN_DIR}/notify-on-failure.conf"
if $DRY_RUN; then
info "[DRY] Would install OnFailure drop-in for ${UNIT} at ${DROPIN_FILE}"
else
mkdir -p "$DROPIN_DIR"
cat > "$DROPIN_FILE" << 'DROPIN_EOF'
[Unit]
OnFailure=notify-failure@%n.service
DROPIN_EOF
chmod 0644 "$DROPIN_FILE"
info "Installed drop-in: $DROPIN_FILE"
fi
done
# Reload systemd to pick up the new unit and drop-ins
if $DRY_RUN; then
info "[DRY] Would run systemctl daemon-reload"
else
systemctl daemon-reload
info "systemctl daemon-reload complete."
fi
# -----------------------------------------------------------------------------
# COMPLETE
# -----------------------------------------------------------------------------
section "Setup Complete"
echo ""
echo " +-------------------------------------------------------------+"
echo " | ClamAV -- Debian / LMDE v4.6.1 |"
echo " +-------------------------------------------------------------+"
echo " | Services: clamav-daemon clamav-freshclam |"
echo " | On-access: /home /tmp /var/tmp /root (BLOCKING) |"
echo " | Quarantine: /var/quarantine/clamav |"
echo " | Signatures: Every 4 hours via freshclam service |"
echo " | Install log: /var/log/clamav/harden_clamav.log |"
echo " | Notify: /usr/local/lib/clamav-notify-failure.sh |"
echo " +-------------------------------------------------------------+"
echo " | Useful commands: |"
echo " | systemctl status clamav-daemon |"
echo " | systemctl status clamav-freshclam |"
echo " | clamdscan --fdpass ~/ |"
echo " | journalctl -u clamav-daemon -f |"
echo " +-------------------------------------------------------------+"
echo ""Update done. Finally fully working across restarts and catching test files and quarantining them.
#enoughsaid
