mailcow's built-in fail2ban does one thing well: it catches the loudest attacks. Ten failed logins in ten minutes earn a thirty-minute ban. Then it forgets them.
Everything quieter gets through. An address that tries one password every few minutes, all day. A botnet that spreads its guesses over hundreds of addresses so no single one trips the threshold. A scanner walking your webmail looking for /.env or wp-login.php. A mailbox whose password was guessed and now sends spam. Good mailcow brute force protection has to see all of that, remember it, and act only when you tell it to.
That is what Security for mailcow does. It reads what mailcow already logs, keeps the history in mailcow's database, applies 13 rules for what fail2ban misses, and bans through mailcow's own netfilter — built so it cannot lock you out. This tutorial walks you from install to a tuned, automatic setup.
Prerequisites
Before you start, check that you have:
- mailcow-dockerized on Linux with docker compose and systemd.
- The mailcow Toolkit 0.6.0 or newer — this product is one of the modules in that package, with its own license.
- Root access on the host to run
install.sh. - Outbound HTTPS from the server to
medialab.worksfor the license check. - mailcow's netfilter container running — it is what applies the bans. If Manage external is on in mailcow's fail2ban settings, netfilter does not touch the firewall and your own firewall has to apply mailcow's list.
- Optional: a Cloudflare token with Account → Account Firewall Access Rules → Edit, and a Telegram bot token if you want Telegram alerts.
Step 1 — Install the Toolkit and open System → Security
On the mailcow host, as root, run the Toolkit installer as usual:
sudo ./install.sh --mailcow-dir /opt/mailcow-dockerized --name <instance>
If you already run the Toolkit, update it from System → Updates (Toolkit) instead.
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. Then open System → Security in the mailcow admin menu.
Step 2 — Activate the 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, inside php-fpm-mailcow:
php /web/mlw-toolkit/cli.php activate mailcow-security <KEY>
Without a valid license nothing is read, detected, banned or sent — but existing bans are still lifted when they expire, and nothing is deleted. Details are in the user guide.
Step 3 — Understand what is being read
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, from sources mailcow already writes:
- dovecot — failed IMAP / POP3 / ManageSieve logins with attempt counts, connections that never log in, successful logins.
- postfix — failed SMTP AUTH, authenticated sends, rspamd and DNS blocklist rejects, relay attempts, protocol probes.
- SOGo — failed and successful webmail logins.
- fail2ban — every ban mailcow's own fail2ban makes.
- web server — 4xx answers, including known exploit paths and other 404s.
- php-fpm — failed mailcow UI and API logins.
Records of private addresses (mailcow's own containers, a reverse proxy in front of mailcow) and of networks you ignore are dropped. History is kept 90 days by default. If Overview → Status says lines were lost, mailcow trimmed its logs before they were read — raise LOG_LINES in mailcow.conf.
Step 4 — Read the attack history
Go to Overview. You get the totals of the last 24 hours per kind, a 14-day chart of failed logins, scans and spam per day, the most active addresses with a Ban button, the targeted mailboxes and the active incidents. A red shield next to the System menu shows open serious incidents on every admin page, so you will notice them without hunting.
Spend a few minutes here before changing anything. It tells you whether your server mostly sees background noise or a real campaign.
Step 5 — Review the 13 rules
Each rule counts records in a sliding window and opens an incident when the threshold is reached. While the attack continues, the same incident is extended, not repeated. The defaults:
| Rule | Counts | Default |
|---|---|---|
| Web login brute force | failed SOGo / 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.
The last four rows are your compromised-mailbox detection: a sending spike, logins from many addresses, failures spread across many addresses on one mailbox, and — the sharpest one — a login from an address that failed on that mailbox before. That is a password that was probably guessed.
Step 6 — Watch in monitor-only mode first
The switch at the top of Rules is off at first. Every incident says what would have been done ("would ban"). Leave it that way for a few days, look at the incidents, adjust thresholds, then turn it on. Incidents whose attack is still going on are acted on at the next run.
When you review incidents, use False positive where appropriate: it puts the address on the allowlist and lifts its bans.
Step 7 — Turn on automatic actions
With automatic actions on, a ban goes into mailcow's fail2ban denylist for the rule's duration. mailcow's netfilter applies it 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.
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;
- with protect known addresses on (default), addresses that signed in successfully to a mailbox in the last 30 days — so a colleague who mistypes a password is not locked out.
Step 8 — Act on accounts through mailcow itself
For a mailbox that looks compromised, account actions go through mailcow's own functions: disallow login (the mailbox still receives mail), end the IMAP / POP3 sessions, require a password change, limit sending. You can do this from the single-address or single-mailbox view, which shows failures, logins, sends, incidents and bans at a glance. Every action — yours or the module's — lands in the audit log.
Step 9 — Manual bans and Cloudflare
Bans shows the module's bans (reason, by whom, until when, Cloudflare state), mailcow's own temporary fail2ban bans with Lift or Make it longer, and the rest of mailcow's denylist. 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 the mailcow UI and 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, removed together with the ban.
Step 10 — Alerts
Set alerts by e-mail and Telegram from the severity you choose, with an hourly limit, and optionally a daily summary. Alerts go only to the address and chat you configure. Everything else — logs, addresses, mailbox names, history — stays in your mailcow database and is never sent to medialab.works.
One more thing worth knowing: when a serious vulnerability hits PHP, nginx, SOGo or another mailcow component, a virtual patch can arrive with the license check — a precise mitigation, applied automatically for critical ones. It reduces risk until you update; it does not replace the update.
Tips and troubleshooting
- Bans do not appear in the firewall. Check that mailcow's netfilter container is running. If Manage external is on, netfilter leaves the firewall alone and your own firewall must apply mailcow's list — the Bans page warns you about this.
- You locked yourself out. Your current address is refused by the manual ban form, and known addresses are protected by default. If you still get caught, remove the ban on mailcow's Fail2ban page — it counts as lifted.
- Too many alerts. Raise thresholds or severities per rule, or rely on the hourly limit and the daily summary.
- Web attacks still arrive. If you are behind Cloudflare's proxy, configure Cloudflare in Settings; netfilter alone cannot see the real client.
- Lines were lost. Raise
LOG_LINESinmailcow.confon a very busy server.
Conclusion
fail2ban is still the first line, and you should keep it. Security for mailcow is the second: a remembered history, 13 rules for the slow and distributed attacks, compromised-mailbox detection, and bans that go through the firewall mailcow already trusts. Start in monitor-only mode, watch for a few days, then let it act. The product page has the feature list, the guide covers every screen, and the changelog tracks what changed. If something does not fit your setup, the knowledge base and FAQ are a good next stop, or contact us.