| | | |

Mail Server Protection: CrowdSec Intrusion Prevention System

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

Series Navigation:

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-nftables

# 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
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' : ''"
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:

├ 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 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.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"
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      │ 
│ 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 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

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

Similar Posts