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:
- Mail arrives at Postfix (port 25)
- Postfix forwards to Rspamd via milter protocol
- Rspamd analyzes message, queries Valkey for statistics
- Rspamd adds spam headers to message
- Mail returns to Postfix with headers
- Postfix delivers to Dovecot LMTP
- Sieve filter checks spam headers
- Clean mail → INBOX, spam → Junk
Outgoing mail (authenticated users):
- User sends mail via port 587 or 465
- Postfix forwards to Rspamd
- Rspamd adds DKIM signature
- 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)
nprocRecommended 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.fullchainAdd 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 --versionExpected: 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.confConfigure Unix Socket and Memory
Edit /etc/valkey/valkey.conf:
vi /etc/valkey/valkey.confFind 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 noticeKey settings explained:
port 0: Disables TCP listening (security – prevents network access)unixsocket /run/valkey/valkey.sock: Creates socketunixsocketperm 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 restartssave 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 valkeyExpected 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.sockTest connection:
# Test with valkey-cli
valkey-cli -s /run/valkey/valkey.sock ping
# Should respond: PONGIf 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 valkeyWhy 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 updateInstall 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 newerVerify 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";
EOFConfiguration explained:
servers: Default connection for all Rspamd moduleswrite_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;
EOFWhy console logging?
- Consistent with Debian 13: Postfix, Dovecot, CrowdSec all use journald
- Unified monitoring: Single
journalctlcommand 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 → journaldlevel = "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 IPv6If 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
EOFThreshold 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.sockExpected: 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;
}
EOFConfiguration explained:
bind_socket: Listen on localhost port 11332 (milter port)milter = yes: Enable milter protocoltimeout: 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";
}
EOFHeaders explained:
- X-Spam-Status: Simple yes/no spam indicator
No, score=0.0– Clean mailYes, 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
- Shows exact score:
- Authentication-Results: Email authentication status
- SPF result:
pass,fail,none - DKIM result:
pass,fail,none - DMARC result:
pass,fail,none
- SPF result:
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 milterSettings 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 postfixTest 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.80Perfect! 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 -fSearch 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 todaySearch 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 20Real-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 greyExample 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: 30What 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 greylistingDKIM 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.sockScore Interpretation
Spam scores in logs:
- Negative scores (< 0): Trusted senders with good authentication
- Example:
[-5.00/15.00]= reply to known conversation + perfect auth
- Example:
- 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-usageVacuum old logs (if journal too large):
# Keep only last 7 days
journalctl --vacuum-time=7d
# Limit to 500MB
journalctl --vacuum-size=500MJournal configuration (/etc/systemd/journald.conf):
[Journal]
SystemMaxUse=500M # Max disk space
MaxRetentionSec=7day # Max retention timeCommon Log Patterns
Successful mail scan:
journalctl -u rspamd | grep "rspamd_task_write_log" | tail -n 1
# Shows: (no action): [score/15.00] with rulesGreylisting:
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.sockErrors:
journalctl -u rspamd -p err
# Shows only error-level messagesRemember: Journal is Primary
All monitoring uses journalctl:
journalctl -u rspamdfor Rspamd logsjournalctl -u postfixfor Postfix logsjournalctl -u dovecotfor 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
- You generate a key pair: private key (server) + public key (DNS)
- Private key signs outgoing emails with a digital signature
- Public key published in DNS allows recipients to verify the signature
- If signature matches: Email is authentic ✅
- 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:
.keyfile: Private key (stays on server, signs mail) – KEEP SECRET!.txtfile: 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}.txtExample 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}/*.txtConfigure 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;
EOFConfiguration explained:
path: Rspamd automatically substitutes$domainand$selectorwith actual valuesselector = "mail": Matches what we used in key generationsign_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:
- Extracts domain from “From:” header (e.g., user@yourdomain.com)
- Looks for key file:
/var/lib/rspamd/dkim/yourdomain.com.mail.key - Signs the email with this key
- 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.luaIf 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 stringIn 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...IDAQABImportant:
- 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.comCheck email source in Gmail (or any recipient):
- Open the email
- Click “⋮” (three dots) → “Show original”
- 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 ~allExplanation:
v=spf1: SPF version 1mx: 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 ~allProduction 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 similarDMARC (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=rExplanation:
v=DMARC1: DMARC version 1p=none: Policy – monitor only (don’t reject, just report)rua=mailto:...: Where to send aggregate reportspct=100: Apply policy to 100% of mailadkim=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=rp=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=sp=reject: Reject mail that fails authenticationadkim=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 similarComplete DNS Configuration Summary
For each domain you send mail from, you need these DNS records:
| Record Type | Name/Host | Value | Purpose |
|---|---|---|---|
| TXT | @ | v=spf1 mx ~all | SPF – authorize mail servers |
| TXT | mail._domainkey | v=DKIM1; k=rsa; p=... | DKIM public key |
| TXT | _dmarc | v=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.comIn 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-managesievedPackages:
dovecot-sieve: Sieve mail filtering plugindovecot-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.confFind 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
EOFConfiguration explained:
sieve_script personal: Users’ personal Sieve scriptssieve_script before: Global script that runs before personal scriptsspam-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 reloadExpected: Dovecot reloads successfully without errors.
Automatic Spam Filtering
Create the global Sieve script that moves spam to Junk folders.
How It Works
- Mail arrives with Rspamd headers (X-Spam: Yes or No)
- Dovecot LMTP receives mail
- Sieve filter checks
X-Spamheader - If “Yes” → Move to Junk folder
- 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;
}
EOFScript explanation:
require ["fileinto", "mailbox"]: Load required Sieve extensionsif 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 filesIf 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):
- Unknown sender emails you
- Rspamd checks: not whitelisted, score ≥4.0
- Postfix responds: “450 4.7.1 Greylisted, try again later”
- Legitimate mail server: Queues message, retries in 5-10 minutes ✅
- Spam bot: Typically doesn’t retry, mail never delivered ❌
Second Contact (5+ minutes later):
- Legitimate server retries
- Rspamd recognizes sender (stored in Valkey)
- 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
EOFThis 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) AAAAEdit the config file:
nano /etc/rspamd/local.d/greylist.confUncomment the example section and replace with your actual IPs:
- Find the commented example section (starts with
#whitelist_ip = [) - Uncomment all lines (remove the
#) - Replace example IPs with YOUR actual IPs
- 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 AAAAWhy 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 greyMonitor 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 laterNote: 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=sentGreylisting 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
timeoutto 600 (10 minutes) if too lenient - Decrease
expireto 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
- You publish TLSA records in DNS with certificate fingerprints
- Recipients look up TLSA records via DNSSEC
- Recipients compare certificate fingerprint to TLSA record
- Match = authentic certificate ✅
- 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-slingerGenerate 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:
| Type | Name | Value |
|---|---|---|
| TLSA | _25._tcp.mx1 | 3 0 1 <hash> |
| TLSA | _25._tcp.mx1 | 3 1 1 <hash> |
| TLSA | _587._tcp.mx1 | 3 0 1 <hash> |
| TLSA | _587._tcp.mx1 | 3 1 1 <hash> |
| TLSA | _465._tcp.mx1 | 3 0 1 <hash> |
| TLSA | _465._tcp.mx1 | 3 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 1and 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 ""
doneExpected: 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:
- Certificate renews (automatic via acme.sh)
- Generate new TLSA records (run the tlsa commands again)
- Update selector 0 TLSA records in DNS
- 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 crowdsecIf 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 mailTest 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.comIn Gmail:
- Open the email
- Click ⋮ (three dots) → “Show original”
- 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 dkimTest 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 beforeWatch 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 deliveredExpected:
- 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 greyTest 6: Online Testing Services
Use these services to verify your setup:
Mail Tester (https://www.mail-tester.com):
- Get temporary test address from site
- Send email:
echo "Test" | mail -s "Mail Test" test-xxxxx@mail-tester.com - Check your score
- Target: 10/10 score
Internet.nl Mail Test (https://internet.nl/mail/):
- Enter your domain:
yourdomain.com - Run the test
- Target: Hall of Fame status (100%)
- 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: YesCheck 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 deliveryRspamd-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 20Postfix + 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_logentries - Spam scores:
score=X.X/15.0 - Actions:
no action,greylist,add header,reject - DKIM signing:
DKIM_SIGNED(0.00){yourdomain.com} - Greylisting:
greylistedorskip 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 statisticsView 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 messageCheck 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 rulesTest specific rules:
# Test DKIM signing
echo "test" | rspamc -h from:test@${DOMAIN}
# Test spam detection
cat spam-sample.eml | rspamcTraining 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_hamConfiguration Commands
Test configuration:
# Check syntax
rspamadm configtest
# View active configuration
rspamadm configdump
# View specific module config
rspamadm configdump | grep -A10 dkim_signingReload configuration:
# Reload without restart (graceful)
systemctl reload rspamd
# Full restart (if needed)
systemctl restart rspamdTroubleshooting Commands
Check service status:
# Service status
systemctl status rspamd
# Recent service messages
journalctl -u rspamd -n 50
# Follow service messages
journalctl -u rspamd -fCheck 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.sockCheck 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: PONGUseful 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 errorsNote: 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 valkeyCheck 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 processedCheck 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:11332Check 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 valkeyCommon 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.confFix 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.sockFix 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.sockDKIM 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 recordVerify key permissions:
ls -la /var/lib/rspamd/dkim/
# Should be owned by _rspamd:_rspamd
# Keys should be chmod 640Check journal:
journalctl -u rspamd | grep -i dkim | tail -n 20
# Look for signing errors or missing keysCommon 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 IPsAdd 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 rspamdTemporarily disable for testing:
# Edit config
vi /etc/rspamd/local.d/greylist.conf
# Set:
enabled = false;
# Restart
systemctl restart rspamdSpam 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 = yesCheck 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 errorsCheck 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.00MIf 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 valkeyBenefits 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 serversCheck Rspamd memory:
ps aux | grep rspamd
# Check RSS column for memory usageTypical 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!