Page content

Running a Mailserver on OpenBSD …



  • OpenBSD VM
  • Public IP & FQDN
  • no Portfilter from Hoster
  • root permission


pkg_add opensmtpd-extras opensmtpd-filter-rspamd dovecot dovecot-pigeonhole redis rspamd-- opensmtpd-filter-senderscore


export host="hostname"
export domain="domain.tld"
export fqdn="${host}.${domain}"


f="/etc/httpd.conf"; test -f ${f} && cp ${f} "${f}-$(date +'%s')"

cat << EOF > ${f}
# added $(date)
server "${fqdn}" {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
chown root:wheel ${f}; chmod 644 ${f}


allow Certain Ports for Any

cat << EOF >> /etc/pf.conf

# Full Access for Mail/Web, added $(date)
pass  in  log quick proto tcp   from any  to (self) port { 25 80 587 993 }

pfctl -nvf /etc/pf.conf && pfctl -f /etc/pf.conf


f="/etc/acme-client.conf"; test -f ${f} && cp ${f} "${f}-$(date +'%s')"

# Copy Example
cp /etc/examples/acme-client.conf /etc/

# Kill Last 10 Lines
sed -i -e :a -e '$d;N;2,6ba' -e 'P;D' /etc/acme-client.conf

# Append MyStuff
cat << EOF >> /etc/acme-client.conf
# added $(date)
domain ${fqdn} {
  domain key "/etc/ssl/private/${fqdn}.key"
  domain full chain certificate "/etc/ssl/${fqdn}.fullchain.pem"
  sign with letsencrypt
chown root:wheel ${f}; chmod 644 ${f}

# Start Web, get Service
rcctl -f start httpd
acme-client -v ${fqdn}
rcctl stop httpd


touch /etc/mail/credentials
chmod 0440 /etc/mail/credentials
chown _smtpd:_dovecot /etc/mail/credentials

Virtual Home

mkdir /var/vmail
useradd -c "Virtual Users Mail Account" -d /var/vmail -s /sbin/nologin -u 2000 -g =uid -L staff vmail
chown vmail:vmail /var/vmail

Create User

export user="user"; export pass=$(smtpctl encrypt PASSWORD)
echo "${user}@${domain}:${pass}:vmail:2000:2000:\
/var/vmail/${domain}/${user}" \
>> /etc/mail/credentials 

add Domains

f="/etc/mail/vdomains"; test -f ${f} && cp ${f} "${f}-$(date +'%s')"

cat << EOF > ${f}
chown _smtpd:wheel ${f}; chmod 640 ${f}

Virtual Users - Main Domain

f="/etc/mail/vusers"; test -f ${f} && cp ${f} "${f}-$(date +'%s')"
cat << EOF >> ${f}
# Domain: ${domain}
[email protected]${domain}:      [email protected]${domain}
[email protected]${domain}: [email protected]${domain}
[email protected]${domain}: [email protected]${domain}
[email protected]${domain}:  [email protected]${domain}
# vmails
[email protected]${domain}:       vmail
[email protected]${domain}:   vmail
# catchall
@${domain}:           [email protected]${domain}

chown _smtpd:wheel ${f}; chmod 640 ${f}

Virtual Users - additional Domains

echo -n "Name of additional Domain? "; read domain
echo "adding Domain: ${domain}
cat << EOF >> vusers
# Domain: ${domain}, added $(date)
[email protected]${domain}:      [email protected]${domain}
[email protected]${domain}: [email protected]${domain}
[email protected]${domain}: [email protected]${domain}
[email protected]${domain}:  [email protected]${domain}
# vmails
[email protected]${domain}:       vmail
[email protected]${domain}:   vmail
# catchall
@${domain}:           [email protected]${domain}

chown _smtpd:wheel ${f}; chmod 640 ${f}


if we wanna relay via some “relayhost” and have to authenticate there ..

echo "label username:password" >> secrets
chown root:_smtpd secrets
chmod 640 secrets


f="/etc/mail/smtpd.conf"; test -f ${f} && cp ${f} "${f}-$(date +'%s')"

cat << EOF > ${f}
# This is the smtpd server system-wide configuration file.
# installed: $(date)

pki "${fqdn}" cert "/etc/ssl/${fqdn}.fullchain.pem"
pki "${fqdn}" key  "/etc/ssl/private/${fqdn}.key"

table aliases     file:/etc/mail/aliases        # ...
table secrets     file:/etc/mail/secrets        # For Relaying
table credentials passwd:/etc/mail/credentials  # Users for Rainloop
table vdomains    file:/etc/mail/vdomains       # List all Domains
table vusers      file:/etc/mail/vusers         # User / Alias Mapping

# Filter potential spam with rspamd
filter "rspamd" proc-exec "/usr/local/libexec/smtpd/filter-rspamd"

filter dnsbl    proc-exec "filter-dnsbl -mv"

# Misc Filters
filter check_dyndns phase connect match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } junk
filter check_rdns   phase connect match !rdns disconnect "550 no rDNS available"
filter check_fcrdns phase connect match !fcrdns disconnect "550 no FCrDNS available"
filter "senderscore" proc-exec "/usr/local/libexec/smtpd/filter-senderscore -blockBelow 10 -junkBelow 70 -slowFactor 1000"

# To accept external mail, replace with: listen on all
listen on lo0
listen on egress          tls         pki "${fqdn}" hostname "${fqdn}"                    filter { check_dyndns, check_rdns, check_fcrdns, dnsbl, rspamd, senderscore }
listen on egress port 587 tls-require pki "${fqdn}" hostname "${fqdn}" auth <credentials> filter { check_dyndns, check_rdns, check_fcrdns, dnsbl, rspamd, senderscore }

# Where to store incoming emails based on the target user.
action  "local_mail"      mbox alias <aliases>
action  "domain_vmail"    maildir "/var/vmail/%{dest.domain}/%{dest.user}"  virtual <vusers>

# Relay mails when they come from authorized clients.
action "outbound"         relay host smtp+tls://[email protected]:587 auth <secrets>

# Block Bad Boys
match mail-from ""      for any                                       reject
match mail-from ""      for domain { "" "" } reject
match mail-from "[email protected]"  for any                                       reject
match mail-from "[email protected]" for rcpt-to regex "mydomain.*.net"            reject

# Next, we match incoming emails.
match       from any      for domain <vdomains>   action "domain_vmail"

# When the mail comes from and for a local user it triggers the "local_mail" action.
match       from local    for local               action "local_mail"

# HEADS UP: Authorize forwarding emails for a local machine
match       from src self for any                 action "outbound"

# HEADS UP: Authorize forwarding emails for a local machine
match       from local    for any                 action "outbound"
match auth  from any      for any                 action "outbound"
chown _smtpd:wheel ${f}; chmod 640 ${f}

Enable and Start Services

smtpd -n
rcctl enable smtpd rspamd redis
rcctl restart smtpd rspamd redis


cat << EOF >> /etc/login.conf


cap_mkdb /etc/login.conf
usermod -L dovecot _dovecot

may need a reboot?


f="/etc/dovecot/local.conf"; test -f ${f} && cp ${f} "${f}-$(date +'%s')"

cat << EOF > ${f}
auth_mechanisms = plain
first_valid_uid = 2000
first_valid_gid = 2000
mail_location = maildir:/var/vmail/%d/%n
mail_plugin_dir = /usr/local/lib/dovecot
managesieve_notify_capability = mailto
managesieve_sieve_capability = fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex  imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext imapsieve vnd.dovecot.imapsieve
mbox_write_locks = fcntl
mmap_disable = yes

namespace inbox {
  inbox = yes
  location =
  mailbox Archive {
  auto = subscribe
  special_use = \Archive
  mailbox Drafts {
  auto = subscribe
  special_use = \Drafts
  mailbox Junk {
  auto = subscribe
  special_use = \Junk
  mailbox Sent {
  auto = subscribe
  special_use = \Sent
  mailbox Trash {
  auto = subscribe
  special_use = \Trash
  prefix =

passdb {
  args = scheme=CRYPT username_format=%u /etc/mail/credentials
  driver = passwd-file
  name =

plugin {
  imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve
  imapsieve_mailbox1_causes = COPY
  imapsieve_mailbox1_name = Junk
  imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_from = Junk
  imapsieve_mailbox2_name = *
  sieve = file:~/sieve;active=~/.dovecot.sieve
  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
  sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
  sieve_plugins = sieve_imapsieve sieve_extprograms

protocols = imap sieve
service imap-login {
    inet_listener imaps {
    port = 993

service managesieve-login {
  inet_listener sieve {
    port = 4190
  inet_listener sieve_deprecated {
    port = 2000

ssl_cert = </etc/ssl/${fqdn}.fullchain.pem
ssl_key = </etc/ssl/private/${fqdn}.key
userdb {
  args = username_format=%u /etc/mail/credentials
  driver = passwd-file
  name =

protocol imap {
  mail_plugins = " imap_sieve"
chown root:wheel ${f}; chmod 640 ${f}


sed -i.bak "s#^ssl_cert = </etc/ssl/dovecotcert.pem#ssl_cert = </etc/ssl/${fqdn}.fullchain.pem#" /etc/dovecot/conf.d/10-ssl.conf
sed -i.bak "s#^ssl_key = </etc/ssl/private/dovecot.pem#ssl_key = </etc/ssl/private/${fqdn}.key#" /etc/dovecot/conf.d/10-ssl.conf

sieve scripts

cd /usr/local/lib/dovecot/sieve
tar xzf sieve-scripts.tar.gz
rm sieve-scripts.tar.gz
sievec report-ham.sieve
sievec report-spam.sieve
chmod 0755
chmod 0755

Start Service

rcctl enable dovecot redis rspamd
rcctl restart dovecot redis rspamd

Check Login

doveadm user ${user}@${domain}
doveadm auth login ${user}@${domain}


mkdir /etc/mail/dkim
cd /etc/mail/dkim
openssl genrsa -out private.key 1024
openssl rsa -in private.key -pubout -out public.key
chmod 0440 private.key
chown root:_rspamd private.key

set SFP Record via DNS

your.domain. IN TXT "v=spf1 a ip4:your-public-ip mx ~all"


default._domainkey.your.domain. IN TXT "v=DKIM1;k=rsa;p=[…public key…]"

dmarc IN TXT "v=DMARC1;p=none;pct=100;rua=mailto:[email protected]"


f="/etc/rspamd/local.d/dkim_signing.conf"; test -f ${f} && cp ${f} "${f}-$(date +'%s')"

cat << EOF > ${f}
domain { {
        path = "/etc/mail/dkim/private.key";
        selector = "default";
chown root:wheel ${f}; chmod 640 ${f}


rcctl enable redis rspamd
rcctl restart redis rspamd

sha256: 7060cbf5c4d145e055b7077f1b57fa5ad7f320fa040cdad189776da247ea523b