Security for mailcow — user guide

Installation

  1. 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 copies data/web/mlw-security/, creates the module's tables in mailcow's database, adds the menu item and the once-a-minute timer mlw-mailcow-security-<instance>.timer, and reloads php-fpm.
  2. 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).

Download requires a license or the free 30-day trial.