Mail Server Protection: CrowdSec Intrusion Prevention System
Part 3 of the Building a Modern Mail Server on Debian 13 series
Series Navigation:
- Part 1: Preparation & Prerequisites – DNS, hostname, certificates
- Part 2: Core Mail Server – Postfix, Dovecot 2.4, PostfixAdmin
- Part 3: CrowdSec Protection – Security hardening
- Part 4: Rspamd Spam Filtering – DKIM, SPF, DMARC, greylisting
- Part 5: Advanced Filtering – ClamAV, neural networks, Razor/Pyzor
- Part 6: Roundcube Webmail – Web-based email access
Introduction
CrowdSec is a modern, collaborative intrusion prevention system (IPS) that protects your mail server from brute-force attacks, spam, and abuse. Unlike traditional tools like Fail2ban, CrowdSec:
- Shares threat intelligence – Benefits from global community blocklists
- Detects sophisticated attacks – Catches slow, distributed attacks
- Supports IPv6 natively – Full IPv4 and IPv6 protection
- Flexible architecture – Separates detection from blocking
This guide provides battle-tested configurations based on real-world deployment and attack patterns.
What This Guide Covers
- Complete CrowdSec installation and configuration
- Dovecot 2.4 support (Debian 13 compatibility)
- Protection for IMAP/POP3, SMTP AUTH, and PostfixAdmin
- Properly-tuned detection for both fast and slow attacks
- IPv4 and IPv6 protection
- Real-world testing and verification
Prerequisites
Before starting:
- Debian 13 system with Postfix + Dovecot installed (Part 1)
- PostfixAdmin configured (Part 2)
- Logs configured in journald (systemd journal)
- Basic understanding of mail server logs
System Requirements:
- 512MB RAM minimum
- nftables (default on Debian 13)
- Direct network access (if behind proxy, see Architecture Considerations)
Understanding CrowdSec Components
Core Components
┌────────────────────────────────┐
│ Mail Server │
│ (Your Server) │
└───────────────┬────────────────┘
│
▼
┌────────────────────────────────┐
│ Logs │
│ ← Postfix, Dovecot, Apache │
└───────────────┬────────────────┘
│
▼
┌────────────────────────────────┐
│ CrowdSec │
│ Agent │
│ ← Detection Engine │
│ (Parsers + Scenarios) │
└───────────────┬────────────────┘
│
▼
┌────────────────────────────────┐
│ Local API │
│ ← Decision Database │
└───────────────┬────────────────┘
│
▼
┌────────────────────────────────┐
│ Bouncers │
│ (nftables) │
│ ← Enforcement │
│ (Firewall Rules) │
└────────────────────────────────┘Terminology
Terminology
- Parser: Extracts data from logs (IPs, usernames, etc.)
- Scenario: Defines attack patterns and triggers bans
- Decision: Ban verdict for an IP address
- Bouncer: Enforces decisions (blocks IPs in firewall)
- Collection: Bundle of related parsers and scenarios
Installation
Step 1: Add CrowdSec Official Repository
Why this is important: Debian’s default repositories may contain outdated versions. The official CrowdSec repository ensures that you receive the latest stable version, complete with all features and security updates.
# Install prerequisites
apt install -y curl gnupg
# Create keyrings directory (if it doesn't exist)
mkdir -p /etc/apt/keyrings
# Add CrowdSec GPG key (Debian best practice location)
curl -fsSL https://packagecloud.io/crowdsec/crowdsec/gpgkey | gpg --dearmor -o /etc/apt/keyrings/crowdsec.gpg
# Add CrowdSec repository
cat > /etc/apt/sources.list.d/crowdsec.list << 'EOF'
deb [signed-by=/etc/apt/keyrings/crowdsec.gpg] https://packagecloud.io/crowdsec/crowdsec/any any main
deb-src [signed-by=/etc/apt/keyrings/crowdsec.gpg] https://packagecloud.io/crowdsec/crowdsec/any any main
EOF
# Update package list
apt updateNote: CrowdSec uses distribution-agnostic repositories (any any), not OS-specific paths. This ensures compatibility across all Debian-based systems.
Verify repository added:
apt-cache policy crowdsecExpected output:
crowdsec:
Installed: (none)
Candidate: 1.7.x
Version table:
1.7.x 500
500 https://packagecloud.io/crowdsec/crowdsec/any any/main ...Step 2: Install CrowdSec
# Install CrowdSec and firewall bouncer
apt install -y crowdsec crowdsec-firewall-bouncer-nftables
# Verify installation
systemctl status crowdsec
cscli versionExpected output:
CrowdSec version: v1.7.x or laterStep 3: Verify Firewall Bouncer
# Check bouncer is running
systemctl status crowdsec-firewall-bouncer
# Verify CrowdSec nftables table exists
nft list tables | grep crowdsecExpected output:
table ip crowdsec
table ip6 crowdsec6Step 4: Basic Configuration
CrowdSec is now installed with default settings. Next, we’ll configure it for mail server protection.
Official Collections (What’s Actually Included)
IMPORTANT: Official collections have limited mail server protection. Understanding what’s included helps you know what custom scenarios you need.
Step 1: Install Official Collections
# Install Postfix collection
cscli collections install crowdsecurity/postfix
# Install Dovecot collection
cscli collections install crowdsecurity/dovecot
# Reload CrowdSec
systemctl reload crowdsecStep 2: Understand What You Actually Get
Postfix Collection Reality
# Inspect what's actually in the collection
cscli collections inspect crowdsecurity/postfixWhat’s ACTUALLY included:
- ✅
crowdsecurity/postfix-spam– Detects spam attempts - ✅
crowdsecurity/postfix-relay-denied– Detects relay abuse - ✅
crowdsecurity/postfix-helo-rejected– Detects HELO/EHLO violations - ✅
crowdsecurity/postfix-non-smtp-command– Detects protocol violations
What’s MISSING (not included!):
- ❌ No SMTP AUTH brute-force protection!
- ❌ No protection against authentication attempts
- ❌ No slow attack detection
Dovecot Collection Reality
# Inspect Dovecot collection
cscli collections inspect crowdsecurity/dovecotWhat’s ACTUALLY included:
- ✅
crowdsecurity/dovecot-spam– Detects spam attempts
What’s MISSING:
- ❌ No IMAP/POP3 brute-force protection!
- ❌ No authentication failure detection
- ❌ No user enumeration protection
Step 3: Verify What’s Running
# List loaded collections
cscli collections list | grep -E "postfix|dovecot"
# List actual running scenarios
cscli scenarios list | grep -E "postfix|dovecot"Critical Note: Official collections DO NOT protect against authentication brute-force attacks! Custom scenarios are essential.
Custom Dovecot 2.4 Parser (Essential for Debian 13)
Why This Is Needed: Debian 13 includes Dovecot 2.4, which uses a different log format than older versions. The default CrowdSec parser cannot parse Dovecot 2.4 logs, meaning authentication failures are silently discarded.
Understanding the Problem
Dovecot 2.4 log format (Debian 13):
auth-worker(account,45.144.212.19)<189477>: request [1]: sql: unknown user
auth-worker(account,45.144.212.19)<189478>: Password mismatchOld Dovecot format (what default parser expects):
dovecot: auth: unknown user account from 45.144.212.19
dovecot: auth: Password mismatch for user account from 45.144.212.19Verification Test (Proves Default Parser Fails)
Important: When testing with cscli explain, you must provide a complete syslog-formatted line including timestamp and hostname (e.g., Dec 06 10:00:00 mail dovecot: message). Just the message portion won’t parse correctly.
# Test with real Dovecot 2.4 log line (complete syslog format)
echo 'Dec 06 10:00:00 mail dovecot: auth-worker(account,45.144.212.19)<189477>: request [1]: sql: unknown user' > /tmp/test.log
cscli explain --file /tmp/test.log --type syslog --verbose
rm /tmp/test.logResult with default parser:
Event leaving node: ko
Log didn't finish stage s01-parse
Discarding lineThis proves the event is DISCARDED – no protection!
Step 1: Create Custom Dovecot 2.4 Parser
IMPORTANT: Custom parsers must go in /etc/crowdsec/parsers/s01-parse/ (NOT s00-user!). See “Directory Structure Explained” section for why.
# Create parser directory
mkdir -p /etc/crowdsec/parsers/s01-parse
# Create custom Dovecot 2.4 parser
cat > /etc/crowdsec/parsers/s01-parse/dovecot-2.4.yaml << 'EOF'
onsuccess: next_stage
debug: false
filter: "evt.Parsed.program == 'dovecot'"
name: my/dovecot-2.4
description: "Parse Dovecot 2.4 auth-worker logs (Debian 13 format)"
nodes:
# Dovecot 2.4 auth-worker format: auth-worker(user,IP)<pid>: request [n]: backend: message
- grok:
pattern: 'auth-worker\(%{DATA:dovecot_user},%{IP:dovecot_remote_ip}\)<%{INT:pid}>: request \[%{INT:request_num}\]: %{WORD:dovecot_user_backend}: %{GREEDYDATA:dovecot_login_message}'
apply_on: message
# Dovecot 2.4 auth format: auth(user,IP,protocol): backend: message
- grok:
pattern: 'auth\(%{DATA:dovecot_user},%{IP:dovecot_remote_ip},%{DATA:dovecot_protocol}\): %{WORD:dovecot_user_backend}: %{GREEDYDATA:dovecot_login_message}'
apply_on: message
statics:
- meta: log_type
value: dovecot_logs
- meta: source_ip
expression: "evt.Parsed.dovecot_remote_ip"
- meta: dovecot_login_result
expression: "any(['Authentication failure', 'Password mismatch', 'password mismatch', 'auth failed', 'unknown user'], {evt.Parsed.dovecot_login_message contains #}) ? 'auth_failed' : ''"
EOFStep 2: Reload and Verify
# Reload CrowdSec
systemctl reload crowdsec
sleep 3
# Verify parser loaded
cscli parsers list | grep "my/dovecot-2.4"Expected:
my/dovecot-2.4 🏠 enabled,local /etc/crowdsec/parsers/s01-parse/dovecot-2.4.yamlStep 3: Test Parser Works
# Test with real Dovecot 2.4 log (complete syslog format)
echo 'Dec 06 10:00:00 mail dovecot: auth-worker(testuser,45.144.212.19)<189477>: request [1]: sql: unknown user' > /tmp/test.log
cscli explain --file /tmp/test.log --type syslog --verbose
rm /tmp/test.logExpected success output:
├ s01-parse
| └ 🟢 my/dovecot-2.4 (+8 ~2)
| └ create evt.Parsed.dovecot_user : testuser
| └ create evt.Parsed.dovecot_remote_ip : 45.144.212.19
| └ create evt.Parsed.pid : 189477
| └ create evt.Parsed.request_num : 1
| └ create evt.Parsed.dovecot_user_backend : sql
| └ create evt.Parsed.dovecot_login_message : unknown user
| └ create evt.Meta.log_type : dovecot_logs
| └ create evt.Meta.source_ip : 45.144.212.19
| └ create evt.Meta.dovecot_login_result : auth_failed
├-------- parser success 🟢Step 4: Verify in Production
# Wait for real authentication attempts, then check metrics
cscli metricsLook for:
Acquisition Metrics:
| journalctl:dovecot.service | X | X | - | X | - |
↑ ↑ ↑
Lines Lines Lines
read parsed poured✅ Success = Lines parsed > 0 and Lines unparsed = 0 or very low
Directory Structure Explained
Why /etc/crowdsec/parsers/s01-parse/ and NOT /etc/crowdsec/parsers/s00-user/?
CrowdSec processes logs in stages:
s00-raw (syslog parsing) → Success
↓
s01-parse (ALL parsers try in parallel)
├─ Official dovecot-logs parser → FAILS on Dovecot 2.4
├─ Custom dovecot-2.4 parser (if in s01-parse) → SUCCESS ✅
└─ At least ONE success → Continue to s02-enrich
s00-user (only runs if s01-parse succeeded)
└─ Parser here → TOO LATE! Event already discarded if s01-parse failedKey principle: Directory name determines execution stage, NOT the stage: field in YAML!
If the parser in /etc/crowdsec/parsers/s00-user/:
- Official parser fails in s01-parse
- Event discarded immediately
- s00-user stage never reached
- Custom parser loads but never executes (0 hits in metrics)
If the parser in /etc/crowdsec/parsers/s01-parse/:
- Runs in parallel with official parser
- Catches what official parser misses
- Event continues to scenarios
Custom Scenarios for Mail Protection
Official collections lack brute-force protection. We’ll create custom scenarios using properly-tuned leaky buckets based on the official ssh-slow-bf pattern.
Understanding Leaky Buckets
Why leaky buckets (not counter)?
- ✅ CrowdSec official standard (all scenarios use leaky)
- ✅ Battle-tested and well-documented
- ✅ Counter buckets have syntax issues and no examples
Leaky bucket principle:
capacity: 3 # Bucket holds 3 events max
leakspeed: "8h" # Empties 3 events over 8 hours
# = Leaks 1 event every 2.67 hours
Attack pattern:
Hour 0:00 → Attack #1 → Bucket: 1.0
Hour 1:30 → Attack #2 → Bucket: 1.4 (leaked 0.6)
Hour 3:00 → Attack #3 → Bucket: 2.3 (leaked 0.5)
Hour 4:30 → Attack #4 → Bucket: 3.0+ → BAN! ✅Key insight: With leakspeed: 8h, attacks spaced up to ~2.5 hours apart still accumulate!
Step 1: Create Scenarios Directory
# Create directory for custom scenarios
mkdir -p /etc/crowdsec/scenarios/s00-user
Note: Scenarios typically go in s00-user (unlike parsers, which need s01-parse).
Step 2: Create Dovecot Brute-Force Protection
cat > /etc/crowdsec/scenarios/s00-user/dovecot-bf.yaml << 'EOF'
type: leaky
name: my/dovecot-bf
description: "Detect IMAP/POP3 authentication brute force (slow and fast)"
filter: "evt.Meta.log_type == 'dovecot_logs' && evt.Meta.dovecot_login_result == 'auth_failed'"
leakspeed: "8h"
capacity: 3
groupby: evt.Meta.source_ip
blackhole: 4h
labels:
service: imap
type: bruteforce
remediation: true
behavior: "imap:bruteforce"
label: "Dovecot Bruteforce"
EOFConfiguration explained:
capacity: 3– Trigger ban after 3 authentication failuresleakspeed: 8h– Bucket empties over 8 hours (catches slow attacks)blackhole: 4h– Ban duration: 4 hoursgroupby: source_ip– Track per IP address
Attack coverage:
- Fast attacks: 3 attempts in minutes → Banned
- Slow attacks: 3 attempts over several hours → Banned
- Catches attacks spaced up to ~2.5 hours apart
Step 3: Create Postfix SMTP AUTH Protection
cat > /etc/crowdsec/scenarios/s00-user/postfix-bf.yaml << 'EOF'
type: leaky
name: my/postfix-bf
description: "Detect SMTP AUTH brute force (slow and fast)"
filter: "evt.Parsed.program startsWith 'postfix/' && evt.Parsed.message contains 'SASL' && evt.Parsed.message contains 'authentication failed'"
leakspeed: "8h"
capacity: 3
groupby: evt.Meta.source_ip
blackhole: 4h
labels:
service: smtp
type: bruteforce
remediation: true
behavior: "smtp:bruteforce"
label: "Postfix SMTP AUTH Bruteforce"
EOFWhat this catches:
Example Postfix log:
Dec 05 17:05:07 mail postfix/smtpd[275212]: warning: unknown[213.209.157.87]:
SASL LOGIN authentication failed: (reason unavailable), sasl_username=spamAttack pattern observed in the wild:
- 1 attempt every 90-120 minutes
- Testing usernames: spam, admin, test, oracle, postmaster, etc.
- Official scenarios MISS these (no brute-force protection!)
- Our scenario CATCHES them ✅
Step 4: Create PostfixAdmin Web Protection
cat > /etc/crowdsec/scenarios/s00-user/postfixadmin-bf.yaml << 'EOF'
type: leaky
name: my/postfixadmin-bf
description: "Detect PostfixAdmin login brute force"
filter: "evt.Meta.log_type == 'http_access-log' && evt.Meta.http_verb == 'POST' && evt.Meta.http_path startsWith '/login.php'"
leakspeed: "2h"
capacity: 5
groupby: evt.Meta.source_ip
blackhole: 2h
labels:
service: http
type: bruteforce
remediation: true
behavior: "http:bruteforce"
label: "PostfixAdmin Bruteforce"
EOFWhy different settings?
capacity: 5(vs 3 for mail) – Web users make more typosleakspeed: 2h(vs 8h for mail) – Web attacks usually fasterblackhole: 2h(vs 4h for mail) – Shorter ban for web interface- Reduces false positives from legitimate users
Step 5: Configure Apache Log Acquisition
For PostfixAdmin protection to work, CrowdSec needs to read Apache logs:
# Create Apache acquisition config
cat > /etc/crowdsec/acquis.d/apache2.yaml << 'EOF'
---
filenames:
- /var/log/apache2/postfixadmin_access.log
- /var/log/apache2/error.log
labels:
type: apache2
---
EOFStep 6: Reload and Verify
# Reload CrowdSec to load new scenarios
systemctl reload crowdsec
sleep 3
# Verify all scenarios loaded
cscli scenarios list | grep "my/"Expected output:
my/dovecot-bf 🏠 enabled,local /etc/crowdsec/scenarios/s00-user/dovecot-bf.yaml
my/postfix-bf 🏠 enabled,local /etc/crowdsec/scenarios/s00-user/postfix-bf.yaml
my/postfixadmin-bf 🏠 enabled,local /etc/crowdsec/scenarios/s00-user/postfixadmin-bf.yamlWhy These Settings Work
Real-world test results:
Attack pattern observed:
Time IP Action
16:01 213.209.157.87 AUTH FAIL (username: server)
17:05 213.209.157.87 AUTH FAIL (username: spam) ← 64 min later
18:08 213.209.157.87 AUTH FAIL (username: ftpuser) ← 63 min later
19:12 213.209.157.87 AUTH FAIL (username: oracle) ← 64 min later
19:12 213.209.157.87 🚫 BANNED (4 hours)Leaky bucket calculation:
Attack #1 (16:01): bucket = 1.0
Attack #2 (17:05): leaked 0.4 → bucket = 1.6
Attack #3 (18:08): leaked 0.4 → bucket = 2.2
Attack #4 (19:12): leaked 0.4 → bucket = 2.8
Attack #5 (next): → bucket = 3.0+ → BAN! ✅With leakspeed: 8h, the leak rate (0.375 events/hour) is slower than attack rate, so bucket accumulates!
Architecture Considerations
Direct Access vs Reverse Proxy
CRITICAL: CrowdSec firewall bouncer ONLY works with direct network access!
✅ Working Architecture (Recommended):
Internet → Mail Server (Direct Connection)
├─ Port 25 (SMTP)
├─ Port 993 (IMAPS)
├─ Port 995 (POP3S)
└─ Port 25443 (PostfixAdmin HTTPS)
CrowdSec detects → nftables blocks → ✅ Works!❌ Broken Architecture:
Internet → Nginx Proxy → Mail Server
Mail Server sees proxy IP, not attacker IP
CrowdSec detects attacker but can't block (traffic already inside)
Firewall bouncer ineffective → ❌ Doesn't work!If Behind Reverse Proxy
You have two options:
Option 1: Install CrowdSec on Proxy Server (Best)
- Run CrowdSec + firewall bouncer on proxy
- Configure it to read proxy logs
- Blocks attackers before they reach mail server
Option 2: Use Nginx Bouncer
- Install
crowdsec-nginx-bounceron proxy - Configure with the mail server’s CrowdSec API
- Blocks at the application layer
Option 3: Direct Access (What we use)
- Use a non-standard port (e.g., 25443) for PostfixAdmin
- Bypass proxy for mail services
- Simplest and most effective for mail servers
Container/LXC Considerations
If running in Incus/LXC container:
- ✅ Works great with direct networking
- ✅ nftables works normally in unprivileged containers
- ✅ IPv6 fully supported (both IPv4 and IPv6 addresses banned)
- ⚠️ If using NAT/bridge, ensure original IPs are preserved
- ⚠️ If behind proxy, see “Behind Reverse Proxy” above
Testing and Verification
Step 1: Verify Log Acquisition
# Check CrowdSec is reading logs
cscli metricsLook for the Acquisition Metrics section:
Acquisition Metrics:
| Source | Lines read | Lines parsed | Lines unparsed |
|------------------------------------------------|------------|--------------|----------------|
| journalctl:dovecot.service | 62 | 60 | 2 |
| journalctl:postfix.service | 298 | 96 | 202 |
| file:/var/log/apache2/postfixadmin_access.log | 59 | 59 | - |✅ Good: Lines parsed > 0, unparsed is low
❌ Bad: Lines parsed = 0, all unparsed (parser not working)
Step 2: Verify Parser Success Rate
# Check parser metrics
cscli metrics | grep -A10 "Parser Metrics"Look for:
Parser Metrics:
| Parser | Hits | Parsed | Unparsed |
|---------------------------|------|--------|----------|
| child-my/dovecot-2.4 | 83 | 51 | 32 |
| crowdsecurity/dovecot-logs| 11 | 9 | 2 |✅ Dovecot 2.4 parser working – Shows hits and parsed events
✅ Parse rate ~60-85% is normal (not all logs are auth failures)
Step 3: Test PostfixAdmin Protection (IPv4 and IPv6)
From another server (NOT the mail server):
#!/bin/bash
# Save as test-postfixadmin.sh
# Tests both IPv4 and IPv6 brute-force detection
MAIL_SERVER="https://mail.yourdomain.com:25443"
LOGIN_URL="${MAIL_SERVER}/login.php"
echo "========================================="
echo "Testing PostfixAdmin Brute Force Protection"
echo "========================================="
echo ""
# Test IPv4
echo "Testing IPv4 attacks..."
for i in {1..6}; do
echo " IPv4 Attempt $i at $(date +%H:%M:%S)"
curl -4 -X POST "${LOGIN_URL}" \
-d "fUsername=test@test.com" \
-d "fPassword=wrong${i}" \
-k -s -o /dev/null -w " HTTP: %{http_code}\n"
sleep 2
done
echo ""
echo "Waiting 5 seconds before IPv6 test..."
sleep 5
echo ""
# Test IPv6 (if available)
echo "Testing IPv6 attacks..."
if curl -6 -s -m 2 "${MAIL_SERVER}" -k -o /dev/null 2>/dev/null; then
for i in {1..6}; do
echo " IPv6 Attempt $i at $(date +%H:%M:%S)"
curl -6 -X POST "${LOGIN_URL}" \
-d "fUsername=test@test.com" \
-d "fPassword=ipv6wrong${i}" \
-k -s -o /dev/null -w " HTTP: %{http_code}\n"
sleep 2
done
else
echo " IPv6 not available on this test machine (skipping IPv6 test)"
fi
echo ""
echo "========================================="
echo "Tests complete!"
echo "========================================="
echo ""
echo "On mail server, check bans:"
echo " cscli decisions list"
echo ""
echo "You should see BOTH IPv4 and IPv6 addresses banned!"Run the test:
chmod +x test-postfixadmin.sh
./test-postfixadmin.shOn the mail server, monitor:
# Watch for bans (should see both IPv4 and IPv6)
watch -n 2 'cscli decisions list'
# Check Apache logs
tail -f /var/log/apache2/postfixadmin_access.logExpected result after 5-6 attempts:
Alert ID XX: Ip:YOUR_TEST_IP | my/postfixadmin-bf | ban:1Step 4: Verify Firewall Blocking
# Check banned IPs in nftables blacklist
nft list set ip crowdsec crowdsec-blacklists
# Or check for a specific IP
nft list set ip crowdsec crowdsec-blacklists | grep YOUR_TEST_IPNote: To see blocked IPs, use nft list set ip crowdsec crowdsec-blacklists. To check if table exists without listing all IPs, use nft list tables | grep crowdsec.
From the test machine, try connecting again:
curl -k https://mail.yourdomain.com:25443/login.phpExpected:
- Connection timeout or refused
- No HTTP response
- Blocked at the network level! ✅
Step 5: Monitor Real Attacks
# View recent bans
cscli alerts list
# View current decisions (active bans)
cscli decisions list
# View alerts for specific scenario
cscli alerts list | grep "my/postfix-bf"Real-world example:
╭────────┬──────────┬───────────────────┬────────────────────┬────────┬─────────╮
│ ID │ Source │ Scope:Value │ Reason │ Action │ Country │
├────────┼──────────┼───────────────────┼────────────────────┼────────┼─────────┤
│ 267695 │ crowdsec │ Ip:143.47.189.215 │ my/postfixadmin-bf │ ban │ NL │
│ 260846 │ crowdsec │ Ip:185.196.11.30 │ my/postfix-bf │ ban │ CH │
│ 254002 │ crowdsec │ Ip:178.16.54.15 │ my/postfix-bf │ ban │ NL │
╰────────┴──────────┴───────────────────┴────────────────────┴────────┴─────────╯
Step 6: Verify IPv4 and IPv6 Protection
After running the test script from Step 3, check that both IPv4 and IPv6 addresses are banned:
# Check decisions list on mail server
cscli decisions listExpected result:
╭────────┬──────────┬────────────────────────────┬────────────────────┬────────┬─────────╮
│ ID │ Source │ Scope:Value │ Reason │ Action │ Country │
├────────┼──────────┼────────────────────────────┼────────────────────┼────────┼─────────┤
│ 267696 │ crowdsec │ Ip:2603:c022:c002:2701::12 │ my/postfixadmin-bf │ ban │ NL │
│ 267695 │ crowdsec │ Ip:143.47.189.215 │ my/postfixadmin-bf │ ban │ NL │
╰────────┴──────────┴────────────────────────────┴────────────────────┴────────┴─────────╯
Both IPv4 and IPv6 addresses from the same source get banned! ✅
This confirms CrowdSec properly detects and blocks attacks from both address families.
Monitoring and Maintenance
Daily Monitoring Commands
# Check for new alerts today
cscli alerts list --since 1d
# View active bans
cscli decisions list
# Check metrics summary
cscli metricsView Specific Scenario Activity
# Dovecot brute-force attempts
cscli alerts list | grep "my/dovecot-bf"
# Postfix SMTP AUTH attacks
cscli alerts list | grep "my/postfix-bf"
# PostfixAdmin login attempts
cscli alerts list | grep "my/postfixadmin-bf"Real-Time Monitoring
# Watch for new bans
watch -n 5 'cscli alerts list | head -15'
# Monitor CrowdSec logs
journalctl -u crowdsec -f
# Monitor bouncer activity
journalctl -u crowdsec-firewall-bouncer -fCheck Community Blocklist (CAPI)
CrowdSec shares threat intelligence. Check what you’re getting from the community:
# View CAPI decisions
cscli decisions list | grep CAPI
# Check console enrollment status
cscli console statusExpected output:
╭────────────────────┬───────────┬──────────────────────────────────────────────────────╮
│ Option Name │ Activated │ Description │
├────────────────────┼───────────┼──────────────────────────────────────────────────────┤
│ custom │ ✅ │ Forward alerts from custom scenarios to the console │
│ manual │ ❌ │ Forward manual decisions to the console │
│ tainted │ ✅ │ Forward alerts from tainted scenarios to the console │
│ context │ ❌ │ Forward context with alerts to the console │
│ console_management │ ❌ │ Receive decisions from console │
╰────────────────────┴───────────┴──────────────────────────────────────────────────────╯
Default enrollment: CrowdSec automatically enrolls and shares alerts from custom scenarios (✅). This helps the community detect new threats!
Manually Ban/Unban IPs
# Manually ban an IP
cscli decisions add --ip 1.2.3.4 --duration 24h --reason "Manual ban"
# Unban an IP
cscli decisions delete --ip 1.2.3.4
# Ban an IP range
cscli decisions add --range 1.2.3.0/24 --duration 48h --reason "Malicious range"Flush Old Decisions
# Remove expired bans (automatic, but can force)
cscli decisions delete --all-expiredTuning and Optimization
Adjust Ban Durations
If you’re getting too many bans or want longer protection:
Edit scenario file:
vi /etc/crowdsec/scenarios/s00-user/postfix-bf.yamlModify blackhole (ban duration):
blackhole: 24h # Change from 4h to 24h for longer bansReload:
systemctl reload crowdsecAdjust Sensitivity
More aggressive (ban faster):
capacity: 2 # Ban after 2 attempts instead of 3
leakspeed: "12h" # Slower leak = attacks accumulate easierLess aggressive (avoid false positives):
capacity: 5 # Ban after 5 attempts instead of 3
leakspeed: "4h" # Faster leak = need faster attacks to triggerWhitelist Trusted IPs
# Whitelist your own IP
cscli decisions add --ip YOUR_IP --type whitelist --reason "Admin IP"
# Whitelist your office network
cscli decisions add --range 192.168.1.0/24 --type whitelist --reason "Office network"
# View whitelisted IPs
cscli decisions list --type whitelistPerformance Optimization
If CrowdSec uses too much CPU/RAM:
- Reduce log verbosity (if needed):
# Edit config
vi /etc/crowdsec/config.yaml
# Set log level
log_level: info # Options: trace, debug, info, warning, error- Limit acquisition (if processing too many logs):
# Check what's using resources
cscli metrics | grep -A5 "Acquisition"- Disable unused collections:
# Remove collections you don't need
cscli collections remove crowdsecurity/linux
# Reload
systemctl reload crowdsecTroubleshooting
Parser Not Working
Symptom: Lines unparsed = high, no events reaching scenarios
Debug:
# Test with real log line (complete syslog format)
echo 'Dec 06 10:00:00 mail dovecot: auth-worker(user,1.2.3.4)<123>: sql: unknown user' > /tmp/test.log
cscli explain --file /tmp/test.log --type syslog --verbose
rm /tmp/test.log
# Check parser is loaded
cscli parsers list | grep dovecotSolution:
- Verify parser is in
/etc/crowdsec/parsers/s01-parse/(NOTs00-user!) - Check YAML syntax:
cscli parsers inspect my/dovecot-2.4 - Reload:
systemctl reload crowdsec
Scenario Not Triggering
Symptom: Attacks in logs but no bans issued
Debug:
# Check scenario is loaded
cscli scenarios list | grep "my/postfix-bf"
# Check if events reaching scenario
cscli metrics | grep -A5 "Bucket Metrics"
# Test scenario filter with real log file
cscli explain --file /var/log/mail.log --type syslog --verbose
# Or test with a single line (must include syslog header)
echo 'Dec 06 10:00:00 mail postfix/smtpd[12345]: warning: unknown[1.2.3.4]: SASL LOGIN authentication failed' > /tmp/test.log
cscli explain --file /tmp/test.log --type syslog --verbose
rm /tmp/test.logCommon issues:
- ❌ Filter doesn’t match log format
- ❌ Parser not extracting required fields
- ❌ Capacity too high (not reaching threshold)
Solution:
- Test filter against actual logs
- Adjust capacity/leakspeed
- Check parser output includes required fields
Firewall Not Blocking
Symptom: Ban issued but traffic still getting through
Debug:
# Check bouncer is running
systemctl status crowdsec-firewall-bouncer
# Verify IP is in nftables
nft list set ip crowdsec crowdsec-blacklists | grep <IP>
# Check bouncer logs
journalctl -u crowdsec-firewall-bouncer -n 50Common issues:
- ❌ Behind reverse proxy (see Architecture Considerations)
- ❌ Bouncer not running
- ❌ nftables rules not applied
Solution:
- If behind proxy: Move CrowdSec to proxy or use direct access
- Restart bouncer:
systemctl restart crowdsec-firewall-bouncer - Verify table exists:
nft list tables | grep crowdsec
CrowdSec Won’t Start
Symptom: Service fails to start
Debug:
# Check detailed error
journalctl -xeu crowdsec
# Test configuration
crowdsec -c /etc/crowdsec/config.yaml -t
# Check YAML syntax
cscli scenarios listCommon errors:
- Syntax error in YAML file
- Missing required fields in scenario
- Invalid collection reference
Solution:
- Check the error message for the file/line number
- Validate YAML syntax online
- Remove the problematic scenario and reload
High CPU/Memory Usage
Symptom: CrowdSec is using excessive resources
Debug:
# Check what's processing
cscli metrics
# Check CrowdSec resource usage
systemctl status crowdsec
# View recent CPU/memory usage
top -b -n 1 | grep crowdsec
# Check log processing activity
journalctl -u crowdsec -n 50 | grep -E "parsed|scenario"Solutions:
- Reduce log sources in acquis.yaml
- Increase polling interval
- Disable unused parsers/scenarios
- Check for log loops (e.g., CrowdSec logging to journal and reading journal)
Restart Resets Buckets
Symptom: Counters reset after CrowdSec restart
Expected behavior: This is normal! CrowdSec buckets are in-memory and don’t persist across restarts.
Impact: Attackers need to start over after CrowdSec restart (bucket resets to 0).
This is by design – not a bug.
Summary and Next Steps
What You Now Have
✅ Complete mail server protection:
- IMAP/POP3 brute-force detection (Dovecot)
- SMTP AUTH brute-force detection (Postfix)
- Web interface protection (PostfixAdmin)
✅ Proper Dovecot 2.4 support (Debian 13)
✅ IPv4 and IPv6 protection
✅ Catches slow AND fast attacks
✅ Community threat intelligence (CAPI)
✅ Battle-tested configurations
Real-World Results
From production deployment:
24 hours after deployment:
- 5+ attackers detected and banned
- 50+ authentication attempts blocked
- 0 false positives
- 100% uptimeAppendix: Quick Reference
Essential Commands
# Status
systemctl status crowdsec
cscli metrics
# Scenarios
cscli scenarios list
cscli scenarios list | grep "my/"
# Alerts & Bans
cscli alerts list
cscli decisions list
# Manual Ban/Unban
cscli decisions add --ip 1.2.3.4 --duration 24h
cscli decisions delete --ip 1.2.3.4
# Reload Configuration
systemctl reload crowdsec
# Test Configuration
crowdsec -c /etc/crowdsec/config.yaml -t
# Debug Log Line (must include syslog header: timestamp hostname program: message)
echo 'Dec 06 10:00:00 mail dovecot: auth-worker(user,1.2.3.4)<123>: unknown user' > /tmp/test.log
cscli explain --file /tmp/test.log --type syslog --verbose
rm /tmp/test.log
File Locations
/etc/crowdsec/
├── config.yaml # Main config
├── acquis.d/ # Log acquisition
│ └── apache2.yaml
├── parsers/
│ └── s01-parse/ # Custom parsers (NOT s00-user!)
│ └── dovecot-2.4.yaml
└── scenarios/
└── s00-user/ # Custom scenarios
├── dovecot-bf.yaml
├── postfix-bf.yaml
└── postfixadmin-bf.yaml
Scenario Settings Comparison
| Service | Capacity | Leakspeed | Blackhole | Why |
|---|---|---|---|---|
| Dovecot | 3 | 8h | 4h | Mail attacks slow |
| Postfix | 3 | 8h | 4h | SMTP AUTH attacks slow |
| PostfixAdmin | 5 | 2h | 2h | Web users make typos |
Attack Types Detected
| Attack Type | Scenario | Example |
|---|---|---|
| IMAP/POP3 brute-force | my/dovecot-bf | Password guessing |
| SMTP AUTH brute-force | my/postfix-bf | Username enumeration |
| PostfixAdmin login | my/postfixadmin-bf | Admin panel attacks |
| Spam attempts | postfix-spam | Mass mailing |
| Relay abuse | postfix-relay-denied | Open relay probing |
Next Steps
With intrusion prevention active, continue with Part 4: Rspamd Spam Filtering, where we add DKIM signing, SPF, DMARC, greylisting, and professional spam filtering to achieve Hall of Fame status on internet.nl.
Return to: Mail Server Series Index
