| | |

Rspamd Spam Filtering for Debian 13 Mail Server

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

Introduction

The previous guides in this series established the foundation for a production mail server. Part 2 covered the core mail infrastructure with Postfix, Dovecot 2.4, MariaDB, and PostfixAdmin for web-based management. Part 3 added CrowdSec intrusion detection to protect against attacks. However, the mail server at this stage accepts all incoming mail without spam filtering and doesn’t sign outgoing messages, which significantly impacts deliverability and security.

This guide focuses on adding professional spam filtering and email authentication to complete the transition from a basic mail server to a production-ready system.

What’s New in Part 4

  • Spam filtering with Rspamd – Modern, fast spam detection
  • Email authentication – DKIM, SPF, DMARC for deliverability
  • Automatic spam handling – Spam delivered to Junk folders automatically
  • Greylisting – Blocks spam bots effectively
  • DANE/TLSA – Certificate pinning for enhanced security

By the end of this guide, your mail server will achieve “Hall of Fame” status on internet.nl and score 10/10 on mail-tester.com!

What You’ll Build

After completing this part, your mail server will have:

Spam Protection

  • Rspamd spam filtering – Analyzes all incoming mail
  • Clear spam headers – X-Spam, X-Spam-Score, etc.
  • Automatic Junk delivery – Spam goes to Junk folder automatically
  • Greylisting protection – Blocks spam bots

Email Authentication

  • DKIM signing – Cryptographically sign outgoing mail
  • SPF records – Authorize your mail servers
  • DMARC policy – Complete authentication framework
  • DANE/TLSA – Certificate validation via DNS

Professional Setup

  • Hall of Fame rating – Top 1% security on internet.nl
  • Excellent deliverability – Mail reaches inboxes, not spam folders
  • Production-ready – Handles real-world email workloads

Important: This part covers core spam filtering to achieve Hall of Fame status. Advanced features like ClamAV antivirus, Razor/Pyzor collaborative detection, and neural network learning are covered in Part 5. Rspamd web interface setup is covered in Part 6.

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                     Incoming/Outgoing Mail                  │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                        Postfix                              │
│                    SMTP Server (Port 25)                    │
│                 Submission (Ports 587/465)                  │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           │ Milter Protocol
                           │ (inet:127.0.0.1:11332)
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                        Rspamd                               │
│              Modern Spam Filtering Engine                   │
│                                                             │
│  ┌─────────────┐  ┌──────────────┐  ┌──────────────┐        │
│  │ Spam Score  │  │ DKIM Signing │  │  Greylisting │        │
│  │  Detection  │  │  (Outgoing)  │  │  Protection  │        │
│  └─────────────┘  └──────────────┘  └──────────────┘        │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           │ Stats/Cache
                           │ Unix Socket
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                        Valkey                               │
│               Redis Alternative (Key-Value Store)           │
│                /run/valkey/valkey.sock                      │
└─────────────────────────────────────────────────────────────┘
                           │
                           │ Headers Added:
                           │ • X-Spam: Yes/No
                           │ • X-Spam-Score: 7.5
                           │ • X-Spam-Level: *******
                           │ • X-Spamd-Result: detailed analysis
                           │ • DKIM-Signature: (for outgoing)
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                    Dovecot LMTP                             │
│                  Mail Delivery (Port 24)                    │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                    Sieve Filter                             │
│              (Global spam-to-junk rule)                     │
│                                                             │
│              if header "X-Spam" "Yes"                       │
│              → fileinto "Junk"                              │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│                   Final Delivery                            │
│                 INBOX or Junk Folder                        │
└─────────────────────────────────────────────────────────────┘

Mail Flow Explained

Incoming mail:

  1. Mail arrives at Postfix (port 25)
  2. Postfix forwards to Rspamd via milter protocol
  3. Rspamd analyzes message, queries Valkey for statistics
  4. Rspamd adds spam headers to message
  5. Mail returns to Postfix with headers
  6. Postfix delivers to Dovecot LMTP
  7. Sieve filter checks spam headers
  8. Clean mail → INBOX, spam → Junk

Outgoing mail (authenticated users):

  1. User sends mail via port 587 or 465
  2. Postfix forwards to Rspamd
  3. Rspamd adds DKIM signature
  4. Mail sent with authentication

Prerequisites Check

Before starting Part 4, verify you’ve completed the previous parts.

From Part 1: Prerequisites

  • Debian 13 (Trixie) installed
  • Unbound DNS resolver running with DNSSEC
  • Firewall configured for mail services
    • Port 25 (SMTP – incoming mail)
    • Port 587 (Submission – authenticated sending with STARTTLS)
    • Port 465 (SMTPS – authenticated sending with implicit TLS)
    • Port 993 (IMAPS – secure IMAP access)
    • Port 25443 (PostfixAdmin HTTPS)
    • ~~Port 995 (POP3S)~~ – Not needed unless using POP3
    • ~~Ports 80/443~~ – Handled by your Nginx reverse proxy
  • DNS records configured (A, AAAA, MX, PTR)
  • Valid SSL/TLS certificates (Let’s Encrypt via acme.sh)

From Part 2: Core Mail Server & PostfixAdmin

  • Postfix installed and receiving mail on port 25
  • Dovecot 2.4 delivering mail to mailboxes (IMAP)
  • MariaDB storing virtual domains and mailboxes
  • PostfixAdmin accessible (via port 25443 or reverse proxy)
  • Virtual domains created and configured
  • Test mailboxes created (e.g., info@yourdomain.com)
  • Can manage domains and users via web interface
  • Can send and receive test emails successfully

From Part 3: CrowdSec Protection

  • CrowdSec installed and protecting mail services
  • Parsers configured for Postfix, Dovecot
  • Collections installed for mail server protection
  • Intrusion detection and automated blocking active

Important: If you’ve completed Part 3 (CrowdSec), remember to whitelist testing services before running online tests (covered in Testing section below).

System Resources

Check you have enough resources:

# Check RAM (need at least 1.5GB free for Rspamd + Valkey)
free -h

# Check disk space (need at least 2GB free)
df -h /

# Check CPU (at least 1 core, 2+ recommended)
nproc

Recommended minimum:

  • 2GB RAM (4GB+ for production)
  • 2 CPU cores
  • 20GB disk space
  • SSD storage (HDD works but slower)

If all prerequisites are met, you’re ready to proceed!

Configuration Variables

This guide uses the centralized configuration file established in Parts 1-3.

Load Existing Configuration

# Source your configuration file
source /root/mail-server-vars.sh

# Verify variables are loaded
echo "Domain: $DOMAIN"
echo "Mail server: $MX1_FQDN"
echo "SSL certificate: $SSL_FULLCHAIN"

Expected output (with your values):

Domain: yourdomain.com
Mail server: mx1.yourdomain.com
SSL certificate: /etc/ssl/acme/mx1.yourdomain.com.fullchain

Add Rspamd Variables

Add DKIM configuration to your existing file:

# Edit configuration file
vi /root/mail-server-vars.sh

# Add these lines at the end:

Add this section:

#=============================================================================
# DKIM Configuration (Part 4)
#=============================================================================
export DKIM_SELECTOR="mail"
export DKIM_DIR="/var/lib/rspamd/dkim"

Save the file and reload:

# Reload configuration
source /root/mail-server-vars.sh

# Verify new variables
echo "DKIM selector: $DKIM_SELECTOR"
echo "DKIM directory: $DKIM_DIR"

Note: We’re only adding DKIM variables for now. Rspamd web interface configuration (including password) will be covered in Part 6.

Valkey Installation

Valkey is an open-source, Redis-compatible key-value store that Rspamd uses for statistics, rate limiting, greylisting data, and fuzzy hash storage.

Why Valkey Instead of Redis?

  • Open source: Truly free (Redis changed licensing in 2024)
  • Drop-in replacement: 100% Redis-compatible
  • Active development: Backed by Linux Foundation
  • Better for Debian: Available in official repositories

Install from Debian Repository

Debian 13 includes Valkey in the official repositories:

# Update package list
apt update

# Install Valkey
apt install -y valkey

# Check installed version
valkey-server --version

Expected: Valkey 7.2.5 or newer

Configure Valkey for Rspamd

Rspamd recommends using a Unix socket instead of TCP for better performance and security.

Edit Valkey Configuration

# Backup original config
cp /etc/valkey/valkey.conf /etc/valkey/valkey.conf.orig

# Clean config (remove comments for readability)
grep -v -E "^#|^;" /etc/valkey/valkey.conf.orig | grep . > /etc/valkey/valkey.conf

Configure Unix Socket and Memory

Edit /etc/valkey/valkey.conf:

vi /etc/valkey/valkey.conf

Find and modify these settings:

# Disable TCP port (we'll use Unix socket)
port 0

# Enable Unix socket - CRITICAL: Must be named valkey.sock
# Rspamd looks for valkey.sock specifically, not valkey-server.sock
unixsocket /run/valkey/valkey.sock
unixsocketperm 770

# Performance settings
maxmemory 256mb
maxmemory-policy volatile-ttl

# Note: For high-traffic mail servers (1000+ emails/hour) with sufficient RAM,
# you can increase maxmemory to 512mb or even 1gb to improve cache performance.
# This allows more greylisting entries, larger Bayes databases, and better
# statistics retention. Monitor with: valkey-cli -s /run/valkey/valkey.sock info memory

# Persistence (for greylisting data)
save 900 1
save 300 10
save 60 10000

# Directory for database files
dir /var/lib/valkey

# Log to journald (Debian 13 default)
loglevel notice

Key settings explained:

  • port 0: Disables TCP listening (security – prevents network access)
  • unixsocket /run/valkey/valkey.sock: Creates socket
  • unixsocketperm 770: Allows group access (rspamd user needs this)
  • maxmemory 256mb: Limits RAM usage (adjust based on your server)
    • 256MB is sufficient for most small-to-medium mail servers (< 1000 emails/hour)
    • For high-traffic servers (1000+ emails/hour), increase to 512mb or 1gb if RAM available
    • More memory allows: larger greylisting cache, more Bayes training data, better statistics
    • Monitor usage: valkey-cli -s /run/valkey/valkey.sock info memory
  • maxmemory-policy volatile-ttl: Rspamd-recommended eviction policy
    • Evicts keys with TTL set, choosing those closest to expiry
    • Respects Rspamd’s caching strategy
    • Only evicts data Rspamd intended to be temporary
  • save: Persistence settings – protects greylisting data across restarts
    • save 900 1: Save after 15 minutes if ≥1 key changed (low activity)
    • save 300 10: Save after 5 minutes if ≥10 keys changed (normal activity)
    • save 60 10000: Save after 1 minute if ≥10,000 keys changed (spam floods)
  • dir: Directory where Valkey saves database files

Why persistence matters: Without persistence, greylisting data is lost on restart. This means legitimate senders who were previously greylisted and passed would be greylisted again, causing temporary mail delays.

Why volatile-ttl policy: Rspamd sets TTLs on cache keys that should expire. The volatile-ttl policy respects these TTLs and evicts the oldest expiring keys first, which is exactly what Rspamd expects.

Start Valkey

# Enable and start Valkey
systemctl enable valkey
systemctl start valkey

# Check status
systemctl status valkey

Expected output:

● valkey.service - Valkey in-memory database
     Loaded: loaded (/lib/systemd/system/valkey.service; enabled)
     Active: active (running) since...

Note: The /run/valkey/ directory is automatically created by systemd when the service starts – no manual configuration needed!

Verify Unix Socket

# Check socket exists with CORRECT NAME
ls -la /run/valkey/valkey.sock

# Should show (with correct name!):
# srwxrwx--- 1 valkey valkey 0 Dec 16 12:34 valkey.sock

# NOT valkey-server.sock - that won't work!

If socket is named wrong (valkey-server.sock):

# Edit config to fix socket name
vi /etc/valkey/valkey.conf

# Ensure line reads:
# unixsocket /run/valkey/valkey.sock

# Restart Valkey
systemctl restart valkey

# Verify correct socket name
ls -la /run/valkey/valkey.sock

Test connection:

# Test with valkey-cli
valkey-cli -s /run/valkey/valkey.sock ping

# Should respond: PONG

If you get PONG, Valkey is working correctly with the proper socket name!

Add Rspamd User to Valkey Group

Rspamd needs permission to access the Valkey socket:

# Add _rspamd user to valkey group
usermod -a -G valkey _rspamd

# Verify group membership
groups _rspamd

# Should include: _rspamd valkey

Why this matters: Without group membership, Rspamd cannot connect to Valkey’s Unix socket, and statistics/greylisting won’t work.

Rspamd Installation

We’ll install Rspamd from the official repository to get the latest version with the newest spam detection rules.

Why Official Repository?

Debian 13 Repository:

  • Stable, well-tested
  • ❌ Older version (may lag behind by months)
  • ❌ Slower rule updates

Rspamd Official Repository:

  • Latest stable version (3.13.x or newer)
  • Frequent rule updates (2-4 times per day)
  • Latest spam detection techniques
  • Better spam filtering (spam evolves quickly)

Verdict: Use official repository for better spam detection.

Add Rspamd Official Repository

# Install prerequisites
apt install -y curl gnupg2 lsb-release

# Create keyrings directory
mkdir -p /etc/apt/keyrings

# Add Rspamd GPG key
curl -fsSL https://rspamd.com/apt-stable/gpg.key | \
  gpg --dearmor -o /etc/apt/keyrings/rspamd.gpg

# Add repository
echo "deb [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ $(lsb_release -cs) main" > \
  /etc/apt/sources.list.d/rspamd.list

# Update package list
apt update

Install Rspamd

# Install Rspamd without unnecessary dependencies
apt install --no-install-recommends -y rspamd

# Check installed version
rspamd --version

# Should show: Rspamd 3.13.x or newer

Verify Rspamd is Running

# Check service status
systemctl status rspamd

# Should show: Active: active (running)

Rspamd starts automatically with default configuration. We’ll customize it in the next sections.

Rspamd Base Configuration

Rspamd uses a layered configuration system:

  • /etc/rspamd/rspamd.conf – Main config (DON’T EDIT!)
  • /etc/rspamd/local.d/ – Local overrides (EDIT HERE!)
  • /etc/rspamd/override.d/ – Full overrides (rarely needed)

Best practice: All customizations go in /etc/rspamd/local.d/ – never edit the main config file.

Configure Valkey Connection

Tell Rspamd to use our Valkey Unix socket:

cat > /etc/rspamd/local.d/redis.conf << 'EOF'
# Valkey connection via Unix socket

# Primary connection (for all modules)
servers = "/run/valkey/valkey.sock";

# Explicit write/read configuration
write_servers = "/run/valkey/valkey.sock";
read_servers = "/run/valkey/valkey.sock";
EOF

Configuration explained:

  • servers: Default connection for all Rspamd modules
  • write_servers / read_servers: Explicit read/write paths (same socket)
  • Using Unix socket: Better performance and security than TCP

Configure Console Logging (systemd Journal)

Configure Rspamd to use journald, matching Postfix, Dovecot, and CrowdSec:

cat > /etc/rspamd/local.d/logging.inc << 'EOF'
# Console logging - outputs to journald (systemd)
type = "console";

# Logging level
level = "info";

# Log task details (spam scanning)
log_usec = true;
EOF

Why console logging?

  • Consistent with Debian 13: Postfix, Dovecot, CrowdSec all use journald
  • Unified monitoring: Single journalctl command for all mail services
  • systemd-native: Modern Debian approach
  • Centralized: All logs in journald, can forward to external systems

Configuration explained:

  • type = "console": Output to stdout/stderr → systemd captures → journald
  • level = "info": Standard verbosity (normal operation)
  • log_usec = true: Microsecond timestamps for precise timing

Alternative: File logging (if you prefer separate files):

# Not recommended - breaks consistency with Postfix/Dovecot
type = "file";
filename = "/var/log/rspamd/rspamd.log";

We use console logging to match the rest of the mail stack!

Configure Network Trust

If you’re using Incus/LXC containers or have specific internal networks, tell Rspamd to trust them:

First, identify your networks:

# Display your IPv4 addresses (excluding localhost)
ip -4 addr show | grep "inet " | grep -v "127.0.0.1"

# Display your IPv6 addresses (excluding localhost)
ip -6 addr show | grep "inet6 " | grep -v "::1"

Example output (Incus container):

inet 10.0.3.200/24 ...      # Container IPv4
inet6 fd42:1234:5678:9abc:1266:6aff:feaf:e302/64 ...  # Container IPv6

If you see container networks (10.x.x.x or fd42::/64), note the network addresses:

  • IPv4: 10.0.3.0/24 (network, not host IP)
  • IPv6: fd42:1234:5678:9abc::/64 (network prefix)

Create /etc/rspamd/local.d/options.inc:

cat > /etc/rspamd/local.d/options.inc << 'EOF'
# Local networks - Rspamd trust configuration

local_addrs = [
  # Localhost
  "127.0.0.0/8",
  "::1",
  
  # Link-local IPv6
  "fe80::/64",
  
  # Container networks (ADJUST TO YOUR ACTUAL NETWORKS!)
  # If you're in an Incus/LXC container, add your networks here:
  # "10.0.3.0/24",                      # Example: Container IPv4
  # "fd42:1234:5678:9abc::/64",         # Example: Container IPv6
];

# DNS configuration
dns {
  # Use Unbound on localhost (from Part 1)
  nameserver = ["127.0.0.1:53"];
  
  # Enable DNSSEC validation
  enable_dnssec = true;
}
EOF

Important: Only add container networks if you’re actually running in a container. For bare metal or VPS installations, the default localhost addresses are sufficient.

What to trust:

  • Always trust: localhost (127.0.0.0/8, ::1)
  • Always trust: link-local IPv6 (fe80::/64)
  • Trust if using containers: container bridge networks
  • ❌ Don’t trust: your server’s public IP (mail from the internet comes through this)

Configure Action Thresholds

Define what Rspamd does at different spam scores:

cat > /etc/rspamd/local.d/actions.conf << 'EOF'
# Spam action thresholds

# Reject obvious spam (score >= 15)
reject = 15.0;

# Add spam headers (score >= 6)
add_header = 6.0;

# Greylist suspicious mail (score >= 4)
greylist = 4.0;

# Scores explained:
# 0-3.99:   Clean mail → Deliver immediately to INBOX
# 4.0-5.99: Suspicious → Greylist (temporary defer, legitimate servers retry)
# 6.0-14.99: Spam → Add headers, deliver to Junk folder (via Sieve)
# 15.0+:    Obvious spam → Reject completely at SMTP level
EOF

Threshold explanation:

These are production-tested thresholds that balance spam blocking with false positives:

  • Below 4.0: Clean mail – delivered immediately
  • 4.0-5.99: Suspicious – greylisted (legitimate servers retry in 5+ minutes)
  • 6.0-14.99: Spam – headers added, moved to Junk by Sieve
  • 15.0+: Obvious spam – rejected at SMTP level (sender gets bounce)

Test Configuration

# Test Rspamd configuration for syntax errors
rspamadm configtest

# Should show: "syntax OK"

If you see errors: Double-check your configuration files for typos. Fix them before proceeding.

Restart Rspamd

# Restart Rspamd to load new configuration
systemctl restart rspamd

# Verify service started successfully
systemctl status rspamd

# Check for Valkey connection in journal
journalctl -u rspamd --since "1 minute ago" | grep -E "redis|valkey"
# Should show successful script uploads to unix:/run/valkey/valkey.sock

Expected: Service should be active (running) with no “cannot connect to redis” errors in the log.

If you see connection errors: Verify Valkey socket name is correct (valkey.sock not valkey-server.sock).

Rspamd-Postfix Integration

Now we connect Rspamd to Postfix using the milter protocol and configure spam headers.

What is a Milter?

A milter (mail filter) allows Postfix to:

  • Send mail to external programs during SMTP
  • Get analysis results back
  • Modify, accept, or reject messages based on results

Rspamd acts as a milter, analyzing mail as it arrives and adding headers.

Configure Rspamd Worker for Milter

Rspamd’s “worker-proxy” handles milter connections:

cat > /etc/rspamd/local.d/worker-proxy.inc << 'EOF'
# Milter worker configuration

# Bind to localhost TCP port 11332
bind_socket = "127.0.0.1:11332";

# Enable milter protocol
milter = yes;

# Timeout for connections
timeout = 120s;

# Upstream configuration (scan locally)
upstream "local" {
  default = yes;
  self_scan = yes;
}
EOF

Configuration explained:

  • bind_socket: Listen on localhost port 11332 (milter port)
  • milter = yes: Enable milter protocol
  • timeout: How long to wait for mail analysis (2 minutes)
  • upstream "local": Process mail locally (not forwarding to another Rspamd)

Why port 11332?

  • Standard Rspamd milter port
  • High port number (no special privileges needed)
  • TCP localhost (works with Postfix chroot)

Configure Spam Headers

Configure clear, user-friendly spam headers that email clients understand:

cat > /etc/rspamd/local.d/milter_headers.conf << 'EOF'
# Configure spam headers for email clients

# Standard headers to add
use = ["x-spam-status", "x-spam-level", "authentication-results"];

# Extended headers for detailed analysis
extended_spam_headers = true;

# X-Spam-Status header format
# Example: X-Spam-Status: Yes, score=7.5/15.0
x-spam-status {
  header = "X-Spam-Status";
}

# X-Spam-Level header (visual spam score)
# Example: X-Spam-Level: *******
x-spam-level {
  header = "X-Spam-Level";
  char = "*";
}

# Authentication-Results header (SPF, DKIM, DMARC)
authentication-results {
  header = "Authentication-Results";
}
EOF

Headers explained:

  • X-Spam-Status: Simple yes/no spam indicator
    • No, score=0.0 – Clean mail
    • Yes, score=12.5 – Spam detected
  • X-Spam-Level: Visual representation using stars
    • Empty = clean mail (score 0)
    • ******* = 7 spam points
  • X-Spamd-Result: Detailed analysis (added automatically)
    • Shows exact score: [0.00 / 15.00]
    • Lists triggered rules: R_SPF_ALLOW(-0.20), MIME_GOOD(-0.10)
    • Shows ASN, country, IP reputation
  • Authentication-Results: Email authentication status
    • SPF result: pass, fail, none
    • DKIM result: pass, fail, none
    • DMARC result: pass, fail, none

These headers are what Dovecot Sieve will check to move spam to Junk folders (configured later).

Configure Postfix to Use Rspamd

Add milter settings to Postfix:

# Configure milter integration
postconf -e "smtpd_milters = inet:127.0.0.1:11332"
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
postconf -e "milter_protocol = 6"
postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"
postconf -e "milter_default_action = accept"

# Verify settings
postconf -n | grep milter

Settings explained:

  • smtpd_milters: Use Rspamd for incoming mail (port 25)
  • non_smtpd_milters: Use Rspamd for locally-generated mail (bounce messages, etc.)
  • milter_protocol = 6: Milter protocol version (standard)
  • milter_mail_macros: Pass client information to Rspamd (IP, hostname, auth status)
  • milter_default_action = accept: If Rspamd is down, accept mail anyway (don’t lose mail!)

Apply Configuration

# Test Rspamd configuration
rspamadm configtest

# Should show: "syntax OK"

# Restart Rspamd to load milter and header configs
systemctl restart rspamd

# Verify Rspamd listening on milter port
ss -tlnp | grep 11332

# Should show: LISTEN 0 4096 127.0.0.1:11332

# Reload Postfix to activate milter
systemctl reload postfix

Test Complete Integration

Now test that milter AND headers work together:

# Source configuration
source /root/mail-server-vars.sh

# Send test email
echo "Testing Rspamd integration with headers" | mail -s "Rspamd Test" info@${DOMAIN}

Monitor logs during test (Postfix only – Rspamd details in journal):

# Watch Postfix mail flow
journalctl -u postfix -f

# Or watch complete mail stack (RECOMMENDED!)
journalctl -u postfix -u dovecot -u rspamd -f

# Postfix logs show mail flow:
# postfix/smtpd: connect from ...
# postfix/cleanup: message-id=<...>  ← Rspamd milter called here
# postfix/qmgr: from=<...>, size=..., nrcpt=1 (queue active)
# postfix/lmtp: to=<...>, status=sent
# postfix/qmgr: removed

# Rspamd shows detailed scoring in journal:
# rspamd_task_write_log: ... (no action): [-0.80/15.00] [R_SPF_ALLOW(-0.20), MIME_GOOD(-0.10)...]

Check delivered email headers (most reliable verification):

Open the delivered email in your mail client and view the message source. You should see:

Authentication-Results: mail.yourdomain.com;
    dkim=none (or pass if from external authenticated domain);
    spf=pass;
    dmarc=none (or pass if configured);

X-Rspamd-Action: no action
X-Spamd-Result: default: False [-0.80 / 15.00];
    R_SPF_ALLOW(-0.20)[+mx];
    MIME_GOOD(-0.10)[text/plain];
    ONCE_RECEIVED(0.20)[];
    (... more rules ...)
X-Rspamd-Server: mail.yourdomain.com
X-Rspamd-Queue-Id: ABC123
X-Spam-Status: No, score=-0.80

Perfect! Rspamd is now analyzing all mail and adding clear spam headers.

Rspamd Logging with systemd Journal

Important: Rspamd logs to systemd’s journal (journald), matching Postfix, Dovecot, and CrowdSec behavior on Debian 13.

How Rspamd Logging Works

We configured console logging (type = "console" in logging.inc):

Rspamd → stdout/stderr → systemd → journald
                                      ↓
                          (optional) rsyslog → files

Primary log location: journald (accessed via journalctl)
Secondary (optional): rsyslog can forward to files if configured

Why journald?

Consistency with mail stack:

  • Postfix uses journald
  • Dovecot uses journald
  • CrowdSec uses journald
  • Rspamd matches the pattern

Benefits:

  • Unified monitoring: Single command for all services
  • Structured logging: Searchable metadata
  • Time-based queries: Built-in date/time filtering
  • Centralized: Easy to forward to external systems
  • systemd-native: Modern Debian 13 approach

Monitoring Rspamd Logs

Watch Rspamd activity in real-time:

# Monitor Rspamd only
journalctl -u rspamd -f

# Monitor ALL mail services together (RECOMMENDED!)
journalctl -u postfix -u dovecot -u rspamd -f

# Include CrowdSec for complete picture
journalctl -u postfix -u dovecot -u rspamd -u crowdsec -f

Search recent Rspamd logs:

# Last 100 lines
journalctl -u rspamd -n 100

# Since specific time
journalctl -u rspamd --since "1 hour ago"

# Between times
journalctl -u rspamd --since "2 hours ago" --until "1 hour ago"

# Today's logs
journalctl -u rspamd --since today

Search for specific patterns:

# Search for mail processing
journalctl -u rspamd | grep "rspamd_task_write_log" | tail -n 20

# Search for DKIM activity
journalctl -u rspamd | grep -i dkim | tail -n 20

# Search for greylisting
journalctl -u rspamd | grep -i grey | tail -n 20

# Search for errors
journalctl -u rspamd | grep -i error | tail -n 20

# Search for Valkey connection
journalctl -u rspamd | grep -E "redis|valkey" | tail -n 20

Real-time filtered monitoring:

# Watch only DKIM activity
journalctl -u rspamd -f | grep --line-buffered -i dkim

# Watch only task processing
journalctl -u rspamd -f | grep --line-buffered "rspamd_task_write_log"

# Watch greylisting decisions
journalctl -u rspamd -f | grep --line-buffered -i grey

Example Rspamd Log Entries

Typical mail processing (from journald):

Dec 17 14:02:05 mail.example.com rspamd[525896]: 2025-12-17 14:02:05.19617 #525896(rspamd_proxy) <f9251d>; proxy; rspamd_task_write_log: 
id: <fc5bfa5b-5a90-456c-92fe-05a5f4342cab@example.com>, 
qid: <A281511EE4>, 
ip: 2001:db8::1, 
from: <user@example.com>, 
(default: F (no action): [-5.00/15.00] 
[REPLY(-4.00){},
DMARC_POLICY_ALLOW(-0.50){example.com;quarantine;},
R_DKIM_ALLOW(-0.20){example.com:s=mail;},
R_SPF_ALLOW(-0.20){+mx;},
MIME_GOOD(-0.10){text/plain;}]), 
len: 2066, time: 381.520ms, dns req: 30

What this shows:

  • Message ID and queue ID
  • Sender IP and email address
  • Final score: -5.00/15.00 (excellent – negative = trusted)
  • Action: F (no action) = delivered normally
  • Rules that fired: REPLY(-4.00) = reply to known conversation
  • Authentication: DMARC pass, DKIM pass, SPF pass
  • Processing time: 381ms, 30 DNS queries

Greylisting activity:

Dec 17 14:02:05 mail.example.com rspamd[525896]: lua; greylist.lua:360: Score too low - skip greylisting

DKIM signing:

Dec 17 14:02:05 mail.example.com rspamd[525896]: DKIM_SIGNED(0.00){example.com:s=mail;}

Valkey connection success:

Dec 17 08:51:31 mail.example.com rspamd[750134]: lua; lua_redis.lua:1560: uploaded redis script from file: bayes_cache_learn.lua to unix:/run/valkey/valkey.sock

Score Interpretation

Spam scores in logs:

  • Negative scores (< 0): Trusted senders with good authentication
    • Example: [-5.00/15.00] = reply to known conversation + perfect auth
  • 0-3.99: Clean mail, delivered to INBOX
  • 4.0-5.99: Suspicious, greylisted
  • 6.0-14.99: Spam, moved to Junk
  • 15.0+: Obvious spam, rejected

Unified Mail Monitoring

The power of journald – monitor entire mail stack:

# Watch complete mail flow
journalctl -u postfix -u dovecot -u rspamd -f

# You'll see:
# 1. Postfix receives mail: postfix/smtpd: connect from...
# 2. Rspamd analyzes: rspamd_task_write_log: ... score=X.X
# 3. Dovecot delivers: dovecot: lmtp: ... saved mail to INBOX

# Track specific email through entire pipeline
journalctl -u postfix -u dovecot -u rspamd | grep "user@domain.com"

# Find all activity in last hour
journalctl -u postfix -u dovecot -u rspamd --since "1 hour ago"

Journal Management

Check journal disk usage:

journalctl --disk-usage

Vacuum old logs (if journal too large):

# Keep only last 7 days
journalctl --vacuum-time=7d

# Limit to 500MB
journalctl --vacuum-size=500M

Journal configuration (/etc/systemd/journald.conf):

[Journal]
SystemMaxUse=500M      # Max disk space
MaxRetentionSec=7day   # Max retention time

Common Log Patterns

Successful mail scan:

journalctl -u rspamd | grep "rspamd_task_write_log" | tail -n 1
# Shows: (no action): [score/15.00] with rules

Greylisting:

journalctl -u rspamd | grep -i grey | tail -n 5
# Shows: "skip greylisting" (your mail) or "greylisted" (unknown sender)

DKIM signing:

journalctl -u rspamd | grep DKIM_SIGNED | tail -n 5
# Shows: DKIM_SIGNED(0.00){yourdomain.com:s=mail;}

Valkey connection:

journalctl -u rspamd --since "5 minutes ago" | grep -E "redis|valkey"
# Shows: uploaded redis script ... to unix:/run/valkey/valkey.sock

Errors:

journalctl -u rspamd -p err
# Shows only error-level messages

Remember: Journal is Primary

All monitoring uses journalctl:

  • journalctl -u rspamd for Rspamd logs
  • journalctl -u postfix for Postfix logs
  • journalctl -u dovecot for Dovecot logs
  • Combined for complete mail flow

This is the modern Debian 13 way!

DKIM Signing Setup

DKIM (DomainKeys Identified Mail) cryptographically signs your outgoing emails, proving they’re from you. This significantly improves deliverability.

Why DKIM is Critical

Without DKIM:

  • ❌ Your emails are more likely to be marked as spam
  • ❌ Recipients can’t verify your emails are legitimate
  • ❌ Failing DMARC authentication
  • ❌ Lower reputation with Gmail, Outlook, etc.

With DKIM:

  • Recipients can verify your emails are authentic
  • Better inbox placement (not spam folder)
  • DMARC passes
  • Professional email setup

How DKIM Works

  1. You generate a key pair: private key (server) + public key (DNS)
  2. Private key signs outgoing emails with a digital signature
  3. Public key published in DNS allows recipients to verify the signature
  4. If signature matches: Email is authentic ✅
  5. If signature doesn’t match: Email was modified or forged ❌

Generate DKIM Keys

Create a DKIM key for your domain:

# Source configuration
source /root/mail-server-vars.sh

# Create DKIM keys directory
mkdir -p ${DKIM_DIR}
cd ${DKIM_DIR}

# Generate DKIM key pair for your domain
rspamadm dkim_keygen \
  -s ${DKIM_SELECTOR} \
  -d ${DOMAIN} \
  -k ${DOMAIN}.${DKIM_SELECTOR}.key \
  > ${DOMAIN}.${DKIM_SELECTOR}.txt

# Verify files were created
ls -lh ${DKIM_DIR}/${DOMAIN}.*

Command explanation:

  • -s ${DKIM_SELECTOR}: Selector name (“mail” from your config)
  • -d ${DOMAIN}: Your domain from config (e.g., yourdomain.com)
  • -k filename.key: Private key filename
  • > filename.txt: DNS record output (public key)

Files created:

  • .key file: Private key (stays on server, signs mail) – KEEP SECRET!
  • .txt file: Public key (publish in DNS, verifies signatures) – Public

Set Proper Permissions

# Rspamd user must own keys
chown -R _rspamd:_rspamd ${DKIM_DIR}

# Private keys: read-only for owner (security!)
chmod 640 ${DKIM_DIR}/*.key

# DNS records: readable by all
chmod 644 ${DKIM_DIR}/*.txt

# Verify permissions
ls -la ${DKIM_DIR}/

Security note: Private keys must be protected! Only _rspamd user should read them. If someone steals your private key, they can forge emails from your domain!

View DNS Record

# Source configuration
source /root/mail-server-vars.sh

# Display the DNS TXT record you need to add
cat ${DKIM_DIR}/${DOMAIN}.${DKIM_SELECTOR}.txt

Example output:

mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
    "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC..." ) ;

Save this output – you’ll add it to DNS in the next step.

For Multiple Domains

If you send mail from multiple domains, generate a key for each:

# Source configuration
source /root/mail-server-vars.sh

# List your additional domains
ADDITIONAL_DOMAINS=("example.com" "anotherdomain.com")

# Generate for each domain
for EXTRA_DOMAIN in "${ADDITIONAL_DOMAINS[@]}"; do
  echo "=== Generating DKIM key for: $EXTRA_DOMAIN ==="
  
  rspamadm dkim_keygen \
    -s ${DKIM_SELECTOR} \
    -d $EXTRA_DOMAIN \
    -k ${DKIM_DIR}/${EXTRA_DOMAIN}.${DKIM_SELECTOR}.key \
    > ${DKIM_DIR}/${EXTRA_DOMAIN}.${DKIM_SELECTOR}.txt
  
  echo "✓ Created: ${EXTRA_DOMAIN}.${DKIM_SELECTOR}.key"
  echo ""
done

# Set permissions for all keys
chown -R _rspamd:_rspamd ${DKIM_DIR}
chmod 640 ${DKIM_DIR}/*.key
chmod 644 ${DKIM_DIR}/*.txt

Configure DKIM Signing in Rspamd

Tell Rspamd where to find the keys and how to sign:

cat > /etc/rspamd/local.d/dkim_signing.conf << 'EOF'
# DKIM signing configuration

# Path to DKIM keys (uses variables)
path = "/var/lib/rspamd/dkim/$domain.$selector.key";

# Selector name (must match key generation)
selector = "mail";

# Allow signing when envelope and header domains differ
allow_username_mismatch = true;

# Sign mail with empty envelope from (system notifications)
allow_envfrom_empty = true;

# Don't sign if From header doesn't match authenticated domain
allow_hdrfrom_mismatch = false;

# Use domain from From header for signing
use_domain = "header";

# Sign authenticated mail (your users' outgoing mail)
sign_authenticated = true;

# Don't sign local/internal mail
sign_local = false;
EOF

Configuration explained:

  • path: Rspamd automatically substitutes $domain and $selector with actual values
  • selector = "mail": Matches what we used in key generation
  • sign_authenticated = true: Signs mail from authenticated users (ports 587/465)
  • sign_local = false: Doesn’t sign mail from localhost (not needed)
  • use_domain = "header": Uses domain from “From:” header for signing

How it works: When a user sends mail through port 587/465 (authenticated), Rspamd:

  1. Extracts domain from “From:” header (e.g., user@yourdomain.com)
  2. Looks for key file: /var/lib/rspamd/dkim/yourdomain.com.mail.key
  3. Signs the email with this key
  4. Adds DKIM-Signature: header to the email

Test Configuration and Restart

# Test Rspamd configuration
rspamadm configtest

# Should show: "syntax OK"

# Restart Rspamd to load DKIM signing
systemctl restart rspamd

# Verify no errors in journal
journalctl -u rspamd --since "1 minute ago" | grep -i dkim

# Should see script uploads:
# uploaded redis script from file: dkim_signing.lua

If you see errors: Check that:

  • Key files exist: ls /var/lib/rspamd/dkim/
  • Permissions correct: ls -la /var/lib/rspamd/dkim/
  • Ownership correct: Files owned by _rspamd:_rspamd

Add DKIM DNS Record

Now publish the public key in DNS so recipients can verify signatures.

Prepare the DNS record:

# Source configuration
source /root/mail-server-vars.sh

# View the DNS record
cat ${DKIM_DIR}/${DOMAIN}.${DKIM_SELECTOR}.txt

# The record is formatted with line breaks for readability,
# but DNS needs it as a single string

In your DNS control panel (where you manage your domain):

Record Type: TXT
Name/Host: mail._domainkey (or mail._domainkey.yourdomain.com)
Value: Copy from the .txt file, removing all line breaks, quotes, and parentheses

Example transformation:

From file (formatted with line breaks):

mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
    "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC..." ) ;

To DNS (single line):

v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...entirekeyhere...IDAQAB

Important:

  • Remove ALL line breaks (make it one long line)
  • Remove opening and closing quotes "
  • Remove parentheses ( and )
  • Remove trailing semicolon ;
  • Remove the “IN TXT” part
  • Keep v=DKIM1; k=rsa; p= at the beginning
  • TTL: 3600 (1 hour)

Common mistakes:

  • ❌ Leaving line breaks in the value
  • ❌ Including quotes or parentheses
  • ❌ Wrong record name (must be mail._domainkey)
  • ❌ Missing the public key entirely

Wait for DNS Propagation

DNS changes take time to propagate. Wait 5-10 minutes, then verify:

# Source configuration
source /root/mail-server-vars.sh

# Check DNS record is published
dig +short ${DKIM_SELECTOR}._domainkey.${DOMAIN} TXT

# Should show: "v=DKIM1; k=rsa; p=..."

# Alternative check
host -t TXT ${DKIM_SELECTOR}._domainkey.${DOMAIN}

If the record doesn’t show up:

  • Wait longer (can take up to 1 hour for full propagation)
  • Check you entered the record correctly in DNS panel
  • Verify the record name: mail._domainkey.yourdomain.com
  • Check with your DNS provider’s help if needed

Test DKIM Signing

Send a test email and verify it’s signed:

# Source configuration
source /root/mail-server-vars.sh

# Send test email from your domain to an external address
echo "DKIM signing test" | mail -s "DKIM Test" test@gmail.com

Check email source in Gmail (or any recipient):

  1. Open the email
  2. Click “⋮” (three dots) → “Show original”
  3. Look for DKIM-Signature: header

Expected:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yourdomain.com;
    s=mail; t=1702483200;
    h=from:subject:date:to;
    bh=abc123...;
    b=xyz789...

In Gmail’s authentication results:

DKIM: 'PASS' with domain yourdomain.com

🎉 Success! Your outgoing mail is now DKIM-signed!

Troubleshooting: If DKIM fails:

  • Wait longer for DNS propagation
  • Verify DNS record is correct: dig +short mail._domainkey.yourdomain.com TXT
  • Check Rspamd journal: journalctl -u rspamd | grep -i dkim | tail -n 20
  • Verify key files exist and have correct permissions

SPF and DMARC DNS Records

DKIM alone isn’t enough – you also need SPF and DMARC for complete email authentication.

SPF (Sender Policy Framework)

SPF tells receiving servers which IP addresses are authorized to send mail for your domain.

How it works:

  • You publish a TXT record listing authorized mail servers
  • Recipient checks if mail came from an authorized IP
  • Authorized = SPF pass ✅
  • Not authorized = SPF fail ❌

Add SPF DNS Record

In your DNS control panel (for your domain):

Record Type: TXT
Name/Host: @ (or just your domain: yourdomain.com)
Value:

v=spf1 mx ~all

Explanation:

  • v=spf1: SPF version 1
  • mx: Servers listed in MX records can send mail
  • ~all: Soft fail for others (mark as suspicious but don’t reject)

More restrictive (if you only send from your mail server):

v=spf1 mx -all
  • -all: Hard fail (reject mail from any other server)

If you use external services (e.g., SendGrid, Mailgun):

v=spf1 mx include:sendgrid.net ~all

Production recommendation: Start with ~all (soft fail) for a week, then switch to -all (hard fail) once confirmed working.

Verify SPF Record

# Source configuration
source /root/mail-server-vars.sh

# Check SPF record
dig +short ${DOMAIN} TXT | grep spf

# Should show: "v=spf1 mx ~all" or similar

DMARC (Domain-based Message Authentication)

DMARC builds on SPF and DKIM, providing policy and reporting.

How it works:

  • Combines SPF and DKIM results
  • Defines policy (what to do with failures)
  • Provides reporting (aggregate reports of authentication results)

DMARC alignment:

  • SPF alignment: envelope-from domain matches From: header domain
  • DKIM alignment: DKIM signature domain matches From: header domain

Add DMARC DNS Record

In your DNS control panel:

Record Type: TXT
Name/Host: _dmarc (or _dmarc.yourdomain.com)
Value – Start with monitoring:

v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com; pct=100; adkim=r; aspf=r

Explanation:

  • v=DMARC1: DMARC version 1
  • p=none: Policy – monitor only (don’t reject, just report)
  • rua=mailto:...: Where to send aggregate reports
  • pct=100: Apply policy to 100% of mail
  • adkim=r: Relaxed DKIM alignment (subdomain OK)
  • aspf=r: Relaxed SPF alignment (subdomain OK)

Progressive DMARC deployment:

Phase 1 – Monitoring (first 1-2 weeks):

v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com; pct=100; adkim=r; aspf=r
  • Monitor authentication results
  • Review reports to ensure everything passes

Phase 2 – Quarantine (after confirming Phase 1 works):

v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com; pct=100; adkim=r; aspf=r
  • p=quarantine: Put suspicious mail in spam folder
  • Use after confirming legitimate mail passes authentication

Phase 3 – Reject (production policy):

v=DMARC1; p=reject; rua=mailto:dmarc-reports@yourdomain.com; pct=100; adkim=s; aspf=s
  • p=reject: Reject mail that fails authentication
  • adkim=s / aspf=s: Strict alignment (exact domain match)
  • Use only after thorough testing

Recommendation: Start with p=none for Part 4. Upgrade to p=quarantine in a week or two after confirming everything works.

Verify DMARC Record

# Source configuration
source /root/mail-server-vars.sh

# Check DMARC record
dig +short _dmarc.${DOMAIN} TXT

# Should show: "v=DMARC1; p=none; ..." or similar

Complete DNS Configuration Summary

For each domain you send mail from, you need these DNS records:

Record TypeName/HostValuePurpose
TXT@v=spf1 mx ~allSPF – authorize mail servers
TXTmail._domainkeyv=DKIM1; k=rsa; p=...DKIM public key
TXT_dmarcv=DMARC1; p=none; rua=...DMARC policy

All three are required for complete email authentication!

Test Complete Authentication

Send test email to Gmail:

# Source configuration
source /root/mail-server-vars.sh

echo "Complete authentication test" | mail -s "Auth Test" test@gmail.com

In Gmail, view message source (⋮ → Show original):

Authentication-Results: mx.google.com;
    spf=pass (google.com: domain of user@yourdomain.com designates ...) 
    dkim=pass header.i=@yourdomain.com header.s=mail
    dmarc=pass (p=NONE sp=NONE dis=NONE)

Triple pass! Your email authentication is perfect!

What this means:

  • SPF pass: Mail came from authorized server
  • DKIM pass: Signature verified authentic
  • DMARC pass: Both SPF and DKIM aligned and passed

Your emails now have maximum deliverability!

Dovecot Sieve Installation

Now that Rspamd is adding spam headers, we need Dovecot Sieve to automatically move spam to Junk folders.

What is Sieve?

Sieve is a mail filtering language that allows automatic mail processing:

  • Move mail to folders based on headers
  • Forward mail
  • Reply automatically
  • Discard mail

We’ll use it to check Rspamd’s spam headers and move spam to the Junk folder.

Install Sieve Packages

# Install Dovecot Sieve support
apt install -y dovecot-sieve dovecot-managesieved

Packages:

  • dovecot-sieve: Sieve mail filtering plugin
  • dovecot-managesieved: Allows users to manage Sieve scripts remotely (ManageSieve protocol)

Enable Sieve for LMTP

Edit /etc/dovecot/conf.d/20-lmtp.conf:

vi /etc/dovecot/conf.d/20-lmtp.conf

Find the protocol lmtp section and add Sieve:

protocol lmtp {
  mail_plugins {
    quota = yes
    sieve = yes     # ← Add this line
  }
  
  # ... rest of existing config ...
}

What this does: Enables Sieve filtering for mail delivered via LMTP (from Postfix to Dovecot).

Configure Sieve (Dovecot 2.4 Syntax)

Create /etc/dovecot/conf.d/90-sieve.conf:

cat > /etc/dovecot/conf.d/90-sieve.conf << 'EOF'
##
## Dovecot 2.4 Sieve Configuration
##

# Personal sieve script location
sieve_script personal {
  path = ~/sieve
  active_path = ~/.dovecot.sieve
}

# Global spam filter - runs BEFORE user scripts
sieve_script before {
  sieve_script_path = /var/lib/dovecot/sieve/spam-global.sieve
}

# Maximum script size
sieve_max_script_size = 1M

# Maximum number of actions per script
sieve_max_actions = 32
EOF

Configuration explained:

  • sieve_script personal: Users’ personal Sieve scripts
  • sieve_script before: Global script that runs before personal scripts
  • spam-global.sieve: The spam filter we’ll create next

Important: Dovecot 2.4 uses sieve_script blocks, NOT the old plugin {} syntax from Dovecot 2.3!

Test Configuration and Reload

# Test Dovecot configuration
doveconf -n

# Should show no errors

# Reload Dovecot
doveadm reload

Expected: Dovecot reloads successfully without errors.

Automatic Spam Filtering

Create the global Sieve script that moves spam to Junk folders.

How It Works

  1. Mail arrives with Rspamd headers (X-Spam: Yes or No)
  2. Dovecot LMTP receives mail
  3. Sieve filter checks X-Spam header
  4. If “Yes” → Move to Junk folder
  5. If “No” → Deliver to INBOX

Create Spam Filter Script

# Create sieve directory
mkdir -p /var/lib/dovecot/sieve
chown vmail:vmail /var/lib/dovecot/sieve
chmod 755 /var/lib/dovecot/sieve

# Create global spam filter script
cat > /var/lib/dovecot/sieve/spam-global.sieve << 'EOF'
require ["fileinto", "mailbox"];

##
## Global spam filter
## Automatically moves spam to Junk folder
##

# Check for Rspamd spam header
if header :is "X-Spam" "Yes" {
    # Create Junk folder if it doesn't exist and file message there
    fileinto :create "Junk";
    stop;
}
EOF

Script explanation:

  • require ["fileinto", "mailbox"]: Load required Sieve extensions
  • if header :is "X-Spam" "Yes": Check if spam header says “Yes”
  • fileinto :create "Junk": Move to Junk folder (create if doesn’t exist)
  • stop: Stop processing (don’t deliver to INBOX also)

Why :create? – Automatically creates the Junk folder if it doesn’t exist for a mailbox.

Compile the Sieve Script

# Compile script
sievec /var/lib/dovecot/sieve/spam-global.sieve

# Set proper ownership
chown vmail:vmail /var/lib/dovecot/sieve/spam-global.sieve*
chmod 644 /var/lib/dovecot/sieve/spam-global.sieve*

Files created:

  • spam-global.sieve: Source script (human-readable)
  • spam-global.svbin: Compiled binary (used by Dovecot)

Verify compilation:

# Check compiled script exists
ls -la /var/lib/dovecot/sieve/

# Should show both .sieve and .svbin files

If compilation fails: Check script syntax, fix errors, and recompile.

Reload Dovecot

# Reload Dovecot to activate spam filter
doveadm reload

# Check logs for any errors
journalctl -u dovecot -n 20

Expected: Dovecot reloads successfully, Sieve filter is active.

Test Spam Filtering

Send a test email and check if spam headers work:

# Source configuration
source /root/mail-server-vars.sh

# Send normal test email
echo "This is a clean test message" | mail -s "Clean Test" info@${DOMAIN}

Check delivered mail:

  • Should be in INBOX (not Junk)
  • Should have header: X-Spam: No

To test spam delivery: Wait until we have actual spam or send a high-scoring test message (covered in testing section).

Greylisting Configuration

Greylisting is a highly effective anti-spam technique that temporarily defers mail from unknown senders.

How Greylisting Works

First Contact (unknown sender):

  1. Unknown sender emails you
  2. Rspamd checks: not whitelisted, score ≥4.0
  3. Postfix responds: “450 4.7.1 Greylisted, try again later”
  4. Legitimate mail server: Queues message, retries in 5-10 minutes ✅
  5. Spam bot: Typically doesn’t retry, mail never delivered ❌

Second Contact (5+ minutes later):

  1. Legitimate server retries
  2. Rspamd recognizes sender (stored in Valkey)
  3. Mail delivered normally ✅

Future Emails:

  • Sender remembered for 24 hours (configurable)
  • No greylisting during this period
  • After 24 hours, pattern repeats if needed

Effectiveness: Blocks 70-90% of spam bots that don’t retry!

Why Greylisting Works

Spam bots:

  • ❌ Usually don’t retry (waste of resources)
  • ❌ Want instant delivery
  • ❌ Move on to next victim

Legitimate mail servers:

  • Always retry (RFC standard behavior)
  • Queue and automatically retry in 5-10 minutes
  • Eventually deliver the mail

Configure Greylisting

Create /etc/rspamd/local.d/greylist.conf:

# Source configuration (to use $DOMAIN variable)
source /root/mail-server-vars.sh

cat > /etc/rspamd/local.d/greylist.conf << 'EOF'
# Enable greylisting for spam prevention
enabled = true;

# Whitelist authenticated users (your outgoing mail)
whitelist_authenticated = true;

# Whitelist your mail infrastructure IPs
# IMPORTANT: Uncomment and replace these with YOUR actual IPs!
# Example for LXD/Incus mail server setup:
#whitelist_ip = [
#  "127.0.0.1",                  # Localhost
#  "::1",                        # Localhost IPv6
#  "203.0.113.10",               # Your mail server IPv4
#  "2001:db8::1",                # Your mail server IPv6
#  "10.0.3.0/24",                # Container network (adjust to your network)
#  "fd42:1234:5678:9abc::/64"    # Container IPv6 (adjust to your network)
#];

# Minimal config (works out-of-box, add your IPs above!)
whitelist_ip = [
  "127.0.0.1",                  # Localhost
  "::1"                         # Localhost IPv6
];

# Whitelist domains with good reputation
whitelist_domains_url = [
  "https://maps.rspamd.com/rspamd/dmarc_whitelist_new.inc.zst",
];

# Greylist settings
expire = 2592000;               # 30 days - how long to remember sender
timeout = 300;                  # 5 minutes - minimum retry time
key_prefix = "rg";              # Valkey key prefix
EOF

This config works immediately (passes syntax check), but you MUST add your actual IPs!

Find your public IPs:

# Get your IPv4
curl -4 ifconfig.me

# Get your IPv6  
curl -6 ifconfig.me

# Or check DNS
dig +short $(hostname -f) A
dig +short $(hostname -f) AAAA

Edit the config file:

nano /etc/rspamd/local.d/greylist.conf

Uncomment the example section and replace with your actual IPs:

  1. Find the commented example section (starts with #whitelist_ip = [)
  2. Uncomment all lines (remove the #)
  3. Replace example IPs with YOUR actual IPs
  4. Comment out or delete the minimal config section below it

After editing, your config should look like this:

# Whitelist your mail infrastructure IPs
# Example for LXD/Incus mail server setup:
whitelist_ip = [
  "127.0.0.1",                  # Localhost
  "::1",                        # Localhost IPv6
  "203.0.113.10",               # Your mail server IPv4 (YOUR IP!)
  "2001:db8::1",                # Your mail server IPv6 (YOUR IP!)
  "10.0.3.0/24",                # Container network (YOUR network!)
  "fd42:1234:5678:9abc::/64"    # Container IPv6 (YOUR network!)
];

# Delete or comment out the minimal config:
#whitelist_ip = [
#  "127.0.0.1",
#  "::1"
#];

Verify syntax:

rspamadm configtest
# Should output: syntax OK

**Configuration explained**:
- `enabled = true`: Turn on greylisting
- `whitelist_authenticated = true`: Never greylist your own users
- `whitelist_ip`: IPs that bypass greylisting (your mail servers)
  - **Commented example**: Full setup with all IPs (for reference)
  - **Minimal config**: Works immediately, add your IPs to the example above!
- `whitelist_domains_url`: Trusted domains from Rspamd (Google, Microsoft, etc.)
- `expire = 2592000`: Remember sender for **30 days** (recommended)
  - After passing once, sender is whitelisted for 30 days
  - Balances spam protection with legitimate sender experience
  - Alternatives: 7 days (604800), 14 days (1209600), 60 days (5184000)
  - **Don't use 24h (86400)** - too aggressive, re-greylists weekly senders!
- `timeout = 300`: Sender must wait 5 minutes before retry

**IMPORTANT**: You **must** add your mail server's public IP addresses to the commented example! Otherwise:
- Mail between your domains gets greylisted
- Internal notifications delayed
- Testing confusing

The minimal config works for initial testing, but uncomment and configure the full example for production use.

**Find your public IPs**:

```bash
# IPv4
curl -4 ifconfig.me

# IPv6
curl -6 ifconfig.me

# Or check DNS
dig +short mx1.yourdomain.com A
dig +short mx1.yourdomain.com AAAA

Why whitelist your own IPs?

  • Mail from your server to your server bypasses greylisting
  • Test emails work immediately
  • Internal system notifications not delayed

Test Configuration and Restart

# Test Rspamd configuration
rspamadm configtest

# Should show: "syntax OK"

# Restart Rspamd
systemctl restart rspamd

# Verify greylisting is active in journal
journalctl -u rspamd --since "1 minute ago" | grep -i grey

# Or watch in real-time:
# journalctl -u rspamd -f | grep --line-buffered -i grey

Monitor Greylisting

Watch for greylisting in action:

# Watch Postfix for temporary rejections (greylisting)
journalctl -u postfix -f | grep "4.7.1"

# You'll see Postfix rejecting with 450 code:
# postfix/smtpd: NOQUEUE: reject: ... 450 4.7.1 Greylisted, try again later

Note: Rspamd file log shows greylisting decisions, but Postfix logs show the actual rejections that users/senders see.

What to look for:

  • First attempt: 450 4.7.1 Greylisted, try again later (temporary rejection)
  • Retry (after 5+ min): Accepted and delivered normally

Example:

[First attempt - 12:00]
postfix/smtpd: NOQUEUE: reject: RCPT from unknown[1.2.3.4]: 450 4.7.1 <user@domain.com>: Recipient address rejected: Greylisted, try again later

[Retry - 12:06]
postfix/smtpd: connect from mail.example.com[1.2.3.4]
postfix/smtpd: ABC123: client=mail.example.com[1.2.3.4]
postfix/qmgr: ABC123: from=<sender@example.com>, status=sent

Greylisting Best Practices

Do whitelist:

  • Your own mail servers (all IPs)
  • Your container networks (internal mail)
  • Trusted providers (loaded from whitelist_domains_url)
  • Regular correspondents (if needed)

Don’t whitelist:

  • ❌ Unknown senders (defeats the purpose)
  • ❌ Entire IP ranges unless necessary
  • ❌ Suspicious networks

Tuning the expire time:

30 days (2592000) – Recommended default:

  • Balances protection and usability
  • Weekly senders pass immediately
  • Monthly contacts usually remembered
  • Rspamd’s standard recommendation

7 days (604800) – More aggressive:

  • Tighter spam control
  • ❌ Weekly senders might get greylisted occasionally
  • Use if spam is a major problem

60 days (5184000) – More lenient:

  • Almost never re-greylist legitimate senders
  • ❌ Spam sources stay whitelisted longer
  • Use for business environments with regular contacts

Never use 24 hours (86400):

  • ❌ Too aggressive – hurts legitimate senders
  • ❌ Weekly contacts always delayed
  • ❌ Poor user experience

Other adjustment options:

  • Increase timeout to 600 (10 minutes) if too lenient
  • Decrease expire to 1209600 (14 days) as middle ground
  • Add specific domains to whitelist if problematic

DANE/TLSA Records

DANE (DNS-based Authentication of Named Entities) using TLSA records adds certificate pinning via DNS.

Why DANE Matters

  • Enhanced security: Publishes certificate fingerprint in DNS
  • MITM protection: Prevents man-in-the-middle attacks
  • Certificate validation: Recipients can verify your cert is authentic
  • Internet.nl Hall of Fame requirement

How DANE Works

  1. You publish TLSA records in DNS with certificate fingerprints
  2. Recipients look up TLSA records via DNSSEC
  3. Recipients compare certificate fingerprint to TLSA record
  4. Match = authentic certificate ✅
  5. Mismatch = possible attack ❌

Requirements:

  • DNSSEC must be enabled (from Part 1)
  • Valid SSL/TLS certificates
  • TLSA records published in DNS

Understanding TLSA Selectors

We’ll publish two TLSA records per port (selector 0 and selector 1):

Selector 0 (Full certificate):

  • Pins the exact certificate
  • Hash of entire certificate
  • Must update DNS on every renewal (every 90 days)

Selector 1 (Public key):

  • Pins only the public key
  • Hash of public key only
  • Survives renewals if key is reused (less maintenance)

Best practice: Publish both for redundancy and graceful renewal transitions.

Install Required Tool

# Install hash-slinger (provides 'tlsa' command)
apt install -y hash-slinger

Generate TLSA Records

We’ll generate TLSA records for all three mail ports:

  • Port 25 (SMTP – incoming mail)
  • Port 587 (Submission with STARTTLS)
  • Port 465 (SMTPS – implicit TLS)
# Source configuration
source /root/mail-server-vars.sh

# Suppress Python warnings from hash-slinger
export PYTHONWARNINGS="ignore::SyntaxWarning"

echo "=== TLSA Records for $MX1_FQDN ==="
echo ""

# Port 25 (SMTP - incoming mail)
echo "Port 25 (SMTP):"
echo ""
echo "Selector 0 (Full certificate):"
tlsa --create --certificate ${SSL_FULLCHAIN} \
     --output rfc --usage 3 --selector 0 --mtype 1 \
     --protocol tcp --port 25 ${MX1_FQDN}

echo ""
echo "Selector 1 (Public key):"
tlsa --create --certificate ${SSL_FULLCHAIN} \
     --output rfc --usage 3 --selector 1 --mtype 1 \
     --protocol tcp --port 25 ${MX1_FQDN}

echo ""
echo "---"
echo ""

# Port 587 (Submission with STARTTLS)
echo "Port 587 (Submission):"
echo ""
echo "Selector 0:"
tlsa --create --certificate ${SSL_FULLCHAIN} \
     --output rfc --usage 3 --selector 0 --mtype 1 \
     --protocol tcp --port 587 ${MX1_FQDN}

echo ""
echo "Selector 1:"
tlsa --create --certificate ${SSL_FULLCHAIN} \
     --output rfc --usage 3 --selector 1 --mtype 1 \
     --protocol tcp --port 587 ${MX1_FQDN}

echo ""
echo "---"
echo ""

# Port 465 (SMTPS - implicit TLS)
echo "Port 465 (SMTPS):"
echo ""
echo "Selector 0:"
tlsa --create --certificate ${SSL_FULLCHAIN} \
     --output rfc --usage 3 --selector 0 --mtype 1 \
     --protocol tcp --port 465 ${MX1_FQDN}

echo ""
echo "Selector 1:"
tlsa --create --certificate ${SSL_FULLCHAIN} \
     --output rfc --usage 3 --selector 1 --mtype 1 \
     --protocol tcp --port 465 ${MX1_FQDN}

Command explanation:

  • --certificate ${SSL_FULLCHAIN}: Path to your certificate
  • --usage 3: DANE-EE (end entity certificate)
  • --selector 0: Full certificate
  • --selector 1: Public key only
  • --mtype 1: SHA-256 hash
  • --port 25/587/465: Service port
  • --protocol tcp: Protocol

Output: You’ll get 6 TLSA records total (2 per port).

Example output:

_25._tcp.mx1.yourdomain.com. IN TLSA 3 0 1 abc123...
_25._tcp.mx1.yourdomain.com. IN TLSA 3 1 1 def456...

Note: The Python warnings about syntax are harmless – they’re from Python 2→3 compatibility in hash-slinger. The output is valid despite the warnings.

Add TLSA DNS Records

In your DNS control panel, add these 6 TLSA records:

TypeNameValue
TLSA_25._tcp.mx13 0 1 <hash>
TLSA_25._tcp.mx13 1 1 <hash>
TLSA_587._tcp.mx13 0 1 <hash>
TLSA_587._tcp.mx13 1 1 <hash>
TLSA_465._tcp.mx13 0 1 <hash>
TLSA_465._tcp.mx13 1 1 <hash>

DNS Settings:

  • Type: TLSA
  • Name: _25._tcp.mx1 (or full: _25._tcp.mx1.yourdomain.com)
  • Value: 3 0 1 <hash> (exactly as shown in output, including the numbers)
  • TTL: 3600 (1 hour)

Important:

  • Copy the entire line including 3 0 1 and the hash
  • Don’t modify the format
  • Don’t remove the numbers before the hash
  • DNSSEC must be enabled for DANE to work!

Verify TLSA Records

Wait 5-10 minutes for DNS propagation, then check:

# Source configuration
source /root/mail-server-vars.sh

# Check port 25 TLSA records
dig +short TLSA _25._tcp.${MX1_FQDN}

# Should show TWO records (both selectors)

# Check all ports
for PORT in 25 587 465; do
  echo "Port $PORT:"
  dig +short TLSA _${PORT}._tcp.${MX1_FQDN}
  echo ""
done

Expected: Each port shows 2 TLSA records (selector 0 and selector 1).

If records don’t show:

  • Wait longer for DNS propagation
  • Verify DNSSEC is enabled: dig +dnssec yourdomain.com
  • Check you entered records correctly in DNS panel
  • Verify record names are correct: _25._tcp.mx1.yourdomain.com

Certificate Renewal Consideration

When your Let’s Encrypt certificate renews (every 90 days):

Selector 0 (full certificate):

  • ❌ Hash changes on every renewal
  • ❌ Must update DNS records
  • ⏰ Plan to update TLSA records after each renewal

Selector 1 (public key):

  • Hash stays same if key is reused
  • No DNS update needed
  • Provides continuity during renewals

Renewal process:

  1. Certificate renews (automatic via acme.sh)
  2. Generate new TLSA records (run the tlsa commands again)
  3. Update selector 0 TLSA records in DNS
  4. Selector 1 records usually don’t need updating

Automation option: You can add a renewal hook to remind you to update TLSA records.

Testing and Verification

Time to test the complete mail filtering pipeline!

Important: CrowdSec Considerations

If you completed Part 3 (CrowdSec Protection): Online testing services like internet.nl and MXtoolbox need to be whitelisted BEFORE testing. These services perform security scans (port scanning, rapid connections) that CrowdSec interprets as attacks. Without whitelisting, these services get banned and testing fails.

Required whitelists (from Part 3):

# Create testing services allowlist
cscli allowlist create testing-services \
  --description "Mail testing and monitoring services"

# Whitelist internet.nl (IPv4 and IPv6)
cscli allowlist add testing-services 62.204.66.0/24
cscli allowlist add testing-services 2a00:d00:ff:162::/64

# Whitelist MXtoolbox (already in CAPI blocklist)
cscli allowlist add testing-services 18.209.86.113

# Verify whitelist
cscli allowlist inspect testing-services

# Reload CrowdSec
systemctl reload crowdsec

If you haven’t completed Part 3 yet: These tests work normally without CrowdSec. You can complete testing now and install CrowdSec protection later.

Why whitelisting is needed: Security testing services probe multiple ports, make rapid connections, and perform “attacks” as part of their security assessment. CrowdSec sees this as malicious activity and bans the IPs, preventing you from completing Hall of Fame testing.

Refer to Part 3: CrowdSec Mail Server Protection for complete CrowdSec setup and whitelist configuration.

Test 1: Normal Email

Send a clean email and verify it delivers to INBOX with correct headers:

# Source configuration
source /root/mail-server-vars.sh

# Send clean email
echo "This is a legitimate test message from $(date)" | \
  mail -s "Clean Mail Test" info@${DOMAIN}

Expected results:

  • Delivers to INBOX (not Junk)
  • Has header: X-Spam: No
  • Low spam score: 0-3 points or negative
  • Has authentication headers

Check email headers (open email, view source):

X-Rspamd-Action: no action
X-Spamd-Result: default: False [-0.80 / 15.00];
    R_SPF_ALLOW(-0.20)[+mx];
    MIME_GOOD(-0.10)[text/plain];
X-Spam-Status: No, score=-0.80
Authentication-Results: mail.yourdomain.com;
    spf=pass;
    dkim=pass (if configured);

Monitor Rspamd activity during test:

# Watch Rspamd process the mail in real-time
journalctl -u rspamd -f | grep --line-buffered "rspamd_task_write_log"

# You'll see detailed scoring:
# rspamd_task_write_log: ... (no action): [-0.80/15.00] [R_SPF_ALLOW(-0.20), MIME_GOOD(-0.10)...]

Test 2: External Email

Have someone send you an email from Gmail, Outlook, or another provider.

Expected results:

  • Delivers to INBOX (if legitimate)
  • Rspamd analyzes and adds headers
  • SPF/DKIM/DMARC checked (shows in Authentication-Results)
  • Appropriate spam score

Check headers:

X-Rspamd-Action: no action
X-Spamd-Result: default: False [-4.80 / 15.00];
    REPLY(-4.00){};                    # Reply to known conversation
    DMARC_POLICY_ALLOW(-0.50){};
    R_DKIM_ALLOW(-0.20){};
    R_SPF_ALLOW(-0.20){};
X-Spam-Status: No, score=-4.80
Authentication-Results: mx1.yourdomain.com;
    spf=pass;
    dkim=pass;
    dmarc=pass;

Test 3: Spam Scoring

Note: Real spam testing requires actual external spam, which is difficult to generate safely. For now, verify spam headers are being added correctly. Actual spam will be caught naturally over time.

Monitor statistics:

rspamc stat

# Shows:
# Messages scanned: 31
# Messages with action reject: 3, 9.68%    # Spam blocked!
# Messages with action no action: 28       # Clean mail

Test 4: DKIM Signing

Send mail from your domain and verify DKIM signature:

# Source configuration
source /root/mail-server-vars.sh

# Send to external address (Gmail recommended)
echo "DKIM signature test - $(date)" | \
  mail -s "DKIM Test" test@gmail.com

In Gmail:

  1. Open the email
  2. Click ⋮ (three dots) → “Show original”
  3. Look for:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yourdomain.com;
    s=mail; ...

And in authentication results:

spf=pass
dkim=pass header.i=@yourdomain.com
dmarc=pass

All pass! Perfect authentication!

Check journal for DKIM activity:

# Check for DKIM signing activity (after sending test)
journalctl -u rspamd | grep -i dkim | tail -n 20

# Should see DKIM signing activity:
# uploaded redis script ... dkim_signing.lua
# DKIM_SIGNED(0.00){yourdomain.com:s=mail;}

# Or watch in real-time while sending:
# journalctl -u rspamd -f | grep --line-buffered -i dkim

Test 5: Greylisting

Send email from a new external source (not previously seen):

# Ask a friend or colleague to send you an email
# Or use a different email service you haven't used before

Watch logs:

# Monitor Postfix for greylisting (Rspamd runs silently)
journalctl -u postfix -f | grep -E "4.7.1|Greylisted"

# Look for:
# First attempt: "450 4.7.1 Greylisted"
# Retry 5+ min later: Accepted and delivered

Expected:

  • First attempt: Temporary rejection (450)
  • Sender’s server queues and retries
  • 5-10 minutes later: Mail delivered
  • Future emails from same sender: No greylisting

Check journal for greylisting decisions:

# Check historical greylisting decisions
journalctl -u rspamd | grep -i grey | tail -n 20

# Shows:
# skip greylisting for local networks  # Your own mail
# greylisted ...  # Unknown sender temporarily deferred

# Or watch in real-time:
# journalctl -u rspamd -f | grep --line-buffered -i grey

Test 6: Online Testing Services

Use these services to verify your setup:

Mail Tester (https://www.mail-tester.com):

  1. Get temporary test address from site
  2. Send email: echo "Test" | mail -s "Mail Test" test-xxxxx@mail-tester.com
  3. Check your score
  4. Target: 10/10 score

Internet.nl Mail Test (https://internet.nl/mail/):

  1. Enter your domain: yourdomain.com
  2. Run the test
  3. Target: Hall of Fame status (100%)
  4. Verify:
    • IPv6 Support
    • DNSSEC
    • DANE (TLSA records)
    • DKIM
    • SPF
    • DMARC

MXToolbox (https://mxtoolbox.com):

  • DKIM Lookup: Verify your DKIM record
  • SPF Lookup: Verify SPF record
  • DMARC Lookup: Verify DMARC record
  • Blacklist Check: Ensure your IP isn’t blacklisted

Important reminder: If CrowdSec is installed (Part 3), whitelist these testing services first.

Verify Spam Folder Delivery

When actual spam arrives, verify it goes to Junk:

# Check Junk folder for a mailbox
doveadm search -u info@${DOMAIN} mailbox Junk ALL

# View headers of messages in Junk
doveadm fetch -u info@${DOMAIN} "hdr.x-spam" mailbox Junk ALL

# Should show: X-Spam: Yes

Check Rspamd Statistics

# View overall statistics
rspamc stat

# Should show:
# Messages scanned
# Actions taken (no action, greylist, add header, reject)
# Learning statistics (if trained)

Monitor Logs

Unified mail stack monitoring (RECOMMENDED):

# Watch ALL mail services together
journalctl -u postfix -u dovecot -u rspamd -f

# Complete mail flow visible:
# 1. Postfix: connect, cleanup, qmgr
# 2. Rspamd: task processing, scoring
# 3. Dovecot: LMTP delivery

Rspamd-specific monitoring:

# Watch Rspamd activity in real-time
journalctl -u rspamd -f

# Recent Rspamd activity (last 20 entries)
journalctl -u rspamd -n 20

# Recent spam decisions (last 20)
journalctl -u rspamd | grep "rspamd_task_write_log" | tail -n 20

# Check for errors (recent)
journalctl -u rspamd | grep -i error | tail -n 20

# Check Valkey connection status
journalctl -u rspamd | grep -E "redis|valkey" | tail -n 20

Postfix + Dovecot (mail flow):

# Watch mail flow
journalctl -u postfix -u dovecot -f

# Postfix shows:
# - Connections: postfix/smtpd: connect from ...
# - Cleanup: postfix/cleanup: message-id=<...>
# - Delivery: postfix/lmtp: status=sent

# Dovecot shows:
# - Sieve processing: sieve: stored mail into mailbox 'Junk'

What to look for in journal:

  • Mail scanning: rspamd_task_write_log entries
  • Spam scores: score=X.X/15.0
  • Actions: no action, greylist, add header, reject
  • DKIM signing: DKIM_SIGNED(0.00){yourdomain.com}
  • Greylisting: greylisted or skip greylisting for local networks
  • Valkey: uploaded redis script (connection working)

Command-Line Management

Rspamd provides excellent command-line tools for monitoring and management. The web interface is optional and will be covered in Part 6 with CrowdSec protection.

Monitoring Commands

View statistics:

# Overall statistics
rspamc stat

# Shows:
# - Messages scanned
# - Spam/ham ratio
# - Actions taken
# - Learning statistics

View recent scans (from journal):

# Last 20 mail scans with full details
journalctl -u rspamd | grep "rspamd_task_write_log" | tail -n 20

# Shows spam scores and actions for each message

Check learning status:

# Bayes and learning statistics
rspamc stat | grep -E "Learned|Messages"

Testing Commands

Test message scoring:

# Test a message file
rspamc < /path/to/message.eml

# Shows spam score and triggered rules

Test specific rules:

# Test DKIM signing
echo "test" | rspamc -h from:test@${DOMAIN}

# Test spam detection
cat spam-sample.eml | rspamc

Training Commands

Manual learning (train Rspamd):

# Train as spam
rspamc learn_spam < spam-message.eml

# Train as ham (not spam)
rspamc learn_ham < ham-message.eml

# Train from mailbox folder
doveadm fetch -u user@domain.com "text" mailbox Junk ALL | rspamc learn_spam
doveadm fetch -u user@domain.com "text" mailbox INBOX ALL | rspamc learn_ham

Configuration Commands

Test configuration:

# Check syntax
rspamadm configtest

# View active configuration
rspamadm configdump

# View specific module config
rspamadm configdump | grep -A10 dkim_signing

Reload configuration:

# Reload without restart (graceful)
systemctl reload rspamd

# Full restart (if needed)
systemctl restart rspamd

Troubleshooting Commands

Check service status:

# Service status
systemctl status rspamd

# Recent service messages
journalctl -u rspamd -n 50

# Follow service messages
journalctl -u rspamd -f

Check journal for activity:

# Watch journal in real-time
journalctl -u rspamd -f

# Recent errors
journalctl -u rspamd | grep -i error | tail -n 20

# Check Valkey connection
journalctl -u rspamd | grep -E "redis|valkey" | tail -n 20
# Should show: uploaded redis script ... to unix:/run/valkey/valkey.sock

Check connections:

# Verify Rspamd is listening
ss -tlnp | grep rspamd

# Should show:
# 127.0.0.1:11332 (milter)
# 127.0.0.1:11334 (web interface)
# 127.0.0.1:11333 (normal worker)

Check Valkey connection:

# Test Valkey connection
valkey-cli -s /run/valkey/valkey.sock ping

# Should respond: PONG

Useful Rspamd Commands Summary

# Status and statistics
rspamc stat                              # Overall statistics
rspamc -h stat                           # Detailed stats with hostname
journalctl -u rspamd -f                  # Watch journal

# Unified mail monitoring
journalctl -u postfix -u dovecot -u rspamd -f   # All mail services

# Testing
rspamc < message.eml                     # Test message scoring
echo "test" | rspamc                     # Quick spam test

# Training
rspamc learn_spam < spam.eml             # Train as spam
rspamc learn_ham < ham.eml               # Train as ham

# Configuration
rspamadm configtest                      # Test config syntax
rspamadm configdump | less               # View active config
systemctl reload rspamd                  # Reload config

# Troubleshooting
systemctl status rspamd                  # Service status
ss -tlnp | grep 11332                    # Check milter listening
journalctl -u rspamd -f                  # Follow journal
journalctl -u rspamd | grep -i error     # Check for errors

Note: The web interface for Rspamd and Roundcube webmail will be covered in Part 6: Rspamd Web Interface & Roundcube, where we’ll set them up with Apache reverse proxy and integrate with CrowdSec protection from Part 3.

Troubleshooting

Common issues and solutions.

Rspamd Not Scanning Mail

Symptoms: No spam headers added, mail goes through unfiltered.

Check 1: Valkey socket name and connection:

# Verify socket exists with correct name
ls -la /run/valkey/valkey.sock
# Should show: srwxrwx--- 1 valkey valkey 0 ... /run/valkey/valkey.sock

# If socket is named valkey-server.sock instead:
vi /etc/valkey/valkey.conf
# Change: unixsocket /run/valkey/valkey-server.sock
# To:     unixsocket /run/valkey/valkey.sock
# Then: systemctl restart valkey

Check 2: View Rspamd journal:

journalctl -u rspamd -f

# Look for:
# - "cannot connect to redis" → Valkey connection problem
# - "proxy_accept_socket: accepted milter connection" → Working!
# - "rspamd_task_write_log" → Mail being processed

Check 3: Verify milter connection:

# Verify Rspamd listening on milter port
ss -tlnp | grep 11332

# Should show: LISTEN ... 127.0.0.1:11332

# Check Postfix milter config
postconf -n | grep milter

# Should show: smtpd_milters = inet:127.0.0.1:11332

Check 4: Test Valkey connection:

valkey-cli -s /run/valkey/valkey.sock ping
# Should respond: PONG

# If socket doesn't exist, check Valkey is running:
systemctl status valkey

Common fixes:

  • Fix socket name in /etc/valkey/valkey.conf
  • Restart both services: systemctl restart valkey rspamd
  • Verify worker-proxy configured: cat /etc/rspamd/local.d/worker-proxy.inc
  • Check for configuration errors: rspamadm configtest

Valkey Connection Errors

Symptoms: File log shows “cannot connect to redis /run/valkey/valkey.sock”

Diagnostic script:

echo "=== Valkey Status ==="
systemctl status valkey

echo ""
echo "=== Socket Check ==="
ls -la /run/valkey/valkey.sock 2>/dev/null || echo "Socket doesn't exist!"

echo ""
echo "=== Socket Name ==="
ls -la /run/valkey/*.sock

echo ""
echo "=== Valkey Test ==="
valkey-cli -s /run/valkey/valkey.sock ping 2>&1 || echo "Cannot connect!"

echo ""
echo "=== Rspamd Group ==="
groups _rspamd | grep valkey && echo "✓ In group" || echo "✗ Not in valkey group"

echo ""
echo "=== Valkey Config ==="
grep -E "^unixsocket|^port" /etc/valkey/valkey.conf

Fix socket name:

# Edit Valkey config
vi /etc/valkey/valkey.conf

# Ensure socket path is:
unixsocket /run/valkey/valkey.sock

# NOT:
# unixsocket /run/valkey/valkey-server.sock

# Restart
systemctl restart valkey

# Verify correct socket
ls -la /run/valkey/valkey.sock

Fix permissions:

# Add user to group
usermod -a -G valkey _rspamd

# Restart both services
systemctl restart valkey rspamd

# Verify no connection errors in journal
journalctl -u rspamd --since "1 minute ago" | grep -E "redis|valkey"
# Should show successful script uploads to unix:/run/valkey/valkey.sock

DKIM Signature Failures

Symptoms: DKIM fails validation, emails marked as suspicious.

Check DNS record:

# Source configuration
source /root/mail-server-vars.sh

# Verify DKIM record in DNS
dig +short ${DKIM_SELECTOR}._domainkey.${DOMAIN} TXT

# Should show: "v=DKIM1; k=rsa; p=..."

Check for multiple records (only one should exist per selector):

dig ${DKIM_SELECTOR}._domainkey.${DOMAIN} TXT
# Should show only ONE TXT record

Verify key permissions:

ls -la /var/lib/rspamd/dkim/
# Should be owned by _rspamd:_rspamd
# Keys should be chmod 640

Check journal:

journalctl -u rspamd | grep -i dkim | tail -n 20
# Look for signing errors or missing keys

Common fixes:

  • Wait for DNS propagation (up to 1 hour)
  • Remove old DKIM records from DNS
  • Regenerate keys if corrupted
  • Restart Rspamd: systemctl restart rspamd

Greylisting Blocking Everything

Symptoms: All external mail delayed 5+ minutes, including from known senders.

Check whitelist:

cat /etc/rspamd/local.d/greylist.conf | grep whitelist_ip
# Should include your mail server IPs

Add your IPs to whitelist:

vi /etc/rspamd/local.d/greylist.conf

# Add your actual IPs:
whitelist_ip = [
  "YOUR_IPv4_HERE",
  "YOUR_IPv6_HERE",
  "127.0.0.1",
  "::1",
];

# Test config and restart
rspamadm configtest
systemctl restart rspamd

Temporarily disable for testing:

# Edit config
vi /etc/rspamd/local.d/greylist.conf

# Set:
enabled = false;

# Restart
systemctl restart rspamd

Spam Not Moving to Junk

Symptoms: Spam headers added but mail stays in INBOX.

Check Sieve is enabled:

# Check LMTP configuration
doveconf -n | grep -A10 "protocol lmtp"
# Should show: sieve = yes

Check global script:

# Verify script exists
ls -la /var/lib/dovecot/sieve/
# Should show .sieve and .svbin files

# Test script syntax
sievec /var/lib/dovecot/sieve/spam-global.sieve
# Should complete without errors

Check Dovecot logs:

journalctl -u dovecot -f
# Should show sieve processing
# Look for: "sieve: msgid=... stored mail into mailbox 'Junk'"

Common fixes:

  • Recompile Sieve script: sievec /var/lib/dovecot/sieve/spam-global.sieve
  • Reload Dovecot: doveadm reload
  • Check script ownership: chown vmail:vmail /var/lib/dovecot/sieve/*

High Memory Usage

Symptoms: Valkey or Rspamd using excessive RAM.

Check Valkey memory usage:

# Detailed memory info
valkey-cli -s /run/valkey/valkey.sock info memory

# Check current usage vs limit
valkey-cli -s /run/valkey/valkey.sock info memory | grep -E "used_memory_human|maxmemory_human"

# Example output:
# used_memory_human:245.67M
# maxmemory_human:256.00M

If usage is consistently near maxmemory:

Option 1: Increase memory (recommended for high-traffic servers):

vi /etc/valkey/valkey.conf

# For high-traffic servers (1000+ emails/hour):
maxmemory 512mb  # Double the limit

# For very high-traffic (5000+ emails/hour):
maxmemory 1gb    # Even more cache space

# Restart
systemctl restart valkey

Benefits of more memory:

  • Larger greylisting cache (remember more senders)
  • More Bayes training data stored
  • Better statistics and reputation tracking
  • Fewer cache evictions
  • Better spam detection accuracy

Option 2: Keep current limit (if memory is constrained):

# Current 256MB is sufficient for most servers
# Valkey will automatically evict old entries using volatile-ttl policy
# This is perfectly fine for smaller mail servers

Check Rspamd memory:

ps aux | grep rspamd
# Check RSS column for memory usage

Typical memory usage:

  • Valkey: 50-256MB (working set) + small RDB files on disk
  • Rspamd: 50-150MB per worker process
  • Total: ~200-500MB for spam filtering stack

Note: For servers handling < 1000 emails/hour, 256MB Valkey memory is optimal. Only increase for genuinely high-traffic scenarios with sufficient available RAM.

No Activity in Journal

Symptoms: Journal shows no mail processing activity.

Verify mail flow:

# Send test mail
source /root/mail-server-vars.sh
echo "test" | mail -s "Log Test" info@${DOMAIN}

# Watch journal
journalctl -u rspamd -f

# Should see:
# proxy_accept_socket: accepted milter connection
# rspamd_task_write_log: ... (action): [score/15.00]

If still no activity:

  • Postfix not forwarding to Rspamd (check milter config)
  • Rspamd worker not started (check systemctl status rspamd)
  • Console logging not configured (check /etc/rspamd/local.d/logging.inc)

What’s Coming Next

Part 5: Advanced Mail Filtering (Optional enhancements):

  • ClamAV antivirus – Scan attachments for viruses/malware
  • Razor – Collaborative spam signature network
  • Pyzor – Spam digest database
  • DCC – Distributed checksum clearinghouse
  • Neural networks – Pattern recognition and learning
  • Fuzzy hashing – Near-duplicate spam detection
  • Bayes classifier – Statistical learning
  • Self-learning – Train Rspamd from user actions

Part 6: Rspamd Web Interface & Roundcube (Management & Webmail):

  • Rspamd web interface – Monitoring and management UI
  • Roundcube webmail – Full-featured webmail client
  • Apache reverse proxy – Secure web access
  • Authentication – Password protection
  • Integration with existing setup – Works with CrowdSec from Part 3
  • Web-based statistics – Visual monitoring and graphs
  • Manual learning interface – Train spam filter via web UI
  • Webmail access – Read/send email via browser

Continue with:
Part 5: Advanced Mail Filtering – Optional enhancements (ClamAV, Razor, Pyzor, Learning)
Part 6: Rspamd Web Interface & Roundcube – Web-based monitoring, management, and webmail

Key Commands Reference

# Journal monitoring (PRIMARY!)
journalctl -u rspamd -f                  # Watch Rspamd
journalctl -u postfix -u dovecot -u rspamd -f  # All mail services
journalctl -u rspamd | grep "rspamd_task_write_log" | tail -n 20

# Status and statistics
rspamc stat                              # Rspamd statistics
systemctl status rspamd valkey           # Service status
journalctl -u rspamd -n 50               # Recent Rspamd logs

# Testing
rspamc < message.eml                     # Test spam scoring
echo "test" | mail -s "Test" user@domain # Send test mail

# Configuration
rspamadm configtest                      # Test syntax
systemctl reload rspamd                  # Reload config

# Troubleshooting
ss -tlnp | grep 11332                    # Check milter port
dig +short mail._domainkey.domain.com TXT # Check DKIM DNS
dig +short TLSA _25._tcp.mx1.domain.com  # Check TLSA DNS
valkey-cli -s /run/valkey/valkey.sock ping # Test Valkey
ls -la /run/valkey/valkey.sock           # Verify socket name!

Similar Posts