ClamAV Setup on Linux: 13 Steps, 45 Min [2026]

A Linux server that accepts uploads, relays mail attachments, or hosts shared drives needs a malware scanner, and ClamAV is still the open-source default for the job. The Cisco Talos team behind it maintains ClamAV 1.5.4, the latest stable release, published as a security patch version. This ClamAV Linux tutorial walks through 13 steps, roughly 45 minutes of work, and ends with a small Python service that scans every upload before your application touches it.

The other reason to set it up now is the size of the thing you are installing. In a November 2025 announcement, the project said it would retire older signatures and cut main.cvd from 163 MB to about 80 MB, with daily.cvd falling from 62 MB to about 22 MB. Smaller databases mean faster updates and a lighter daemon, which makes ClamAV easier to justify on a small VPS. Everything below works on Debian and Ubuntu with apt. I note the differences for Fedora and its relatives, which you can compare in our Ubuntu vs Fedora breakdown if you have not picked a distro yet.

By the end you will have signature updates running on their own, a clamd daemon listening on a locked-down Unix socket, a nightly systemd timer that scans and quarantines, real-time blocking through clamonacc, an alert hook, and the upload gate. You will also have a troubleshooting list for the errors that cost most people an afternoon.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What ClamAV Is and Why It Still Matters in 2026

ClamAV is a GPL-2.0 antivirus engine, not a full endpoint product. It ships a command-line scanner (clamscan), a daemon (clamd) with a thin client (clamdscan), a signature updater (freshclam), an on-access scanner (clamonacc) and a mail filter (clamav-milter). The engine matches files against signature databases distributed as .cvd and .cld files. It also unpacks archives, documents and executables so it can look inside them.

That design makes it a good fit for the places where Windows-style endpoint agents do not run. Think file servers that Windows and macOS clients write to, web applications that accept user uploads, mail gateways, CI runners that unpack third-party artifacts, and Linux workstations that exchange files with people who run other operating systems. You scan content on its way through, rather than protecting a user session.

It also has limits, and you should know them before you rely on it. Signature scanning catches known samples. It will miss freshly compiled malware, living-off-the-land attacks and anything fileless. ClamAV is a detection layer for files, so treat it as one control next to patching, least privilege and monitoring.

Cisco Talos explained the retirement as a response to the cost of distributing a database that had grown without bound since 2002. The team judges which signatures still earn their place by watching match rates across its own and partner data feeds over an extended period, and it says it will restore any signature if new attacks call for them. It also plans to make retired signatures available later for researchers. The announcement used the following figures.

Database fileSeptember 2025 sizeDecember 2025 size (after retirement)Change
main.cvd163 MBabout 80 MBroughly half
daily.cvd62 MBabout 22 MBroughly two thirds smaller
Docker Hub image storageover 300 GiB hostedonly supported versions keptold tags removed

The same post told container users to pin a feature-release tag such as 1.5 instead of a specific minor version, so security fixes arrive without edits to your compose file. I come back to that advice in the hardening step.

Prerequisites and Versions

You need a Linux host where you have sudo, a working internet connection to reach the ClamAV update network, and enough free memory for clamd to load its database. Plan on at least 1 GB of free RAM for the daemon, and more if you scan large archives with many threads. The tutorial was written against the versions below. Distribution packages often trail upstream, so check what your package manager actually installs.

ComponentVersion usedNotes
ClamAV1.5.4 (feature line) or 1.4.6 (LTS line)Both released August 7, 2026 with the same security fixes
Linux kernel3.8 or newerRequired for on-access scanning through fanotify
libcurl7.45 or newerRequired by clamonacc according to the ClamAV docs
Python3.10 or newerFor the upload gate, standard library only
systemdAny current releaseUsed for timers and service units
Test fileEICAR standard test stringHarmless file that every scanner flags on purpose

The commands assume Ubuntu or Debian. If you are weighing a desktop distro for a small office server, the Linux Mint vs Ubuntu comparison covers the package base differences that matter here. You also need root or sudo for the daemon steps, a text editor, and about 2 GB of free disk space for the databases and logs.

Steps 1-2: Pick a Release Line and Install ClamAV

Step 1: choose between the feature line and the LTS line. ClamAV runs two supported tracks side by side. The 1.5 series gets new features and the 1.4 series is a long-term support line that takes only fixes. Both received the August 2026 security release, so for a production server the practical rule is simple. Take the newest patch of whichever line your distribution ships, and move to the newest line only when you want its features. The table below shows the recent patch history from the project’s release page.

ReleaseDateLineWhat it did
1.5.4August 7, 2026FeatureFixed CVE-2026-20337 and CVE-2026-20338 (ZIP handling), CVE-2026-20345 (GPT names), CVE-2026-20339 (PESpin), CVE-2026-20346 (PDF), CVE-2026-20347 (Mach-O), CVE-2026-20348 (XAR), CVE-2025-8088 (UnRAR) and the clamd STATS race
1.4.6August 7, 2026LTSSame parser fixes, minus the bugs that only hit the 1.5 series
1.5.3July 1, 2026FeaturePrevious patch, affected by the bugs above
1.5.2March 4, 2026FeatureEarlier patch

Read that table as a patching signal. Several of those flaws live in file parsers, which means the thing you built to inspect hostile files can itself be attacked by a malformed archive. Run an old build and your scanner becomes part of your attack surface. As of October 25, 2026, 1.5.4 is the newest release I found on the project’s page.

Step 2: install the packages. Debian splits ClamAV into several packages, and the project’s own packaging notes say most people only need clamav and clamav-daemon. I also install clamav-freshclam so updates run as a managed service, and clamav-testfiles for sample detections.

sudo apt update
sudo apt install -y clamav clamav-daemon clamav-freshclam clamav-testfiles

# confirm what you actually got
clamscan --version
dpkg -l | grep -i clam

The first command prints the engine version followed by the signature database version and date. If the engine number is older than 1.4.6 or 1.5.4, your distribution is behind the August fixes. You can still use it for learning, but plan to move to a newer package source or build before you point it at untrusted uploads. RAR support comes from a separate library under a restrictive license. On Debian it sits in the non-free archive as libclamunrar9, so install it if your users send RAR files.

Rolling-release distros tend to ship newer packages than fixed-release ones, which is one practical argument in the Arch Linux vs Ubuntu debate when the tool in question needs frequent security fixes. On Fedora, RHEL and compatible systems, the packages are clamav, clamd and clamav-update, the daemon config lives under /etc/clamd.d/ and the unit is clamd@scan. Everything else in this guide maps over once you swap the paths.

Steps 3-4: Run the First Signature Update and Enable FreshClam

Step 3: stop the updater service and update by hand once. The Debian and Ubuntu packages start the freshclam daemon on install, and that daemon holds a lock on its log file. Running the command manually while it is active fails with a message that the log is locked by another process. Stop the service, pull the databases, then start it again.

sudo systemctl stop clamav-freshclam
sudo freshclam
sudo systemctl start clamav-freshclam

A healthy first run downloads main.cvd, daily.cvd and bytecode.cvd, then reports each one as updated. The numbers will differ on your machine, but the shape should look like this.

ClamAV update process started at Sun Oct 25 14:02:11 2026
daily.cvd updated (version: ..., sigs: ..., f-level: ..., builder: ...)
main.cvd updated (version: ..., sigs: ..., f-level: ..., builder: ...)
bytecode.cvd updated (version: ..., sigs: ..., f-level: ..., builder: ...)
Database updated (... signatures) from database.clamav.net

Step 4: confirm the updater runs on a schedule. The packaged freshclam.conf already points at database.clamav.net and checks for new signatures 24 times a day, which is hourly. That is plenty. Raising the count buys nothing and can trip the CDN’s rate limiting, which answers with a 429 or 403 error and a cool-down period. Verify the service, the database folder and the config in a few seconds.

systemctl status clamav-freshclam --no-pager
ls -lh /var/lib/clamav
grep -Ev '^(#|$)' /etc/clamav/freshclam.conf

You should see the three .cvd files, or .cld files once incremental updates have been applied, a service marked active, and a short config that includes DatabaseMirror, Checks and NotifyClamd lines. NotifyClamd matters because it tells the running daemon to reload signatures after each update. Without it the daemon keeps scanning with stale data until you restart it. If you ever see the file list shrink or the sizes look like the old 163 MB main.cvd, your mirror or cache is serving outdated content, so rerun freshclam and check your outbound proxy.

Steps 5-6: Configure and Start clamd

Step 5: set the socket, limits and exclusions. The daemon reads /etc/clamav/clamd.conf on Debian-family systems. If you installed from source, the docs tell you to generate your own with clamconf -g clamd.conf and delete the line that says Example, because clamd refuses to start while that line is present. The settings below are the ones that matter on day one. Edit them with sudoedit.

sudoedit /etc/clamav/clamd.conf

# --- values to set or confirm ---
User clamav
LocalSocket /var/run/clamav/clamd.ctl
LocalSocketGroup clamav
LocalSocketMode 660
FixStaleSocket true

MaxFileSize 100M
MaxScanSize 400M
StreamMaxLength 100M
MaxThreads 12
MaxQueue 200

LogFile /var/log/clamav/clamav.log
LogTime yes
LogRotate yes

ExcludePath ^/proc/
ExcludePath ^/sys/
ExcludePath ^/dev/

A few of these deserve a sentence each. LocalSocketMode 660 with LocalSocketGroup clamav means only root, the clamav user and members of the clamav group can talk to the daemon, which is exactly what you want when an application account needs scan access. StreamMaxLength caps how many bytes clamd accepts through the INSTREAM command, and the upload gate in Step 12 depends on it, so it must be at least as large as your biggest allowed upload. ExcludePath takes regular expressions, and the three entries keep scans out of kernel pseudo-filesystems where reads are slow, meaningless or endless.

Step 6: start the daemon and wait until it is really ready. clamd reads and compiles the whole signature set at startup, which can take from several seconds to a couple of minutes depending on CPU and disk. During that window the socket is not answering, and clients get a connection error that looks like a failure. Use the built-in ping with a retry count instead of guessing.

sudo systemctl enable --now clamav-daemon

# wait up to 30 attempts, 2 seconds apart
clamdscan --ping 30:2

# talk to the socket directly
printf 'PING\n' | sudo nc -U /var/run/clamav/clamd.ctl
printf 'VERSION\n' | sudo nc -U /var/run/clamav/clamd.ctl

The direct commands should answer PONG and a version string that starts with ClamAV and your engine number. If the ping command gives up, read the last lines of /var/log/clamav/clamav.log and the output of journalctl -u clamav-daemon before changing anything. The most common culprits are a socket path mismatch, a config line left over from an old release, and a host so short on memory that the kernel kills the daemon during startup.

Step 7: Test the Install With the EICAR File

Never test an antivirus with real malware. The EICAR test string is a 68-byte text file that all scanners agree to flag, and it does nothing when executed. I split the string across two arguments in the command below so the article text itself does not match the signature, then printf joins them into the file.

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

# standalone scanner (loads signatures itself, slow start)
clamscan /tmp/eicar-test.com

# daemon client (uses the loaded database, fast)
clamdscan --fdpass /tmp/eicar-test.com
/tmp/eicar-test.com: Win.Test.EICAR_HDB-1 FOUND

----------- SCAN SUMMARY -----------
Infected files: 1
Time: 0.004 sec (0 m 0 s)

Two outcomes tell you what is wrong if the test fails. If clamscan flags the file but clamdscan does not, the daemon is running with an empty or failed database load, so check its log. If clamdscan reports it cannot access the file, you have a permissions problem, and the fix is the –fdpass flag, which makes the client hand an open file descriptor to the daemon. Without that flag the daemon user has to be able to read the path itself, and your home directory usually blocks it.

The table below sums up how the tools differ, because mixing them up is behind many of the complaints about ClamAV being slow.

ToolLoads databaseBest forNeeds clamd running
clamscanOn every runOne-off scans, cron jobs on small hostsNo
clamdscanUses the daemon’s copyFrequent scans, large trees, scriptsYes
clamonaccUses the daemon’s copyBlocking access to infected files in real timeYes
clamav-milterUses the daemon’s copyFiltering mail inside Sendmail or PostfixYes
freshclamWrites the databaseKeeping signatures currentNo, but notifies clamd

Step 8: Run Your First Real Scan

With the plumbing proven, scan something that matters. Start with the directories where outside content lands, such as upload folders, download directories, mail spools and web roots. A full-disk scan of a busy server is rarely the right first move because it burns I/O for little value and floods you with noise from package caches and container layers.

# standalone, recursive, only print infected files, skip pseudo-filesystems
sudo clamscan -r -i \
  --exclude-dir='^/(proc|sys|dev)' \
  --max-filesize=100M \
  --log=/var/log/clamav/manual-scan.log \
  /home /srv

# daemon-based, multi-threaded, good for big trees
sudo clamdscan --fdpass --multiscan -i /srv/uploads

The -i flag prints only infected files, which keeps the output readable. The exit code is the part scripts should care about: 0 means clean, 1 means at least one infection, and 2 means an error occurred. Here is what a clean run looks like. These counts come from a small lab directory, so yours will differ.

----------- SCAN SUMMARY -----------
Scanned directories: 214
Scanned files: 3187
Infected files: 0
Data scanned: 412.30 MB
Time: 31.8 sec (0 m 31 s)

Gaming boxes deserve a mention here. People who run Windows launchers through Wine, such as the setups in our Lutris setup guide, download executables, mods and cracked-installer bait from the open internet all the time. Pointing a scheduled scan at the downloads folder is cheap insurance, because Linux does not make a Windows trojan harmless when it sits on a shared drive that a Windows machine later opens.

Step 9: Schedule Scans With a systemd Timer

Cron works, but a systemd timer gives you logging in the journal, catch-up runs after downtime, and a failure state you can alert on. The wrapper script below checks that the daemon is up, scans three trees, moves anything infected into a quarantine folder, and returns clamdscan’s exit code so systemd marks an infection as a failed run.

sudo install -d -m 700 /var/quarantine

sudo tee /usr/local/bin/clam-nightly-scan.sh > /dev/null <<'EOF'
#!/bin/bash
set -uo pipefail
QUAR=/var/quarantine
LOG=/var/log/clamav/nightly-scan.log

clamdscan --ping 30:2 || exit 2

clamdscan --fdpass --multiscan --infected \
  --move="$QUAR" --log="$LOG" \
  /home /srv /var/www
rc=$?
exit "$rc"
EOF
sudo chmod 750 /usr/local/bin/clam-nightly-scan.sh
sudo tee /etc/systemd/system/clam-nightly-scan.service > /dev/null <<'EOF'
[Unit]
Description=Nightly ClamAV scan
After=clamav-daemon.service
Requires=clamav-daemon.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/clam-nightly-scan.sh
Nice=19
IOSchedulingClass=idle
EOF

sudo tee /etc/systemd/system/clam-nightly-scan.timer > /dev/null <<'EOF'
[Unit]
Description=Run the ClamAV scan every night

[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now clam-nightly-scan.timer
systemctl list-timers clam-nightly-scan.timer
sudo systemctl start clam-nightly-scan.service
journalctl -u clam-nightly-scan.service -n 20 --no-pager

Three choices in those units are deliberate. Nice and the idle I/O class keep the scan from starving your real workload. RandomizedDelaySec spreads load if you run the same timer across many hosts. Persistent=true runs a missed scan at the next boot, so a server that was off at 02:30 does not skip a night. The –move option relocates infected files instead of deleting them, which keeps evidence for your incident notes and protects you from a false positive wiping a legitimate file. The account that runs clamdscan needs write access to the quarantine folder, so check ownership if moves fail.

Steps 10-11: On-Access Scanning and Virus Alerts

Step 10: turn on real-time blocking with clamonacc. Scheduled scans find problems after the fact. On-access scanning blocks a read of an infected file the moment a process tries it. The ClamAV docs state that this feature exists only on Linux, needs kernel 3.8 or newer because it relies on fanotify, and needs libcurl 7.45 or newer. The prevention happens in kernel space, so it is stronger than a user-space watcher that reacts after the fact. clamonacc runs next to clamd and reuses the same connection settings from clamd.conf.

Add the following to /etc/clamav/clamd.conf. The docs call for four things: the paths to watch, prevention switched on, and an exclusion for the daemon’s own user so clamd does not trigger itself in a loop. I watch the upload folder only and leave the whole of /home out, because on-access scanning on a busy tree costs real CPU.

OnAccessIncludePath /srv/uploads
OnAccessPrevention yes
OnAccessExcludeUname clamav
OnAccessExcludeRootUID yes
OnAccessMaxThreads 8
command -v clamonacc    # adjust ExecStart below if the path differs

sudo tee /etc/systemd/system/clamonacc.service > /dev/null <<'EOF'
[Unit]
Description=ClamAV on-access scanner
After=clamav-daemon.service
Requires=clamav-daemon.service

[Service]
Type=simple
ExecStart=/usr/sbin/clamonacc -F --fdpass --log=/var/log/clamav/clamonacc.log
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl restart clamav-daemon
sudo systemctl enable --now clamonacc.service

# test: drop the EICAR file into the watched folder and read it
sudo cp /tmp/eicar-test.com /srv/uploads/
cat /srv/uploads/eicar-test.com
cat: /srv/uploads/eicar-test.com: Operation not permitted

That Operation not permitted message is the success signal. It means fanotify refused the open at kernel level and ClamAV logged the detection. The docs also suggest raising MaxQueue, MaxThreads and OnAccessMaxThreads in clamd.conf if default performance disappoints you and the host has spare resources. Desktop users get a related benefit. On immutable gaming distributions such as the one in our Bazzite install walkthrough, you can watch only the Downloads folder and leave system paths alone.

Step 11: make detections noisy. A quarantine folder nobody reads protects nobody. clamd can run any program when it finds something, through the VirusEvent option, and it passes the details in environment variables named CLAM_VIRUSEVENT_FILENAME and CLAM_VIRUSEVENT_VIRUSNAME. The script below writes to the system log and, if you set a webhook URL, posts a JSON message. It builds that JSON with jq so a hostile filename cannot break out of the string, which is the classic mistake in homemade alert scripts.

sudo apt install -y jq curl

sudo tee /etc/clamav/alert.env > /dev/null <<'EOF'
WEBHOOK_URL=
EOF
sudo chmod 640 /etc/clamav/alert.env
sudo chgrp clamav /etc/clamav/alert.env

sudo tee /usr/local/bin/clam-alert.sh > /dev/null <<'EOF'
#!/bin/bash
set -u
[ -r /etc/clamav/alert.env ] && . /etc/clamav/alert.env
FILE="${CLAM_VIRUSEVENT_FILENAME:-unknown}"
NAME="${CLAM_VIRUSEVENT_VIRUSNAME:-unknown}"

logger -t clamav-alert -p auth.warning "malware ${NAME} found in ${FILE}"

if [ -n "${WEBHOOK_URL:-}" ]; then
  jq -n --arg n "$NAME" --arg f "$FILE" \
    '{text: ("ClamAV found " + $n + " in " + $f)}' \
  | curl -fsS -m 10 -X POST -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL" || true
fi
EOF
sudo chown root:clamav /usr/local/bin/clam-alert.sh
sudo chmod 750 /usr/local/bin/clam-alert.sh

echo 'VirusEvent /usr/local/bin/clam-alert.sh' | sudo tee -a /etc/clamav/clamd.conf
sudo systemctl restart clamav-daemon

Test it by scanning the EICAR file again with clamdscan, then run journalctl -t clamav-alert -n 5. You should see one warning line with the signature name and the path. The script runs as the clamav user, which is why the file group is clamav and the mode is 750. If the hook silently does nothing, permissions or a mandatory access control profile are the first suspects, and the troubleshooting section covers both.

Step 12: Build an Upload-Scanning Gate in Python

This is the working project. Web applications are where most Linux malware scanning happens in practice, and the clean pattern is to scan the upload stream before you store or process it. clamd offers an INSTREAM command for exactly this. The client sends the bytes over the socket in length-prefixed chunks, and the daemon answers with a verdict. No file touches the disk, so there is nothing for an attacker to race. The project below uses only the Python standard library, so there is no dependency to patch and no package to vet.

The clamd client module

The protocol is small. You send the null-terminated command zINSTREAM, then each chunk as a 4-byte big-endian length followed by the data, then a zero length to finish. The reply is a line such as stream: OK or stream: Win.Test.EICAR_HDB-1 FOUND. One subtle case matters in production. If the upload is bigger than StreamMaxLength, clamd answers with an error and closes the socket while you are still sending, so the client must catch the broken pipe and still read the reply.

# /opt/uploadgate/clamd_client.py
import socket
import struct


class ClamdError(RuntimeError):
    pass


class ClamdClient:
    def __init__(self, path="/var/run/clamav/clamd.ctl", timeout=30.0, chunk=64 * 1024):
        self.path = path
        self.timeout = timeout
        self.chunk = chunk

    def _connect(self):
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock.settimeout(self.timeout)
        sock.connect(self.path)
        return sock

    @staticmethod
    def _read_reply(sock):
        data = b""
        while not data.endswith(b"\0"):
            part = sock.recv(4096)
            if not part:
                break
            data += part
        return data.rstrip(b"\0").decode("utf-8", "replace").strip()

    def ping(self):
        with self._connect() as sock:
            sock.sendall(b"zPING\0")
            return self._read_reply(sock) == "PONG"

    def version(self):
        with self._connect() as sock:
            sock.sendall(b"zVERSION\0")
            return self._read_reply(sock)

    def scan_stream(self, fileobj):
        """Return ("clean", None) or ("infected", signature). Raise ClamdError otherwise."""
        with self._connect() as sock:
            sock.sendall(b"zINSTREAM\0")
            try:
                while True:
                    chunk = fileobj.read(self.chunk)
                    if not chunk:
                        break
                    sock.sendall(struct.pack("!I", len(chunk)) + chunk)
                sock.sendall(struct.pack("!I", 0))
            except (BrokenPipeError, ConnectionResetError):
                pass  # clamd closed early, its reply explains why
            reply = self._read_reply(sock)

        if reply.endswith("FOUND"):
            signature = reply.split(": ", 1)[-1][: -len(" FOUND")]
            return "infected", signature
        if reply.endswith("OK"):
            return "clean", None
        raise ClamdError(reply or "empty reply from clamd")

The HTTP gate

The gate exposes two routes. POST /scan takes the raw upload as the request body and answers 200 for clean, 422 for infected, 413 for oversize and 503 when the scanner cannot be reached. That last choice is the important one. A scanner that fails open turns an outage into a bypass, so the gate fails closed and your application should treat any answer other than 200 as a rejection. GET /health reports whether clamd answers a ping, which suits a load balancer check.

# /opt/uploadgate/upload_gate.py
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from clamd_client import ClamdClient, ClamdError

MAX_BYTES = int(os.environ.get("GATE_MAX_BYTES", 50 * 1024 * 1024))
BIND = os.environ.get("GATE_BIND", "127.0.0.1")
PORT = int(os.environ.get("GATE_PORT", "8081"))
client = ClamdClient(os.environ.get("CLAMD_SOCKET", "/var/run/clamav/clamd.ctl"))


class Limited:
    """File-like wrapper that stops after exactly `left` bytes of the request body."""

    def __init__(self, rfile, left):
        self.rfile = rfile
        self.left = left

    def read(self, size):
        size = min(size, self.left)
        if size <= 0:
            return b""
        data = self.rfile.read(size)
        self.left -= len(data)
        return data


class Gate(BaseHTTPRequestHandler):
    server_version = "UploadGate/1.0"

    def _send(self, code, payload):
        body = json.dumps(payload).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        if self.path != "/health":
            return self._send(404, {"error": "not found"})
        try:
            up = client.ping()
        except OSError:
            up = False
        self._send(200 if up else 503, {"clamd": "up" if up else "down"})

    def do_POST(self):
        if self.path != "/scan":
            return self._send(404, {"error": "not found"})
        try:
            length = int(self.headers.get("Content-Length", "0"))
        except ValueError:
            return self._send(400, {"error": "bad Content-Length"})
        if length <= 0:
            return self._send(400, {"error": "empty body"})
        if length > MAX_BYTES:
            return self._send(413, {"error": "too large", "limit": MAX_BYTES})
        try:
            verdict, signature = client.scan_stream(Limited(self.rfile, length))
        except (ClamdError, OSError) as exc:
            return self._send(503, {"status": "scanner_unavailable", "detail": str(exc)})
        if verdict == "infected":
            return self._send(422, {"status": "infected", "signature": signature})
        self._send(200, {"status": "clean"})

    def log_message(self, fmt, *args):
        pass


if __name__ == "__main__":
    ThreadingHTTPServer((BIND, PORT), Gate).serve_forever()

Run it as a locked-down service and smoke-test it

Create a dedicated account, add it to the clamav group so it can open the socket, and let systemd sandbox the process. Keep GATE_MAX_BYTES at or below the StreamMaxLength you set in Step 5, otherwise clamd rejects files that your gate happily forwards.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin uploadgate
sudo usermod -aG clamav uploadgate
sudo install -d /opt/uploadgate
sudo cp clamd_client.py upload_gate.py /opt/uploadgate/

sudo tee /etc/systemd/system/uploadgate.service > /dev/null <<'EOF'
[Unit]
Description=ClamAV upload gate
After=clamav-daemon.service
Requires=clamav-daemon.service

[Service]
User=uploadgate
SupplementaryGroups=clamav
WorkingDirectory=/opt/uploadgate
ExecStart=/usr/bin/python3 /opt/uploadgate/upload_gate.py
Environment=GATE_MAX_BYTES=52428800
Restart=on-failure
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now uploadgate.service

curl -s http://127.0.0.1:8081/health
curl -s -o /dev/null -w '%{http_code}\n' -X POST --data-binary @/etc/hostname http://127.0.0.1:8081/scan
curl -s -X POST --data-binary @/tmp/eicar-test.com http://127.0.0.1:8081/scan
{"clamd": "up"}
200
{"status": "infected", "signature": "Win.Test.EICAR_HDB-1"}

Wire it into your application by forwarding each upload to POST /scan before writing it anywhere permanent, and refuse anything that does not return 200. The gate listens on localhost only, so put it on the same host as the app, or add TLS and authentication before you expose it. If your stack is not Python, the protocol above ports to any language in about thirty lines.

Step 13: Harden ClamAV and Keep It Patched

An antivirus daemon parses untrusted input as a full-time job, so it deserves the same care as a web server. The August 2026 release is the proof, since its fix list is mostly parser bugs that a malformed archive could trigger. Work through this checklist once and add it to your build image or configuration management.

  • Keep the socket local. Use LocalSocket with mode 660 and a group. Do not enable TCPSocket unless a remote client truly needs it, and if you do, bind it to a private interface and firewall port 3310. The INSTREAM protocol has no authentication.
  • Run unprivileged. The packaged daemon runs as the clamav user. Do not change it to root to make a permissions problem disappear.
  • Pin container tags to a feature line. If you use the official image, follow the project’s advice and pin 1.5 or 1.4 instead of a minor version, then rebuild on a schedule so fixes land.
  • Patch on a clock. Subscribe to the release feed, enable unattended security upgrades, and restart clamd after the package updates, since a running daemon keeps the old binary in memory.
  • Respect SELinux and AppArmor. On RHEL-family systems, enable the antivirus_can_scan_system boolean only if you need full-disk scans. On Ubuntu, read the AppArmor profile for clamd before you add scan paths.
  • Log and rotate. Keep LogRotate on, ship /var/log/clamav to your log pipeline, and alert on FOUND lines.

A short audit command keeps you honest. Run it from monitoring or by hand after each patch window.

clamscan --version
systemctl is-active clamav-daemon clamav-freshclam clamonacc uploadgate
sudo find /var/lib/clamav -name '*.c[vl]d' -mtime -2 | wc -l
journalctl -t clamav-alert --since '24 hours ago' --no-pager | tail -n 5

If the find command prints 0, no database file changed in two days and your updater is broken or blocked. ClamAV also covers a different layer than a vulnerability scanner. It looks for malicious files, while a tool such as the one in our OpenVAS Greenbone setup guide finds missing patches and exposed services on the host itself. Running both gives you coverage on two axes.

7 Common Pitfalls That Break ClamAV Deployments

1. Running freshclam by hand while the service is active. The log lock error looks like a network failure, but it is a collision with your own daemon. Stop the service first, as in Step 3.

2. Starving the host of memory. clamd holds the full signature set in RAM, and during a database reload it briefly holds the old and new copies together. A 1 GB virtual machine with a database and an application can hit the out-of-memory killer at the exact moment freshclam notifies the daemon. Give the box headroom or add swap as a safety net.

3. Using clamscan in a tight loop. Each clamscan run reloads every signature, which takes seconds of CPU. Anything that scans more than a few times an hour should call clamdscan or talk to the socket instead.

4. Forgetting –fdpass. The daemon user cannot read files in a private home directory, so clamdscan reports access errors that look like missing files. File descriptor passing fixes it without loosening permissions. Anyone who scans a game library kept in a private home folder, such as one managed with the Heroic Games Launcher, can run into it.

5. Watching too much with on-access scanning. Pointing clamonacc at a whole home directory or a build cache slows every file open and can freeze a busy machine. Watch narrow, high-risk folders and leave bulk checks to the nightly timer.

6. Mismatched size limits. If your web server accepts larger uploads than the scanner is configured to take, big files can be rejected at the scanner in a way your users experience as a random upload bug. Check the size limits in your clamd configuration against your web server’s, set them in one place and document them.

7. Treating a clean scan as a clean bill of health. Signature engines miss new and targeted malware. A pass from ClamAV tells you the file matched nothing known, which is useful and nothing more. Keep your other controls.

Troubleshooting: 10 ClamAV Errors and Fixes

1. Could not connect to clamd on LocalSocket right after boot or restart. The daemon is still loading signatures. Wait with clamdscan –ping 30:2 and add the same call to any script that scans at startup. If it never comes up, read journalctl -u clamav-daemon for the real error.

2. freshclam.log is locked by another process. The packaged updater is running. Stop clamav-freshclam, update manually, start it again.

3. freshclam reports error 429 or 403 from the CDN. You were rate limited or blocked, usually from checking too often or from many hosts sharing one public address. Wait out the cool-down, keep Checks at 24 or lower, and for a fleet mirror the databases once with the cvdupdate tool and point your hosts at the internal mirror.

4. clamdscan says Can't open file or directory for files that exist. The daemon user lacks read access. Add –fdpass, or scan from a path the clamav user can reach.

5. clamd refuses to start with a config error. On source builds the sample file contains a line reading Example, which must be deleted. After a package upgrade, also check for options that the new release dropped, and run clamconf to validate what the daemon actually parses.

6. The socket file is missing or in a different place. Debian and Ubuntu use /var/run/clamav/clamd.ctl. Fedora and RHEL builds use a path under /run/clamd.scan/ for the scan instance. Match LocalSocket in clamd.conf, your client and your service units.

7. The daemon dies during a reload. This is the memory spike from pitfall 2. Check dmesg for oom-killer lines, add RAM or swap, and consider lowering MaxThreads on small hosts.

8. clamonacc does not block anything. Check the kernel version and libcurl version against the requirements, confirm OnAccessPrevention is yes, make sure the path is inside OnAccessIncludePath, run it with elevated privileges, and verify the file owner is not the excluded clamav user.

9. INSTREAM size limit exceeded. The upload is larger than StreamMaxLength. Raise it in clamd.conf, restart the daemon, and keep your gate’s maximum at or below the new value.

10. The VirusEvent script never runs. The path must be absolute, the file executable by the clamav user, and not blocked by AppArmor or SELinux. Check journalctl for denials, and test the script by hand with the two environment variables set.

11. RAR module is not available warnings. RAR support ships separately. On Debian enable the non-free archive and install libclamunrar9, then restart clamd.

Advanced Tips for Bigger Deployments

Write your own signatures. sigtool can hash a known-bad file and produce a database entry that clamd loads from the database folder. This is the fastest way to block a specific sample your team found during an incident.

sigtool --md5 suspicious.bin | sudo tee -a /var/lib/clamav/local.hdb
sudo clamdscan --reload
clamdscan --fdpass suspicious.bin

Automate across a fleet. Configuration management keeps every host on the same version and config. CFEngine, for example, published a guide to its ClamAV module for antivirus scanning at scale, and the same ideas apply to Ansible or Puppet. Enforce the package version, the clamd.conf template and the timer unit, then alert on drift.

Scan mail with the milter. clamav-milter plugs into Sendmail and Postfix and rejects infected messages during the SMTP conversation, which beats cleaning a mailbox later. It reads its own config file and talks to the same clamd.

Run it in a container for isolation. A containerized daemon on 127.0.0.1:3310 keeps parsing risk away from the host, and your applications can scan through the INSTREAM protocol over TCP. Pin the 1.5 or 1.4 tag, mount a volume for the signature database so restarts do not redownload everything, and never publish port 3310 to the internet.

Pair it with the rest of your stack. File scanning is one slice of host defense, so browse the wider cybersecurity coverage for patching, detection and hardening guides that complete the picture. Also test your tuning. Time a scan of a representative folder before and after you change MaxThreads or the exclusion list, and keep the numbers.

FAQ: ClamAV on Linux

Is ClamAV free to use for commercial servers? Yes. The engine is released under GPL-2.0 and the signature databases are distributed free through the project’s update network. Check the license terms of any optional components, such as the non-free RAR library on Debian, against your own policy.

What is the current ClamAV version? As of October 25, 2026, the newest release listed on the project’s GitHub page is 1.5.4, published August 7, 2026. The LTS line received 1.4.6 on the same day. Run clamscan –version to see what your system has.

Do I need clamd, or is clamscan enough? For a nightly scan on a small machine, clamscan is fine. If you scan often, handle uploads, or want on-access protection, run the daemon, because it loads the signatures once and reuses them.

How much RAM does ClamAV need? Budget at least 1 GB free for the daemon and more for heavy parallel scanning. Memory spikes briefly when signatures reload, so leave margin. The smaller databases after the signature retirement help, but the project did not publish a fixed memory figure.

Can ClamAV protect a Linux desktop in real time? Yes, through clamonacc on Linux kernel 3.8 or newer. Watch specific folders such as Downloads, not the whole home directory, to keep the desktop responsive.

Does ClamAV replace an EDR product? No. It is a signature-based file scanner. It does not watch process behavior, network activity or memory. Use it as a file-inspection layer next to patching, monitoring and least-privilege design.

Why did the signature database shrink in late 2025? Cisco Talos retired older signatures that no longer match current threats, citing rising distribution costs. The announcement put main.cvd at about 80 MB and daily.cvd at about 22 MB after the first pass, and said the team would restore signatures if needed.

How do I know the upload gate really blocks infected files? Send the EICAR test file to POST /scan and expect HTTP 422 with the signature name. Then stop clamd and send a clean file, and expect 503. Both results prove the gate blocks bad files and fails closed during an outage.

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles