Skip to main content

Virus scanning

Virus scanning is optional: the elpan-clamav package. Installed, it runs ClamAV in a container and the spam filter sends every attachment to it, on mail coming in and on mail going out. Removed, scanning stops and nothing is left pointing at a scanner that isn't there.

It is left out of the default install because of its memory: ClamAV holds its whole signature set in memory and needs an 8 GB server.

Install it​

At installation:

install.sh --with-clamav

Or later, on a server that runs:

dnf install elpan-clamav

Both refuse a server whose kernel sees less than 7 GiB of memory (an 8 GB server sees about 7.6). If you have checked what else lives on the server, you can override the package's check:

mkdir -p /etc/elpan/clamav
touch /etc/elpan/clamav/allow-small-host
dnf install elpan-clamav

Upgrades are never refused.

The package starts elpan-clamav.service on its first install and restarts both spam-filter instances to pick up the scanner rule. The container image ships inside the package, so nothing is pulled from a registry; the signatures don't. On first start the container downloads about 250 MB from database.clamav.net before the scanner answers, which can take up to 30 minutes.

A server without internet egress

The engine installs offline, the signatures don't. Point DatabaseMirror in /usr/lib/elpan/clamav/freshclam.conf at a mirror of your own, or the scanner has no database and scans nothing.

What it scans​

The spam filter (rspamd) runs twice on the server, once for incoming mail and once for outgoing mail, and both ask the scanner. A virus is a virus in either direction; outgoing scanning is what protects the server's own reputation when an account's website or mailbox is compromised.

Each attachment is scanned as a separate part; plain text and image parts aren't. Messages up to 80 MiB are scanned, the same bound as the spam filter's own; the scanner is given 20 seconds per message. A result is cached for two hours.

What a detection does​

A detected attachment adds the symbol CLAM_VIRUS with a weight of 100, against a reject threshold of 30 for incoming and 8 for outgoing mail, so the message is rejected on its score and no allow-list can rescue it: a recipient must not be able to allow-list their way into receiving malware. The reply names the file and the signature instead of calling it spam:

550 5.7.1 Message rejected: the attachment "invoice.doc" contains malware (Win.Test.EICAR_HDB-1).

For incoming mail the sending server gets that reply; for outgoing mail, the sending client or script does.

It fails open​

When the scanner is unreachable, still downloading its first signatures, or reloading them, the spam filter records CLAM_VIRUS_FAIL with a weight of 0 and the message goes through unscanned. That is deliberate: a forced deferral on a scan failure would turn one container's bad day into a deferral of every message on the server.

The signature set is updated four times a day. Each update blocks scans for a few seconds while the engine reloads, and those messages pass unscanned too.

Watch the container, not the symbol

The signal is the service, not the per-message symbol:

systemctl status elpan-clamav
podman healthcheck run elpan-clamav
journalctl -u elpan-rspamd-in -g CLAM_VIRUS_FAIL --since today

A scanner that stops answering is killed and restarted by its health check. A high count of CLAM_VIRUS_FAIL on a running service is what to look into.

A server that wants to fail closed can add a force_actions rule to /var/lib/elpan/rspamd/in/av.d/; it isn't the default.

Memory​

Two engines have to fit, not one:

Memory
clamd with its signature set loadedabout 1.0 to 1.6 GB
freshclam testing a downloaded update, a second full engine, four times a dayabout 0.6 to 1.0 GB
Peak, during an update2.0 to 2.6 GB
The service's hard limit (MemoryMax)4 GB

On a 3 or 4 GB server, the update peak is most of what the platform leaves for the accounts; that is the reason for the refusal above and for the package staying out of the elpan metapackage. See Sizing profiles.

Keep the hard limit, add no soft one

The service has MemoryMax and deliberately no MemoryHigh. A soft limit below the real peak throttles instead of killing: the container has no swap and almost no page cache, so the kernel reclaims the engines' own code and they fault it straight back in. One such update once wedged a server's scanner for eight days with the service reporting active (running) and every message delivered unscanned. Under the hard limit alone, an oversized update is an OOM kill and a restart you can see in the journal.

Check it​

podman exec elpan-clamav clamdcheck.sh
podman exec elpan-clamav freshclam --version
podman exec elpan-clamav ls -l /var/lib/clamav
podman exec elpan-rspamd-in rspamadm configdump antivirus
journalctl -u elpan-rspamd-in -g CLAM_VIRUS --since '5 min ago'

The first says whether the scanner answers; the next two show the signature freshness; the fourth shows the rule the spam filter loaded. To test end to end, mail the EICAR test file in as an attachment from outside and look for CLAM_VIRUS in the last line's output.

The scanner listens on the server's internal container network only, 10.91.3.6:3310; the firewall closes it to the accounts' shells and to the other containers, because the protocol has no authentication.

Remove it​

dnf remove elpan-clamav

The scanner rule goes with the package, both spam-filter instances are restarted without it, and scanning stops. The signature volume is kept, so a reinstall needn't download it again:

podman volume rm elpan-clamav-db