Understanding Mailcow IP Blocks - From Watchdog Alerts to Programmatic Monitoring

Introduction: Beyond Email Notifications

Mailcow’s integrated Watchdog service provides excellent security monitoring, automatically blocking malicious IP addresses and notifying administrators via email. However, in production environments, email alone is insufficient—you need programmatic access to block data for automation, incident response, and centralized monitoring.

This guide explains why Mailcow blocks IP addresses and provides multiple methods to extract this data programmatically.

Why Mailcow Blocks IP Addresses

Fail2ban Integration

Service Trigger Condition Ban Duration
SOGo 5 failed logins in 10 min 30 minutes
Postfix Authentication failures Configurable
Dovecot IMAP/POP3 auth failures Configurable
ACME Certificate request failures Variable

Common Indicators:

  • Brute force login attempts
  • Invalid credentials
  • Protocol violations
  • Rate limit exceeded

Postscreen Protection

Postscreen protects against:

  • Connection overload
  • SMTP protocol abuse
  • Zombie mailers

Indicators:

  • Too many RCPT TO commands
  • Invalid SMTP pipelining
  • Connection rate exceeding thresholds

Rspamd Rate Limiting

Greylisting and reputation-based blocking:

  • Unknown sender deferral
  • Message rate limits
  • Poor reputation scores

Programmatic Access Methods

Method 1: Docker Log Parsing

Access logs directly from containers:

1
2
3
4
5
6
7
8
# Live fail2ban log stream
docker-compose logs --tail=100 -f fail2ban

# Recent bans
docker-compose logs --since="1h" fail2ban | grep "Ban"

# Postscreen blocks
docker-compose logs --since="24h" postfix-mailcow | grep "NOQUEUE"

Python script for automated parsing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3
import subprocess
import re
import json
from datetime import datetime

def get_fail2ban_bans():
result = subprocess.run(
['docker-compose', 'logs', '--since=24h', 'fail2ban'],
cwd='/opt/mailcow-dockerized',
capture_output=True, text=True
)

pattern = r'(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}).*Ban\s+([\d\.]+)'
bans = []

for line in result.stdout.split('\n'):
match = re.search(pattern, line)
if match:
timestamp, ip = match.groups()
service = "sogo" if "sogo" in line else \
"postfix" if "postfix" in line else \
"dovecot" if "dovecot" in line else "other"

bans.append({
'ip': ip,
'timestamp': timestamp,
'service': service
})

return bans

if __name__ == '__main__':
print(json.dumps(get_fail2ban_bans(), indent=2))

Method 2: Redis Query

Mailcow stores block data in Redis:

1
2
3
4
5
6
7
# Enter Redis container
docker-compose exec redis-mailcow redis-cli

# List blocked IPs
KEYS fail2ban:*
GET fail2ban:192.168.1.100
TTL fail2ban:192.168.1.100

Python Redis client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import redis

def get_redis_blocks():
r = redis.Redis(host='172.22.1.253', port=6379, decode_responses=True)
blocks = []

for key in r.scan_iter("fail2ban:*"):
ip = key.split(":")[1]
blocks.append({
'ip': ip,
'ttl': r.ttl(key),
'data': r.get(key)
})

return blocks

Method 3: Fail2ban Client

Direct fail2ban socket access:

1
2
3
4
5
6
7
8
# List all jails
docker-compose exec fail2ban fail2ban-client status

# Specific jail status
docker-compose exec fail2ban fail2ban-client status sogo-auth

# View banned IPs
docker-compose exec fail2ban fail2ban-client status sogo-auth | grep "Banned IP list"

Method 4: API Integration

For push-based monitoring, use Mailcow’s API:

1
2
3
4
# Requires API key from mailcow UI
curl -X GET \
https://mailcow.example.com/api/v1/get/fail2ban \
-H "X-API-Key: YOUR-API-KEY"

Automation Strategies

Centralized Logging with Promtail

Forward Docker logs to Loki/Grafana:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/positions.yaml

clients:
- url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: mailcow-fail2ban
static_configs:
- targets:
- localhost
labels:
job: fail2ban
__path__: /var/lib/docker/containers/*/*-json.log
pipeline_stages:
- match:
selector: '{job="fail2ban"}'
stages:
- regex:
expression: '.*Ban\s+(?P<ip>\d+\.\d+\.\d+\.\d+).*'
- labels:
ip:

Slack/Discord Webhook Notifications

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3
import requests
import json

def notify_slack(ip, service, timestamp):
webhook_url = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

message = {
"text": f"Mailcow blocked IP",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*IP:* {ip}\n*Service:* {service}\n*Time:* {timestamp}"
}
}
]
}

requests.post(webhook_url, json=message)

# Use with parser from above
bans = get_fail2ban_bans()
for ban in bans:
notify_slack(ban['ip'], ban['service'], ban['timestamp'])

Automated Unblocking with Conditions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
import subprocess
import requests

def check_ip_reputation(ip):
"""Check if IP is from known good source"""
try:
# Check against whitelist or reputation service
response = requests.get(f"https://api.abuseipdb.com/api/v2/check?ipAddress={ip}",
headers={"Key": "YOUR-API-KEY"}, timeout=5)
data = response.json()
return data['data']['abuseConfidenceScore'] < 25
except:
return False

def unblock_if_safe(ip):
if check_ip_reputation(ip):
subprocess.run([
'docker-compose', 'exec', '-T', 'fail2ban',
'fail2ban-client', 'unban', ip
], cwd='/opt/mailcow-dockerized')
print(f"Unblocked {ip} based on reputation check")

Configuration Best Practices

Tuning Fail2ban Thresholds

Edit data/conf/fail2ban/fail2ban.conf:

1
2
3
4
5
6
7
8
[sogo-auth]
enabled = true
port = 80,443
filter = sogo-auth
logpath = /var/log/sogo/sogo.log
maxretry = 5
findtime = 600
bantime = 1800

Whitelist Critical IPs

1
2
3
4
5
6
# Add to jail.local
docker-compose exec fail2ban fail2ban-client set <JAIL> addignoreip 192.168.1.0/24

# Or edit data/conf/fail2ban/jail.local:
[DEFAULT]
ignoreip = 127.0.0.1/8 192.168.1.0/24 10.0.0.0/8

Custom Filtering

Create custom filters in data/conf/fail2ban/filter.d/:

1
2
3
4
5
# custom-auth.conf
[Definition]
failregex = authentication failure.*rhost=<HOST>
Failed password.*from <HOST>
ignoreregex =

Monitoring Dashboard with Grafana

Create visualizations for:

  • Block frequency by hour/day
  • Top offending IPs
  • Service-specific block rates
  • Geographic distribution of blocks

Sample Prometheus query:

1
rate(fail2ban_bans_total[5m])

Conclusion

While Mailcow’s email notifications are useful, programmatic access to block data enables sophisticated automation. Choose your method