Stalwart + ClamAV
| Description / name | Input element |
|---|---|
| Container Configuration Root Path | |
| User ID | |
| Group ID |
This guide adds virus scanning to a Stalwart mail server with the ClamAV image. Infected messages are rejected while the sender is still connected, so they never reach a mailbox.
How it fits together
The ClamAV image runs three services:
| Service | Port | Role |
|---|---|---|
clamd |
3310 |
The scanning engine; holds the signatures in memory |
clamav-milter |
7357 |
Speaks the milter protocol that Stalwart uses for filters |
freshclam |
none | Keeps the signatures current and tells clamd to reload |
Stalwart hands each incoming message to clamav-milter at the SMTP DATA
stage. The milter scans it through clamd and answers:
- Clean: the message is accepted, with
X-Virus-ScannedandX-Virus-Status: Cleanheaders added. - Infected: Stalwart rejects the message with
503 5.5.3 Message rejected., and ClamAV logsMessage from <...> to <...> infected by <signature>. Attachments are scanned too, not just the message body. - Scanner unavailable: Stalwart defers the message with
451 4.3.5 Unable to accept message at this time., so the sending server retries later instead of the mail arriving unscanned.
Requirements
- Memory: clamd keeps every signature in RAM, about 1.4 GB, and briefly holds a second copy while it reloads after an update. Plan for a few GB.
- Container DNS: Stalwart reaches ClamAV by its container name. On FreeBSD
that needs container name resolution; see
Networking. Without it, use the ClamAV container's IP
address instead of
clamavbelow.
1. Run both containers
Save as compose.yaml:
2. Wait for the first signature download
On its first start, freshclam downloads the full signature database (a few
hundred MB) into /path/to/containers/clamav/db. clamd and the milter wait for it:
It's ready once you see [clamav-milter] starting on port 7357. Later restarts
reuse the downloaded database and come up in seconds.
Keep /config persistent
ClamAV's mirrors rate-limit hosts that download the whole database
repeatedly. Recreating the container with an empty /config each time will
eventually get you temporarily blocked.
3. Make Stalwart listen on IPv4
Stalwart 0.16 listens on IPv6 only on FreeBSD
A fresh Stalwart 0.16 creates its listeners (SMTP, IMAP, submission, ...)
bound to [::] only. On Linux that also accepts IPv4, but FreeBSD
containers keep net.inet6.ip6.v6only=1, so nothing answers on IPv4
and mail sent to your published port 25 never arrives. Add an IPv4 bind to
each listener you use.
In the web admin, edit each listener and add the IPv4 address next to the
IPv6 one (for SMTP: 0.0.0.0:25 alongside [::]:25). Or do it with
stalwart-cli, for example for SMTP:
Restart the Stalwart container afterwards so the new binds take effect.
4. Add the milter in Stalwart
If Stalwart is new, finish its setup wizard first (see the Stalwart image page). Then in the web admin, open Settings › MTA › Filters › Milters and add a milter:
| Field | Value |
|---|---|
| Hostname | clamav |
| Port | 7357 |
| Stages | data only |
| Use TLS | off (the connection stays on the container network) |
| Temp-fail on error | on (the default: defer mail if ClamAV is down) |
Leave the timeouts and protocol version at their defaults, and save.
Only the data stage matters for virus scanning: that's the stage where
Stalwart passes the message body to the milter.
The same milter with stalwart-cli:
5. Test it
Send a message containing the EICAR test string from another host. It's a harmless file every antivirus flags. With swaks:
The server should answer 503 5.5.3 Message rejected., and ClamAV should
log the hit:
A normal message goes through and arrives with an X-Virus-Status: Clean
header.
Tuning
The milter's settings are in /path/to/containers/clamav/clamav-milter.conf,
written on first start. Edit it and restart the container.
| Option | Image default | Notes |
|---|---|---|
OnInfected |
Reject |
ClamAV's own default is Quarantine; this image uses Reject because the milter quarantine action isn't handled the same way by every MTA |
OnFail |
Defer |
What to do if clamd can't scan: Defer (retry later), Accept or Reject |
AddHeader |
Replace |
Adds X-Virus-Status and replaces any the sender forged |
MaxFileSize |
100 MB | Larger messages pass through unscanned. Keep it at least as large as Stalwart's maximum message size |
The full list of options is in /usr/local/etc/clamav-milter.conf.sample inside
the container.