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.
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.
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 loaded | about 1.0 to 1.6 GB |
freshclam testing a downloaded update, a second full engine, four times a day | about 0.6 to 1.0 GB |
| Peak, during an update | 2.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.
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