Sources:
How to setup an OpenBSD 6.6 Mail Server with:
Note: This is mostly a copy/past guide. It is still recommended to read the man-pages to ‘understand’ why and how things work.
Note: You need to replace all occurrences of example.com or example.org with your domain of choice.
Ports required:
pf -> relay to spamd (tarpit)
pf -> relay to opensmtpd -> rspamd (greylist handling)
pf -> relay to opensmtpd -> rspamd (spam filtering) ->
deliver to mda (kopano-dagent)
kopano (kopano-spooler) -> opensmtpd ->
rspamd (DKIM) -> relay out
opensmtpd (AUTH) -> rspamd (DKIM) -> relay out
Basic DNS config:
cat <<EOF
IN MX 10 mx.example.com.
@ 86400 IN TXT "v=spf1 a:mx.example.com -all"
IN CAA 128 issue "letsencrypt.org"
IN CAA 128 issuewild ";"
mx IN A <your ip>
mail IN A <your ip>
mta-sts IN A <your ip>
_mta-sts IN TXT "v=STSv1; id=$(date +%Y%m%d%H%M%SZ)"
EOF
Install:
pkg_add rspamd opensmtpd-filter-rspamd redis p5-Mail-SpamAssassin
Change the controller password:
mkdir /etc/rspamd/{local.d,override.d}
echo "password = \"$(rspamadm pw)\";" > /etc/rspamd/local.d/worker-controller.inc
echo 'local_addrs = "127.0.0.0/8, ::1";' > /etc/rspamd/local.d/options.inc
echo "enabled = true;" > /etc/rspamd/local.d/mx_check.conf
cat <<EOF> /etc/rspamd/local.d/classifier-bayes.conf
servers = "127.0.0.1";
backend = "redis";
EOF
cat <<EOF> /etc/rspamd/local.d/redis.conf
servers = "127.0.0.1";
write_servers = "127.0.0.1";
EOF
cat <<EOF> /etc/rspamd/local.d/neural.conf
servers = 127.0.0.1:6379; # Redis server to store learning data and ANN
train {
max_train = 1k; # Number of trains per epoch
max_usages = 20; # Number of learn iterations while ANN data is valid
spam_score = 8; # Score to learn spam
ham_score = -2; # Score to learn ham
learning_rate = 0.01; # Rate of learning (Torch only)
max_iterations = 25; # Maximum iterations of learning (Torch only)
}
timeout = 20; # Increase redis timeout
EOF
cat <<EOF> /etc/rspamd/local.d/force_actions.conf
rules {
NOMX {
action = "reject";
expression = "MX_MISSING";
message = "Sending domain has no MX record";
}
}
EOF
cat <<EOF> /etc/rspamd/local.d/multimap.conf
spammy_tld_env_from {
type = "from";
filter = "email:domain:tld";
map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/tlds.txt";
symbol = "SPAMMY_TLD_ENVFROM";
description = "Sending TLD likely spam";
score = 1.5;
regexp = true;
}
spammy_tld_from {
type = "header";
header = "from";
filter = "email:domain:tld";
map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/tlds.txt";
symbol = "SPAMMY_TLD_FROM";
require_symbols = "!SPAMMY_TLD_ENVFROM";
description = "Sending TLD likely spam";
score = 1.5;
regexp = true;
}
MXROUTE_BODY_SPAM {
type = "content";
filter = "text";
map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/body-spam.map";
symbol = "MXROUTE_BODY_SPAM";
prefilter = true;
action = "reject";
regexp = true;
message = "The text of this email contained a string that we identified to be spam.";
}
SPAMMY_SUBJECT {
type = "header";
header = "subject";
map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/subject-spam.map";
symbol = "SPAMMY_SUBJ";
prefilter = true;
action = "reject";
regexp = true;
}
SENDER_FROM_BLACKLIST {
type = "from";
map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/blacklisted-sender-strings.map";
regexp = true;
description = "Blacklisted sender";
prefilter = true;
filter = "email:addr"
action = "reject";
message = "This sender has been blocked for matching a known spam trend";
}
MXROUTE_EXPLORING {
type = "content";
filter = "text";
map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/exploring.map";
symbol = "MXROUTE_EXPLORING";
prefilter = true;
action = "add_header";
regexp = true;
message = "We are observing this data to see how many and what kind of data it returns.";
}
EOF
cat <<EOF> /etc/rspamd/local.d/mx_check.conf
timeout = 5.0;
symbol_bad_mx = "MX_INVALID";
symbol_no_mx = "MX_MISSING";
symbol_good_mx = "MX_GOOD";
expire = 86400;
key_prefix = "rmx";
enabled = true;
greylist_invalid = false;
exclude_domains = [
"https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/mxcheck_exclude.inc",
];
EOF
cat <<EOF> /etc/rspamd/override.d/actions.conf
reject = 15;
add_header = 8;
greylist = 40;
EOF
cat <<EOF> /etc/rspamd/override.d/fuzzy_check.conf
rule "rspamd.com" {
servers = "";
}
EOF
cat <<EOF> /etc/rspamd/override.d/metrics.conf
symbol "MIME_BASE64_TEXT_BOGUS" {
weight = 3;
description = "Has text part encoded in base64 that does not contain any 8bit characters";
}
symbol "DMARC_POLICY_REJECT" {
weight = 100;
description = "Domain owner requested this be rejected by DMARC";
}
symbol "MXROUTE_BODY_SPAM" {
weight = 100;
description = "Message identified by us as likely spam";
}
symbol "ONCE_RECEIVED_STRICT" {
weight = 0;
description = "Lets kill this rule";
}
symbol "DBL_ABUSE" {
weight = 100;
description = "Email mentions domain listed at Spamhaus DBL";
}
symbol "NO_BOUNCE" {
weight = 100;
description = "No bounce loops";
}
symbol "FREEMAIL_TO" {
weight = 0.0;
description "Freemail recipients";
}
symbol "SPAMMY_SUBJ" {
weight = 100;
description = "Email subject matches spam trend";
}
symbol "IP_SCORE" {
weight = 0.0;
description = "IP reputation";
}
symbol "FROM_NEQ_DISPLAY_NAME" {
weight = 0.0;
description = "Display name contains an email address different to the From address";
}
symbol "R_SUSPICIOUS_URL" {
weight = 3.0;
description = "Suspicious URL";
}
symbol "FORGED_SENDER" {
weight = 1.5;
description = "From address has been spoofed";
}
symbol "FREEMAIL_FROM" {
weight = 3.0;
description = "From address has been spoofed";
}
symbol "R_RATELIMIT" {
weight = 9;
description = "Rate limited";
}
symbol "BROKEN_HEADERS" {
weight = 0;
description = "Header check";
}
symbol "FORGED_RECIPIENTS" {
weight = 0;
description = "Recipient check";
}
symbol "HAS_PHPMAILER_SIG" {
weight = 2;
description = "PHP Mailer header"
}
symbol "SPAM_FLAG" {
weight = 7;
description = "Identified as spam by previous server"
}
symbol "MIME_BAD_ATTACHMENT" {
weight = 0;
description = "Bad attachment"
}
symbol "BLACKLIST_DMARC" {
weight = 0;
description = "Mail comes from the whitelisted domain and has valid failed DMARC and DKIM policies";
}
symbol "MX_INVALID" {
weight = 0;
description = "From domain has no valid MX records or this rule sucks";
}
symbol "DBL_SPAM" {
weight = 15;
description = "Contains reference domain listed at SpamHaus";
}
symbol "SPOOF_REPLYTO" {
weight = 0;
description = "Spoofing reply-to has valid use cases so setting score to 0";
}
EOF
Generate keys:
Note: add the outputs of rspamadm dkim_keygen to your DNS config.
mkdir -p /var/lib/rspamd/dkim
rspamadm dkim_keygen -s 'dkim' -b 2048 -d example.com -k /var/lib/rspamd/dkim/example.com.dkim.key
rspamadm dkim_keygen -s 'eddsa' -d example.com -t ed25519 -k /var/lib/rspamd/dkim/example.com.eddsa.key
chmod 640 /var/lib/rspamd/dkim/*
chgrp _rspamd /var/lib/rspamd/dkim/*
Config:
cat <<EOF> /etc/rspamd/local.d/dkim_signing.conf
# allow_username_mismatch = true;
domain {
example.com {
selectors [
{
path = "/var/lib/rspamd/dkim/example.com.dkim.key";
selector = "dkim";
},
{
path = "/var/lib/rspamd/dkim/example.com.eddsa.key";
selector = "eddsa";
}
]
}
example.org {
path = "/var/lib/rspamd/dkim/example.org.eddsa.key";
selector = "eddsa";
}
}
EOF
cat <<'EOF'> /etc/rspamd/local.d/worker-normal.inc
dkim_signing {
.include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/dkim_signing.conf"
}
EOF
Rspamd version 1.9.4 causes zombie processes on shutdown.
Stop workaround:
sed -i '/rc_cmd/d' /etc/rc.d/rspamd
cat <<'EOF'>> /etc/rc.d/rspamd
rc_stop() {
pkill -T "${daemon_rtable}" -xf "${pexp}"
i=0
while [[ $i -lt 5 ]]; do sleep 1; rc_check || return; i=$((i+1)); done
pkill -9 -T "${daemon_rtable}" -xf "${pexp}"
}
rc_cmd $1
EOF
rcctl enable redis rspamd
rcctl start redis rspamd
Basic config at OpenBSD httpd
Kopano config:
prefork 4
mx="mx.example.com"
mail="mail.example.com"
mtasts="mta-sts.example.com"
server $mx {
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location "/*" {
block return 301 "https://$mail$DOCUMENT_URI"
}
}
server $mail {
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location "/*" {
block return 301 "https://$SERVER_NAME$DOCUMENT_URI"
}
}
server $mail {
listen on * tls port 443
tls {
certificate "/etc/ssl/mx.example.com.fullchain.pem"
key "/etc/ssl/private/mx.example.com.key"
ciphers "AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128"
ecdhe "X25519:secp384r1:secp521r1"
}
connection {
max request body 20971520
}
hsts { max-age 31536000, subdomains }
root "/kopano-webapp"
directory index "index.php"
location "/robots.txt" {
root "/robots"
}
location "*.php" {
fastcgi socket "/run/php-fpm.sock"
}
location "/Microsoft-Server-ActiveSync" {
root "/z-push/index.php"
fastcgi socket "/run/php-fpm.sock"
}
}
server $mtasts {
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location "/*" {
block return 301 "https://$SERVER_NAME$DOCUMENT_URI"
}
}
server $mtasts {
listen on * tls port 443
tls {
certificate "/etc/ssl/mx.example.com.fullchain.pem"
key "/etc/ssl/private/mx.example.com.key"
ciphers "AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128"
ecdhe "X25519:secp384r1:secp521r1"
}
hsts { max-age 31536000, subdomains }
root "/htdocs"
}
Add mta-sts file:
mkdir /var/www/htdocs/.well-known
cat <<EOF> /var/www/htdocs/.well-known/mta-sts.txt
version: STSv1
mode: enforce
mx: mx.example.com
max_age: 604800
EOF
Domains:
See: Lets Encrypt
Config:
domain mx.example.com {
alternative names { mail.example.com, mta-sts.example.com }
domain key "/etc/ssl/private/mx.example.com.key"
domain certificate "/etc/ssl/mx.example.com.pem"
domain full chain certificate "/etc/ssl/mx.example.com.fullchain.pem"
sign with letsencrypt
}
cat <<'EOF' >/usr/local/sbin/cron-acme-update
#!/bin/sh
/usr/sbin/acme-client example.com
if [ $? -eq 0 ]; then
rcctl restart smtpd spamd httpd kopano_gateway kopano_ical
fi
EOF
Install: https://git.giftfish.de/ston1th/smtp_auth_sync
Boot startup:
rcctl enable smtpd
rcctl set smtpd flags -v
This file holds all virtual domains:
cat /etc/mail/vdomains
example.com
example.org
Create the virtual system user:
useradd -d /var/empty -s /sbin/nologin vmail
This file holds the virtual user mappings:
# cat /etc/mail/vusers
john@example.com vmail
@example.com john@example.com
doe@example.org vmail
hostmaster@example.org doe@example.org
Create secrets file:
touch /etc/mail/secrets
chmod 600 /etc/mail/secrets
Replace example.com with your domain in /etc/mail/smtpd.conf.
smtp max-message-size 20M
pki mx.giftfish.de cert "/etc/ssl/mail.example.com.fullchain.pem"
pki mx.giftfish.de key "/etc/ssl/private/mail.example.com.key"
smtp ciphers AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128
filter check_dyndns phase connect match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } \
disconnect "550 no residential connections"
filter check_rdns phase connect match !rdns \
disconnect "550 no rDNS entry"
filter check_fcrdns phase connect match !fcrdns \
disconnect "550 no FCrDNS entry"
#filter senderscore \
# proc-exec "filter-senderscore -blockBelow 10 -junkBelow 70 -slowFactor 5000"
filter rspamd proc-exec "filter-rspamd"
filter header proc-exec "filter-header"
table aliases "/etc/mail/aliases"
table vdomains "/etc/mail/vdomains"
table vusers "/etc/mail/vusers"
table secrets "/etc/mail/secrets"
listen on lo0 mask-src filter { header, rspamd }
listen on egress tls pki mail.example.com auth-optional \
filter { check_dyndns, check_rdns, check_fcrdns, rspamd }
listen on egress smtps pki mail.example.com auth-optional \
filter { check_dyndns, check_rdns, check_fcrdns, rspamd }
listen on egress port submission tls pki mail.example.com \
auth <secrets> mask-src filter { header, rspamd }
action "local" maildir junk alias <aliases>
action "outbound" relay helo mx.giftfish.de
action "kopano" mda "/usr/local/sbin/kopano-dagent -s %{dest:lowercase|strip}" virtual <vusers> user "_kopano"
match for local action "local"
match from any for domain <vdomains> action "kopano"
match from any auth action "outbound"
match from local for any action "outbound"
match !for domain <vdomains> reject
Headers filtered:
Based on: https://github.com/jirutka/opensmtpd-filter-rewrite-from
cat <<'EOF'> /usr/local/libexec/smtpd/filter-header
#!/usr/bin/awk -f
function die(msg) {
printf("mail-from: %s\n", msg) > "/dev/stderr"
exit 1
}
function output(type, v, t, s, msg) {
if (v < "0.5") {
print(type, t, s, msg)
} else {
print(type, s, t, msg)
}
fflush()
}
BEGIN {
ARGC = 0
FS = "|"
OFS = FS
_ = FS
in_body[""] = 0
}
"config|ready" == $0 {
print("register|filter|smtp-in|data-line")
print("register|ready")
fflush()
next
}
"config" == $1 {
next
}
"filter" == $1 {
if (NF < 7) {
die("invalid filter command: expected >6 fields!")
}
ver = $2
sid = $6
token = $7
line = substr($0, length($1$2$3$4$5$6$7) + 8)
}
"filter|smtp-in|data-line" == $1_$4_$5 {
if (line == "") {
in_body[sid] = 1
}
if (!in_body[sid] && match(line, /^User-Agent:[\t ]*|^X-Mailer:[\t ]*/)) {
next
}
if (line == ".") {
delete in_body[sid]
}
output("filter-dataline", ver, token, sid, line)
}
EOF
chmod 755 /usr/local/libexec/smtpd/filter-header
Restart smtpd: rcctl restart smtpd
Create custom spam files:
touch /etc/mail/custom.black
Config:
cat <<EOF> /etc/mail/spamd.conf
all:\
:nixspam:custom_black:
nixspam:\\
:black:\\
:msg="Your address %A is in the nixspam list\n\\
See http://www.heise.de/ix/nixspam/dnsbl_en/ for details":\\
:method=http:\\
:file=www.openbsd.org/spamd/nixspam.gz
custom_black:\\
:black:\\
:msg="Your address %A has been manually blacklisted":\\
:method=file:\\
:file=/etc/mail/custom.black:
EOF
Boot startup:
Replace example.com with your domain.
rcctl enable spamd
rcctl set spamd flags "-v -G 4:4:864 -K /etc/ssl/private/example.com.key -C /etc/ssl/example.com.fullchain.pem"
rcctl enable spamlogd
Create spamd-setup cronjob: echo "*/5 * * * * /usr/libexec/spamd-setup -b >/dev/null 2>&1" >> /var/cron/tabs/root
cat <<EOF>> /etc/pf.conf
set limit table-entries 400000
set skip on lo
block all
smtp_ports="{ smtp, smtps }"
in_tcp = "{ ssh, http, https, imaps, 8443 }"
pass in on egress proto tcp to any port $in_tcp
bgp_addrs = "{ 64.142.121.62, 217.31.80.170, 2a00:15a8:0:100:0:d91f:50aa:1 }"
pass in on egress proto tcp from $bgp_addrs to any port bgp
pass in on egress proto icmp
table <spamd> persist
table <bgp_spamd> persist
table <bruteforce> persist
match in all scrub (no-df random-id max-mss 1440)
block in quick log from urpf-failed label uRPF
block quick log from <bruteforce>
block quick proto {tcp,udp} user www
pass in quick log on egress proto tcp from any to any port 587
pass in quick log on egress proto tcp \
from { <bgp_spamd>, <spamd> } \
to egress port $smtp_ports \
rdr-to 127.0.0.1 port spamd
pass in quick log on egress proto tcp \
from any to any port $smtp_ports \
flags S/SA modulate state
out_tcp = "{ ssh, domain, smtp, smtps, http, https }"
pass out log on egress proto tcp to any port $out_tcp
pass out log on egress proto tcp to $bgp_addrs port bgp
out_udp = "{ domain, ntp }"
pass out log on egress proto udp to any port $out_udp
pass out log on egress proto { icmp, icmp6 } to any
EOF
pfctl -f /etc/pf.conf
Start spamd and spamlogd:
rcctl start spamd
rcctl start spamlogd
Basic config at OpenBSD MariaDB
Create database:
mysql -u root -p
CREATE DATABASE kopano CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL ON kopano.* TO 'kopano'@'localhost' IDENTIFIED BY '<password>';
FLUSH PRIVILEGES;
Sets required:
Install: pkg_add kopano-core kopano-mapi py3-bsddb3 py3-magic
Boot startup: rcctl enable kopano
Make sure mysqld starts before kopano: rcctl order mysqld
Change services to start:
Note: The services kopano_search and kopano_spamd are broken and do not work.
perl -p -i -e 's/^(_pkg_scripts=.*)/#$1\n_pkg_scripts="kopano_server kopano_spooler kopano_monitor kopano_gateway kopano_ical"/' /etc/rc.d/kopano
Allow socket in chroot:
mkdir /var/www/run/kopano
chown _kopano: /var/www/run/kopano
/etc/kopano/dagent.cfg
spam_header_name = X-Spam
/etc/kopano/server.cfg
# Replace
server_listen = *:236
# with
server_listen =
# Add:
server_pipe_name = /var/run/kopano/server.sock /var/www/run/kopano/server.sock
# Set DB Credentials:
mysql_user = kopano
mysql_password = <password>
mysql_socket = /var/run/mysql/mysql.sock
disabled_features = pop3
log_file = /var/log/kopano/server.log
/etc/kopano/gateway.cfg
pop3_listen =
pop3s_listen =
imap_listen =
imaps_listen = *:993
server_socket = file:///var/run/kopano/server.sock
disable_plaintext_auth = yes
ssl_private_key_file = /etc/ssl/private/example.com.key
ssl_certificate_file = /etc/ssl/example.com.fullchain.pem
tls_min_proto = tls1.2
ssl_ciphers = AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128
ssl_prefer_server_ciphers = yes
log_file = /var/log/kopano/gateway.log
/etc/kopano/ical.cfg
ical_listen =
icals_listen = *:8443
server_socket = file:///var/run/kopano/server.sock
ssl_private_key_file = /etc/ssl/private/example.com.key
ssl_certificate_file = /etc/ssl/example.com.fullchain.pem
tls_min_proto = tls1.2
ssl_ciphers = AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128
ssl_prefer_server_ciphers = yes
log_file = /var/log/kopano/ical.log
/etc/kopano/spooler.cfg
log_file = /var/log/kopano/spooler.log
/etc/kopano/dagent.cfg
log_file = /var/log/kopano/dagent.log
Start kopano: rcctl start kopano
Create a user: kopano-admin -c "john@example.com" -f "John Doe" -e "john@example.com" -P
Credentials:
https://mx.example.com:8443/caldav/john@example.com/kopano-dbadm k-1216
kopano-dbadm usmp
Install: pkg_add kopano-webapp
Boot startup: rcctl enable php74_fpm
echo "extension=mapi.so" > /etc/php-7.4/kopano.ini
sed -i -e 's/^pm.max_children.*/pm.max_children = 20/' \
-e 's/^pm.start_servers.*/pm.start_servers = 4/' \
-e 's/^pm.min_spare_servers.*/pm.min_spare_servers = 2/' \
-e 's/^pm.max_spare_servers.*/pm.max_spare_servers = 6/' /etc/php-fpm.conf
sed -i 's/^expose_php.*/expose_php = off/' /etc/php-7.4.ini
Change Server Socket:
sed -i 's|^.define("DEFAULT_SERVER".*|define("DEFAULT_SERVER", "file:///run/kopano/server.sock");|' /var/www/kopano-webapp/config.php
Remove unused plugins:
rm -rf /var/www/kopano-webapp/plugins/{filesbackendSMB,filesbackendOwncloud}
Start php-fpm: rcctl start php74_fpm
Install: pkg_add z-push
Change Server Socket:
sed -i "s|^define('MAPI_SERVER'.*|define('MAPI_SERVER', 'file:///run/kopano/server.sock');|" /var/www/z-push/backend/kopano/config.php
The password plugin cannot be used anymore.
The required functions have been removes from kopano-core in: https://stash.kopano.io/projects/KC/repos/kopanocore/commits/9a003acd34857cf2c27d0d9c581af0e233b48510#php7-ext/main.cpp
Project: bgp-spamd
Boot startup: rcctl enable bgpd
OpenBGPD config:
cat <<'EOF'> /etc/bgpd.conf
spamdAS="65066"
AS 65001
fib-update no # Mandatory, to not update the local routing table
nexthop qualify via default # Make sure the nexthop is valid
group "spamd-bgp" {
remote-as $spamdAS
multihop 64
export none
# us.bgp-spamd.net
neighbor 64.142.121.62
# eu.bgp-spamd.net
neighbor 217.31.80.170
# IPv6 eu.bgp-spamd.net
#neighbor 2a00:15a8:0:100:0:d91f:50aa:1
}
deny to any
deny from any
allow from group "spamd-bgp"
match from group "spamd-bgp" community $spamdAS:666 set pftable "bgp_spamd"
EOF
chmod 600 /etc/bgpd.conf
Start OpenBGPD: rcctl start bgpd
Add these options to your /etc/pf.conf:
set limit table-entries 400000
table <bgp_spamd_bypass> persist
table <bgp_spamd> persist
bgp_addrs = "{ 64.142.121.62, 217.31.80.170, 2a00:15a8:0:100:0:d91f:50aa:1 }"
pass in on egress proto tcp from $bgp_addrs to any port bgp
Change your pf smtp rules to match these:
pass in quick log on egress proto tcp \
from { <bgp_spamd_bypass>, <nospamd>, <spamd-white> } \
to any port $smtp_ports \
flags S/SA modulate state
pass in quick proto tcp from { <bgp_spamd>, <spamd> } \
to egress port $smtp_ports \
rdr-to 127.0.0.1 port spamd
Reload pf: pfctl -f /etc/pf.conf
Edit the /etc/mail/spamd.conf:
# Add bgp-spamd to the all list:
all:\
:nixspam:custom_black:bgp-spamd:
# Add the bgp-spamd list:
bgp-spamd:\
:black:\
:msg="Your address %A has sent mail to a spamtrap\n\
within the last 24 hours":\
:method=file:\
:file=/etc/mail/spamd.black:
Create the spamd-reload script:
cat <<'EOF'> /usr/local/sbin/spamd-update
#!/bin/sh
black="/etc/mail/spamd.black"
custom="/etc/mail/custom.black"
bgpctl show rib community 65066:666|tail -n+5|
awk '{sub(/\/.*/, "", $1);print $1}'|uniq >${black}
for ip in $(cat ${custom}); do
/usr/sbin/spamdb -d ${ip} 2> /dev/null
done
/usr/libexec/spamd-setup -b
EOF
chmod 0744 /usr/local/sbin/spamd-update
Replace the spamd-setup cronjob from above with the new script and run it once manually: /usr/local/sbin/spamd-update
#!/bin/sh
db="kopano"
db_user="kopano"
db_pw="<password>"
set -A paths \
/etc/kopano \
/etc/mail \
/etc/rspamd \
/etc/pf.conf \
/etc/httpd.conf \
/etc/acme \
/etc/ssl \
/etc/acme-client.conf \
/etc/bgpd.conf \
/etc/rc.conf.local \
/etc/my.cnf \
/etc/rc.d/kopano \
/etc/php-7.3/kopano.ini \
/etc/php-7.3.ini \
/var/rspamd \
/var/lib/rspamd \
/var/db/spamd \
/var/db/kopano \
/var/www/htdocs/robots.txt \
/var/www/htdocs/.well-known \
/var/www/kopano-webapp/config.php \
/var/www/kopano-webapp/server/includes/core/class.webappauthentication.php \
/var/www/z-push/backend/kopano/config.php \
/usr/local/sbin/spamd-update \
/usr/local/sbin/cron-acme-update \
/var/cron/tabs/root \
/var/redis/dump.rdb \
/tmp/*.pf \
/tmp/${db}.sql
for t in $(pfctl -s Tables); do
pfctl -t ${t} -T show > /tmp/${t}.pf
done
mysqldump -u ${db_user} -p${db_pw} ${db} >/tmp/${db}.sql
redis-cli <<EOF
SAVE
EOF
tar cfvz /root/mailserver_backup_$(date +%Y%m%d_%H%M%S).tgz ${paths[@]}
rm /tmp/${db}.sql /tmp/*.pf
Restore backup:
pkg_add rspamd opensmtpd-filter-rspamd kopano-core kopano-mapi py3-bsddb3 py3-magic kopano-webapp z-push
tar xfpvz mailserver_backup<date>.tgz -C /
mkdir /var/www/run/kopano
chown _kopano: /var/www/run/kopano
useradd -d /var/empty -s /sbin/nologin vmail
Restore pf tables:
for t in $(ls /tmp/*.pf); do pfctl -t ${t%.pf} -T add -f ${t}; done
Restore database:
mysql -u root -p kopano < /tmp/kopano.sql
cat <<'EOF'>/usr/local/sbin/watchdog
#!/bin/sh
for p in $(rcctl ls failed); do
rcctl start ${p}
done
chmod 755 /usr/local/sbin/watchdog
echo "*/5 * * * * /usr/local/sbin/watchdog >/dev/null 2>&1" >> /var/cron/tabs/root
NOTE: replace example.com with your domain.
For the manual spam and ham reporting to work properly, you have to forward the desired mails as attachment to spam@example.com (for spam training - bad mails) and ham@example.com (for ham training - good mails).
Attachment parser script:
cat <<"EOF"> /usr/local/bin/attachment_report.pl
#!/usr/bin/perl
use strict;
use warnings;
use Config;
use Email::MIME::Attachment::Stripper;
my $rspamc = "/usr/local/bin/rspamc";
my $learn;
if ($ARGV[0] eq "spam") {
$learn = "${rspamc} learn_spam";
} elsif ($ARGV[0] eq "ham") {
$learn = "${rspamc} learn_ham";
} else {
die "error: argument must be 'spam' or 'ham'\n";
}
if ($Config{osname} eq "openbsd") {
eval "use OpenBSD::Pledge";
if ($Config{osvers} >= "6.6") {
eval "use OpenBSD::Unveil";
unveil($rspamc, "rx") or die "Unable to unveil: $!";
}
pledge("proc exec prot_exec") or die "Unable to pledge: $!";
}
local $/;
my $mail = <STDIN>;
my $s = Email::MIME::Attachment::Stripper->new($mail);
my @a = $s->attachments;
for my $i (1 .. $#a) {
open(IN, "| $learn") or die "$learn failed: $!\n";
print IN $a[$i]{payload};
close(IN);
}
EOF
chmod 755 /usr/local/bin/attachment_report.pl
Create spam virtual users:
cat <<EOF> /etc/mail/spam
spam@example.com vmail
ham@example.com vmail
EOF
Add the following lines to your smtpd.conf:
table spam "/etc/mail/spam"
action "ham" mda "/usr/local/bin/attachment_report.pl ham" virtual <spam> user "_rspamd"
action "spam" mda "/usr/local/bin/attachment_report.pl spam" virtual <spam> user "_rspamd"
match from local for domain <vdomains> rcpt-to "ham@example.com" action "ham"
match from local for domain <vdomains> rcpt-to "spam@example.com" action "spam"