Security for mailcow — user guide
Installation
- The module ships in mailcow Toolkit 0.6.0 or newer (one package, one license per product). On the mailcow host, as root, run the Toolkit installer as usual:
sudo ./install.sh --mailcow-dir /opt/mailcow-dockerized --name <instance>, or update the Toolkit from System → Updates (Toolkit). The installer copiesdata/web/mlw-security/, creates the module's tables in mailcow's database, adds the menu item and the once-a-minute timermlw-mailcow-security-<instance>.timer, and reloads php-fpm. - Open System → Security in the mailcow admin menu.
License
Open System → Updates (Toolkit) and find the Security for mailcow card: paste the key and activate it, or start the 30-day trial. On the shell: php /web/mlw-toolkit/cli.php activate mailcow-security <KEY> inside php-fpm-mailcow. The license is checked daily; without internet the module works for 7 days after the last good check.
Without a valid license nothing is read, detected, banned or sent; bans that were made earlier are still lifted when they expire, and the history stays. Nothing is deleted.
The first run
The first run imports what mailcow still keeps in its logs (usually the last hours) and neither bans nor alerts. From then on the job reads only new lines, every minute. Overview → Status shows the last run; if a line says that lines were lost, mailcow trimmed its logs before they were read (a very busy server): raise LOG_LINES in mailcow.conf.
What is read
| Source | What becomes a record |
|---|---|
dovecot (redis DOVECOT_MAILLOG) |
failed IMAP / POP3 / ManageSieve logins (with the attempt count), connections that never log in, successful logins |
postfix (redis POSTFIX_MAILLOG) |
failed SMTP AUTH, authenticated sends, rspamd rejects, DNS blocklist rejects, relay attempts, protocol probes (lost connection after EHLO, pipelining, TLS errors, postscreen) |
SOGo (redis SOGO_LOG) |
failed and successful webmail logins |
fail2ban (redis NETFILTER_LOG) |
every ban mailcow's own fail2ban makes |
| web server (docker log, through the host agent) | 4xx answers: known exploit paths (/.env, /.git, wp-login.php, phpMyAdmin, …) and other 404s |
| php-fpm (docker log, through the host agent) | failed mailcow UI and API logins |
Records of private addresses (mailcow's own containers, a reverse proxy in front of mailcow) and of the networks you ignore are dropped. Records are counted per minute, address, mailbox and source; the history is kept 90 days by default (Settings).
Overview
- Totals of the last 24 hours per kind; a click opens the matching events.
- Last 14 days: failed logins, scans and spam per day (the values are under Values).
- Status: mode (monitor only or automatic protection), active bans, mailcow's own fail2ban bans, open incidents, the last run.
- Most active addresses with a Ban button, targeted mailboxes, active incidents.
Rules and incidents
A rule counts records in a sliding window. When the threshold is reached it opens an incident; while the attack goes on, the same incident is extended, not repeated.
| Rule | Counts | Default |
|---|---|---|
| Web login brute force | failed SOGo / mailcow UI / API logins per address | 5 in 15 min → ban 24 h |
| Slow brute force | failed logins per address over a day | 20 in 24 h → ban 24 h |
| Password spraying | distinct mailboxes one address failed on | 5 in 60 min → ban 7 days |
| Repeat offender | mailcow fail2ban bans of one address | 3 in 7 days → ban 30 days |
| Web exploit scan | requests for exploit / leak paths | 3 in 30 min → ban 7 days |
| Web 404 scan | 404 answers per address | 40 in 30 min → alert |
| Relay probing | "Relay access denied" per address | 3 in 60 min → ban 7 days |
| Protocol scan | connections that never log in or break the protocol | 30 in 60 min → alert |
| Spam flood | rspamd and blocklist rejects per address | 20 in 60 min → alert |
| Targeted mailbox | failures on one mailbox from at least 5 addresses | 30 in 60 min → alert |
| Sending spike | messages sent by one mailbox | 300 in 60 min → alert |
| Logins from many addresses | distinct addresses one mailbox signs in from | 8 in 60 min → alert |
| Login after failures | a first login from an address that failed on the mailbox | 3 failures in 24 h → alert (critical) |
In Rules you can turn each rule off and change its threshold, window, action and ban duration (0 = permanent). Address rules can alert or ban; mailbox rules can alert, limit sending or disallow login. Reset to defaults brings them all back.
Incidents lists them by status (open, seen, resolved, false positive), minimum severity and rule. False positive puts the address on the allowlist and lifts its bans.
Automatic actions
The switch at the top of Rules is off at first: the module only watches, and every incident says what would have been done ("would ban"). Leave it like that for a few days, look at the incidents, adjust the rules, then turn it on. Incidents whose attack is still going on are acted on at the next run.
Never banned automatically:
- private and reserved addresses;
- the module's allowlist (Settings) and mailcow's fail2ban allowlist (Configuration → Fail2ban);
- the server's own public addresses (from the Toolkit's reverse DNS check);
- with protect known addresses on (default), addresses that signed in successfully to a mailbox in the last 30 days — a colleague who mistypes a password is not locked out.
Bans
A ban puts the address or network into mailcow's fail2ban denylist. mailcow's netfilter applies it in the firewall on every mailcow port within a minute, and you see it on mailcow's own Fail2ban page. The module lifts it when it expires; a ban you delete on mailcow's Fail2ban page counts as lifted.
Bans shows the module's bans (reason, by whom, until when, Cloudflare state), mailcow's own temporary fail2ban bans (Lift or Make it longer), the rest of mailcow's denylist and the history. The form at the top bans an address or a network (CIDR) for 1 hour to 30 days, or permanently; your own current address is refused.
If Manage external is on in mailcow's fail2ban settings, netfilter does not touch the firewall: the page warns you, and your own firewall has to apply mailcow's list.
Cloudflare
When the mailcow web hosts (the UI, webmail) are behind Cloudflare's proxy, web attacks reach mailcow from Cloudflare's addresses, and netfilter cannot stop them. In Settings → Cloudflare choose bans of the web rules and ticked manual bans or every ban: each ban then also becomes an account-level Cloudflare IP Access Rule ("block"), covering every zone of the account, removed together with the ban. It uses the Cloudflare credentials saved in System → DNS & Cloudflare; the token needs Account → Account Firewall Access Rules → Edit (the Global API Key can do it). Cloudflare blocks single addresses and the ranges /16 and /24 (IPv4), /32, /48 and /64 (IPv6).
One address, one mailbox
Click any address to see its records, the mailboxes it tried, its successful logins, incidents and bans, its reverse DNS and links to AbuseIPDB, ipinfo and Shodan; ban it, lift its ban or put it on the allowlist (optionally on mailcow's too).
Click any mailbox to see the addresses that tried it, with failures and successful logins side by side (a warning sign marks an address with both), its sends, logins and incidents. Actions, all through mailcow's own functions and logged by mailcow too:
- Disallow login: the mailbox keeps receiving mail, nobody can sign in (mailcow's "disallow login"); Allow login again undoes it.
- End IMAP / POP3 sessions: dovecot disconnects the sessions now (through the host agent).
- Require a password change at the next sign-in.
- Limit sending to N messages per hour (Settings), or remove the limit.
Alerts
In Settings → Alerts: the e-mail address (empty = the Toolkit's notification address), the Telegram bot token (encrypted; "-" removes it) and chat id, the minimum severity (default: high), the most alerts per hour (the rest are counted in the next alert), the language (English or Romanian) and the daily summary (after 07:00). Send a test alert checks both channels. Each new incident at or above the severity is one alert, with a link to it.
Audit log and shell
Audit log lists every action: bans, lifts, allowlist changes, incident statuses, account actions, rule and settings changes — by an administrator or by a rule.
Inside php-fpm-mailcow (docker compose exec -u www-data php-fpm-mailcow …):
php /web/mlw-security/cli.php status
php /web/mlw-security/cli.php ban 203.0.113.7 48 "reason"
php /web/mlw-security/cli.php unban 203.0.113.7
Uninstall
sudo ./uninstall.sh lifts every ban of the module first, then removes the files and timers. The tables mlw_sec_* stay in mailcow's database (and in its backups).