| | | |

Mail Server Protection: CrowdSec Intrusion Prevention System

Part 3 of the Building a Modern Mail Server on Debian 13 series

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 update

Note: 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 crowdsec

Expected 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

# Verify installation
systemctl status crowdsec
cscli version

Expected output:

CrowdSec version: v1.7.x or later

Step 3: Verify Firewall Bouncer

# Check bouncer is running
systemctl status crowdsec-firewall-bouncer

# Verify CrowdSec nftables table exists
nft list tables | grep crowdsec

Expected output:

table ip crowdsec
table ip6 crowdsec6

Step 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 crowdsec

Step 2: Understand What You Actually Get

Postfix Collection Reality

# Inspect what's actually in the collection
cscli collections inspect crowdsecurity/postfix

What’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/dovecot

What’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 mismatch

Old 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.19

Verification 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.log

Result with default parser:

Event leaving node: ko
Log didn't finish stage s01-parse
Discarding line

This 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
name: my/dovecot-2.4
description: "Parse Dovecot 2.4 authentication logs (Debian 13 format)"
filter: "evt.Parsed.program == 'dovecot'"
grok:
  name: DOVECOT24
  apply_on: message
pattern: |
  auth-worker\(%{DATA:account},%{IP:source_ip}\)<%{NUMBER}>\: (?:request \[\d+\]: sql: )?%{GREEDYDATA:action}
statics:
  - meta: log_type
    value: dovecot_auth
  - meta: service
    value: imap
  - meta: source_ip
    expression: evt.Parsed.source_ip
nodes:
  - grok:
      name: DOVECOT24_FAIL
      apply_on: action
      statics:
        - meta: auth_status
          value: failed
      pattern: "(?:unknown user|Password mismatch)"
EOF

Step 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.yaml

Step 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.log

Expected success output:

+ Grok "DOVECOT24" returned data
{"account":"testuser","source_ip":"45.144.212.19"...}
Event leaving node: ok
Event reaching next stage

Step 4: Verify in Production

# Wait for real authentication attempts, then check metrics
cscli metrics

Look 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 failed

Key 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.Parsed.program == 'dovecot' && (evt.Parsed.message contains 'unknown user' || evt.Parsed.message contains 'Password mismatch')"
leakspeed: "8h"
capacity: 3
groupby: evt.Meta.source_ip
blackhole: 4h
labels:
  service: imap
  type: bruteforce
  remediation: true
  behavior: "imap:bruteforce"
  label: "Dovecot Bruteforce"
EOF

Configuration explained:

  • capacity: 3 – Trigger ban after 3 authentication failures
  • leakspeed: 8h – Bucket empties over 8 hours (catches slow attacks)
  • blackhole: 4h – Ban duration: 4 hours
  • groupby: 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"
EOF

What 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=spam

Attack 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"
EOF

Why different settings?

  • capacity: 5 (vs 3 for mail) – Web users make more typos
  • leakspeed: 2h (vs 8h for mail) – Web attacks usually faster
  • blackhole: 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
---
EOF

Step 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.yaml

Why 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-bouncer on 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 metrics

Look 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.sh

On 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.log

Expected result after 5-6 attempts:

Alert ID XX: Ip:YOUR_TEST_IP | my/postfixadmin-bf | ban:1

Step 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_IP

Note: 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.php

Expected:

  • 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 list

Expected result:

╭────────┬──────────┬────────────────────────────┬────────────────────┬────────┬─────────╮
│   ID   │  Source  │         Scope:Value        │       Reason       │ Action │ Country │
├────────┼──────────┼────────────────────────────┼────────────────────┼────────┼─────────┤
│ 267696 │ crowdsec │ Ip:2603:c022:c002:2701::12 │ my/postfixadmin-bf │ ban    │ NL      │ ←IPv6
│ 267695 │ crowdsec │ Ip:143.47.189.215          │ my/postfixadmin-bf │ ban    │ NL      │ ←IPv4
╰────────┴──────────┴────────────────────────────┴────────────────────┴────────┴─────────╯

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 metrics

View 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 -f

Check 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 status

Expected 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-expired

Tuning 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.yaml

Modify blackhole (ban duration):

blackhole: 24h  # Change from 4h to 24h for longer bans

Reload:

systemctl reload crowdsec

Adjust Sensitivity

More aggressive (ban faster):

capacity: 2     # Ban after 2 attempts instead of 3
leakspeed: "12h"  # Slower leak = attacks accumulate easier

Less aggressive (avoid false positives):

capacity: 5     # Ban after 5 attempts instead of 3
leakspeed: "4h"   # Faster leak = need faster attacks to trigger

Whitelist 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 whitelist

Performance Optimization

If CrowdSec uses too much CPU/RAM:

  1. Reduce log verbosity (if needed):
# Edit config
vi /etc/crowdsec/config.yaml

# Set log level
log_level: info  # Options: trace, debug, info, warning, error
  1. Limit acquisition (if processing too many logs):
# Check what's using resources
cscli metrics | grep -A5 "Acquisition"
  1. Disable unused collections:
# Remove collections you don't need
cscli collections remove crowdsecurity/linux

# Reload
systemctl reload crowdsec

Troubleshooting

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 dovecot

Solution:

  • Verify parser is in /etc/crowdsec/parsers/s01-parse/ (NOT s00-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.log

Common 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 50

Common 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 list

Common 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% uptime

Appendix: 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

ServiceCapacityLeakspeedBlackholeWhy
Dovecot38h4hMail attacks slow
Postfix38h4hSMTP AUTH attacks slow
PostfixAdmin52h2hWeb users make typos

Attack Types Detected

Attack TypeScenarioExample
IMAP/POP3 brute-forcemy/dovecot-bfPassword guessing
SMTP AUTH brute-forcemy/postfix-bfUsername enumeration
PostfixAdmin loginmy/postfixadmin-bfAdmin panel attacks
Spam attemptspostfix-spamMass mailing
Relay abusepostfix-relay-deniedOpen relay probing

Next Steps

Critical next step: Proceed immediately with Part 4: Rspamd Mail Filtering and Spam Protection to complete your mail server security:

  • Advanced spam filtering with machine learning
  • DMARC, SPF, and DKIM validation for incoming mail
  • Greylisting to block spam waves
  • Virus and malware scanning integration
  • Rate limiting to prevent abuse
  • Reputation scoring and adaptive filtering

Your mail server is now protected from intrusion attempts, but incoming spam and malware can still reach user mailboxes. Rspamd provides essential content filtering and reputation management before accepting production email traffic.

Return to: Mail Server Series Index

Similar Posts