Created by: ston1th 2020-01-18 13:22:07
Updated by: ston1th 2020-01-18 13:22:44
Visibility: Public

OpenBSD Mail Server

DEPRECATED This is an old version of the OpenBSD mail server.

TODO

https://poolp.org/posts/2019-09-14/setting-up-a-mail-server-with-opensmtpd-dovecot-and-rspamd/

Intro

How to setup an OpenBSD 6.4 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.

Mail-Flow

Incoming mail (blacklisted)

pf -> relay to spamd (tarpit)

Incoming mail (greylisted)

pf -> relay to spamd (greylist handling)

Incoming mail (whitelisted)

pf -> relay to opensmtpd on lo0 ->
relay untagged mail to spampd (spamassassin) on port 10025 ->
return to opensmtpd on lo0 port 10026 and tag it SPAM_IN ->
deliver to mda (kopano-dagent)

Outgoing mail

kopano (kopano-spooler) -> opensmtpd on lo0 -> relay untagged mail to dkimproxy on port 10027 ->
return to opensmtpd on lo0 port 10028 and tag it DKIM_OUT ->
relay out -> pf

SpamAssassin

Install: pkg_add p5-Mail-SpamAssassin spampd

Boot startup:

rcctl enable spampd
rcctl enable spamassassin
rcctl set spampd flags "--port=10025 --relayhost=127.0.0.1:10026 --tagall --aw"

Config

Change Rewrite: sed -i 's/^# \(rewrite_header.*\)/\1/' /etc/mail/spamassassin/local.cf

Permissions: chown -R _spampd /var/spool/spamassassin

Start the daemons:

rcctl start spampd
rcctl start spamassassin

Create the daily update cronjob:

echo "0 3 * * * /usr/local/bin/sa-update >/dev/null 2>&1" >> /var/cron/tabs/root

DKIM Proxy

Install: pkg_add dkimproxy opendkim

Boot startup: rcctl enable dkimproxy_out

Config

Note: replace example.com with your domain.

Edit config:

sed -i 's/^domain.*/domain    example.com/' /etc/dkimproxy_out.conf
sed -i 's|^keyfile.*|keyfile   /var/dkimproxy/default.private|' /etc/dkimproxy_out.conf
sed -i 's/^selector.*/selector  default/' /etc/dkimproxy_out.conf
sed -i 's/^signature dkim.*/signature dkim(c=relaxed,a=rsa-sha256)/' /etc/dkimproxy_out.conf

Generate signing-key:

mkdir /var/dkimproxy
opendkim-genkey -d example.com -D /var/dkimproxy -b 4096
chgrp _dkimproxy /var/dkimproxy/*
chmod 640 /var/dkimproxy/*

Put the contents of /var/dkimproxy/default.txt in your DNS-Zone file.

Start dkimproxy: rcctl start dkimproxy_out

smtpd

Boot startup:

rcctl enable smtpd
rcctl set smtpd flags -v

Virtual Domains

This file holds all virtual domains:

cat /etc/mail/vdomains
example.com
example.org

Virtual Users

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

smtpd.conf

Replace example.com with your domain in /etc/mail/smtpd.conf.

smtp max-message-size 20MB
pki mail.example.com cert "/etc/ssl/example.com.fullchain.pem"
pki mail.example.com key "/etc/ssl/private/example.com.key"
smtp ciphers ECDHE-RSA-CHACHA20-POLY1305:CHACHA20:ECDHE+AESGCM:ECDHE+AES

listen on lo0
listen on lo0 port 10026 tag SPAM_IN
listen on lo0 port 10028 tag DKIM_OUT
listen on egress tls pki mail.example.com auth-optional
listen on egress smtps pki mail.example.com auth-optional

table aliases file:/etc/mail/aliases
table vdomains file:/etc/mail/vdomains
table vusers file:/etc/mail/vusers

action "local" mbox alias <aliases>
action "relay" relay
action "relay_spampd" relay host smtp://127.0.0.1:10025
action "relay_dkim" relay host smtp://127.0.0.1:10027
action "kopano" mda "/usr/local/sbin/kopano-dagent -s %{dest}" virtual <vusers> user "_kopano"

# tags
match tag SPAM_IN for domain <vdomains> action "kopano"
match tag DKIM_OUT for any action "relay"

# local
match for local action "local"

match from any for domain <vdomains> action "relay_spampd"
match for any action "relay_dkim"
match !for domain <vdomains> reject

Restart smtpd: rcctl restart smtpd

spamd

Create custom spam files:

touch /etc/mail/{nospamd,spamd.black,custom.black}

Config:

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:

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

pf Rules

cat <<EOF>> /etc/pf.conf
set limit table-entries 400000

smtp_ports = "{ smtp, 465 }"
table <spamd> persist
table <nospamd> persist file "/etc/mail/nospamd"
table <spamd-white> persist

pass in quick log on egress proto tcp \
  from { <nospamd>, <spamd-white> } \
  to any port $smtp_ports \
  flags S/SA modulate state

pass in quick proto tcp from <spamd> \
  to egress port $smtp_ports \
  rdr-to 127.0.0.1 port spamd

pass in quick proto tcp from any \
  to egress port $smtp_ports \
  rdr-to 127.0.0.1 port spamd

pass out log on egress proto tcp to any port $smtp_ports
EOF
pfctl -f /etc/pf.conf

Start spamd and spamlogd:

rcctl start spamd
rcctl start spamlogd

MariaDB

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;

Kopano

Install: pkg_add kopano-core kopano-mapi

Boot startup: rcctl enable kopano

Make sure mysqld starts before kopano: rcctl order mysqld

Config

Change services to start:

perl -p -i -e 's/^(_pkg_scripts=.*)/#$1\n_pkg_scripts="kopano_server kopano_spooler kopano_monitor kopano_gateway"/' /etc/rc.d/kopano

Link kopano socket to chroot:

sed '/rc_cmd/d' /etc/rc.d/kopano_server
cat <<'EOF'>> /etc/rc.d/kopano_server
sock="/var/run/kopano/server.sock"
www_sock="/var/www/run/server.sock"

rc_start() {
        ${rcexec} "${daemon} ${daemon_flags} ${_bg}"
        exc=$?
        sleep 2
        ln $sock $www_sock
        return $exc
}

rc_post() {
        rm -f $www_sock
}

rc_cmd $1
EOF
/etc/kopano/server.cfg

Set DB Credentials:

mysql_user = kopano
mysql_password = <password>
mysql_socket = /var/run/mysql/mysql.sock
/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
log_file = /var/log/kopano/gateway.log
/etc/kopano/search.cfg

server_bind_name = file:///var/run/kopano/search.sock

Start kopano: rcctl start kopano

Create a user: kopano-admin -c "john@example.com" -f "John Doe" -e "john@example.com" -P

Kopano Webapp

Install: pkg_add kopano-webapp

Boot startup: rcctl enable php70_fpm

Config

echo "extension=mapi.so" > /etc/php-7.0/kopano.ini

Don’t expose php: sed -i 's/^expose_php.*/expose_php = off/' /etc/php-7.0.ini

Change Server Socket:

sed -i 's|^.define("DEFAULT_SERVER".*|define("DEFAULT_SERVER", "file:///run/server.sock");|' /var/www/kopano-webapp/config.php

Start php-fpm: rcctl start php70_fpm

Z-Push

Install: pkg_add z-push

Config

Change Server Socket:

sed -i "s|^define('MAPI_SERVER'.*|define('MAPI_SERVER', 'file:///run/server.sock');|" /var/www/z-push/backend/kopano/config.php

httpd

Basic config at OpenBSD httpd

Kopano config:

  connection {
    max request body 20971520
  }
  root "/kopano-webapp"
  directory index "index.php"
  location "*.php" {
    fastcgi socket "/run/php-fpm.sock"
  }
  location "/Microsoft-Server-ActiveSync" {
    root "/z-push/index.php"
    fastcgi socket "/run/php-fpm.sock"
  }

Optionals

Kopano Passwd Plugin

If not yet installed: pkg_add unzip--

Install:

mkdir /var/www/kopano-webapp/plugins/passwd
ftp -o /var/www/kopano-webapp/plugins/passwd-1.3.zip https://github.com/silentsakky/zarafa-webapp-passwd/releases/download/1.3/passwd-1.3.zip
unzip -q /var/www/kopano-webapp/plugins/passwd-1.3.zip -d /var/www/kopano-webapp/plugins/passwd

Config:

sed -i "s|^define('PLUGIN_PASSWD_USER_DEFAULT_ENABLE'.*|define('PLUGIN_PASSWD_USER_DEFAULT_ENABLE', true);|" /var/www/kopano-webapp/plugins/passwd/config.php
sed -i "s|^define('PLUGIN_PASSWD_LOGIN_WITH_TENANT'.*|define('PLUGIN_PASSWD_LOGIN_WITH_TENANT', true);|" /var/www/kopano-webapp/plugins/passwd/config.php

OpenBGPD

Project: bgp-spamd

Boot startup: rcctl enable bgpd

Config

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' is required, to remove entries when routes are withdrawn
# This updates the <bgp-spamd-bypass> table in PF
match from group "spamd-bgp" community $spamdAS:42 set pftable "bgp_spamd_bypass"
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

Bypass Big Mailproviders

Add this to your /usr/local/sbin/spamd-update:

smtpctl spf walk <<EOF | sort -u | pfctl -t trusted_bypass -T replace -f- 2>/dev/null
_netblocks.google.com
_netblocks2.google.com
_netblocks3.google.com
steampowered.com
amazonses.com
outlook.com
_spf.apple.com
EOF

Add the trusted_bypass table to your pf.conf:

table <trusted_bypass> persist

pass in quick log on egress proto tcp \
  from { <trusted_bypass>, <bgp_spamd_bypass>, <nospamd>, <spamd-white> } \
...

Manual Spam-Reporting

For the manual spam-reporting to work properly, you have to forward the desired SPAM-Mail as attachment to spamreport@example.com (replace example.com with your domain).

The spamreport fake MDA (Mail Delivery Agent) will inspect the Recieved: mail header to find the sender IP-address.

This IP-address will then be blacklisted in the custom blacklist.

Add the spamreport system user: useradd -d /var/empty -s /sbin/nologin spamreport

Create the spamreport MDA:

cat <<'EOF'> /usr/local/sbin/spamreport
#!/bin/sh
tmp=$(mktemp)
blacklist="/etc/mail/custom.black"
perl -ne 'print "$1\n" if/\[([12]?[0-9]{1,2}.*)\]/&&!/127/' >>${blacklist}
sort -u -o ${tmp} ${blacklist}
cp ${tmp} ${blacklist}
rm -f ${tmp}
EOF
chmod 755 /usr/local/sbin/spamreport

Change blacklist permissions:

chgrp spamreport /etc/mail/custom.black
chmod 664 /etc/mail/custom.black

Change your smtpd.conf to look like this (replace example.com with your domain):

action "spamreport" mda "/usr/local/sbin/spamreport" user "spamreport"
match from local rcpt-to "spamreport@example.com" action "spamreport"

Restart smtpd: rcctl restart smtpd

ClamAV

Optional

Install: pkg_add clamav clamsmtp

Boot startup:

rcctl enable clamd

Config

clamd

/etc/clamd.conf

Comment out the 'Example' string:

#Example

Uncomment these options:

TCPSocket 3310
TCPAddr 127.0.0.1

freshclam

/etc/freshclam.conf

Comment out the 'Example' string:

#Example

Change
#DatabaseMirror db.XY.clamav.net
to
DatabaseMirror db.de.clamav.net

Create the update cronjob:

echo "20 * * * * /usr/local/bin/freshclam >/dev/null 2>&1" >> /var/cron/tabs/root

clamsmtpd

Create two clamsmtpd configs:

cp /etc/clamsmtpd.conf /etc/clamsmtpd-in.conf
cp /etc/clamsmtpd.conf /etc/clamsmtpd-out.conf
/etc/clamsmtpd-in.conf
Change to match these values:
OutAddress: 10026
Listen: 127.0.0.1:10025
User: _clamav

Add this option:
ClamAddress: 127.0.0.1:3310
/etc/clamsmtpd-out.conf
Change to match these values:
OutAddress: 10028
Listen: 127.0.0.1:10027
User: _clamav

Add this option:
ClamAddress: 127.0.0.1:3310

Create the two init scripts:

cat <<'EOF'> /etc/rc.d/clamsmtpd_in
#!/bin/sh

daemon="/usr/local/sbin/clamsmtpd -f /etc/clamsmtpd-in.conf"

. /etc/rc.d/rc.subr

rc_cmd $1
EOF
cat <<'EOF'> /etc/rc.d/clamsmtpd_out
#!/bin/sh

daemon="/usr/local/sbin/clamsmtpd -f /etc/clamsmtpd-out.conf"

. /etc/rc.d/rc.subr

rc_cmd $1
EOF

chmod 0755 /etc/rc.d/clamsmtpd_{in,out}

Enable the services:

rcctl enable clamsmtpd_in
rcctl enable clamsmtpd_out

Run the initial freshclam: /usr/local/bin/freshclam

Start the daemons:

rcctl start clamd
rcctl start clamsmtpd_in
rcctl start clamsmtpd_out

Backup

#!/bin/sh

db="kopano"
db_user="kopano"
db_pw="<password>"

set -A paths \
/etc/kopano \
/etc/mail \
/etc/pf.conf \
/etc/httpd.conf \
/etc/clamsmtpd-in.conf \
/etc/clamsmtpd-out.conf \
/etc/clamd.conf \
/etc/freshclam.conf \
/etc/ssl \
/etc/acme-client.conf \
/etc/bgpd.conf \
/etc/rc.conf.local \
/etc/rc.d/clamsmtpd_in \
/etc/rc.d/clamsmtpd_out \
/etc/my.cnf \
/etc/rc.d/kopano \
/etc/php-7.0/kopano.ini \
/etc/php-7.0.ini \
/var/db/clamav \
/var/db/spamassassin \
/var/db/spamd \
/var/db/kopano \
/var/www/kopano-webapp/config.php \
/var/www/z-push/backend/kopano/config.php \
/usr/local/sbin/spamd-update \
/usr/local/sbin/cron-acme-update \
/usr/local/sbin/spamreport \
/usr/local/sbin/watchdog \
/etc/mail/spamassassin/local.cf \
/var/cron/tabs/root \
/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

tar cfvz /root/mailserver_backup_$(date +%Y%m%d_%H%M%S).tgz ${paths[@]}

rm /tmp/${db}.sql /tmp/*.pf

Restore backup:

tar xfpvz mailserver_backup<date>.tgz -C /

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

Bug fixes

Watchdog

cat <<'EOF'>/usr/local/sbin/watchdog
#!/bin/sh

for p in $(rcctl ls failed|grep -v spampd); do
  rcctl start ${p}
done

z=$(ps aux|grep -E "_spampd.*Z"|grep -v grep)
if [ "${z}" != "" ] || ! rcctl check spampd >/dev/null; then
  pkill -9 -f spampd
  rcctl start spampd
fi
EOF
chmod 755 /usr/local/sbin/watchdog
echo "*/5 * * * * /usr/local/sbin/watchdog >/dev/null 2>&1" >> /var/cron/tabs/root

END