you@vsis.online:~ $ ls .
❌ ERROR: No such vsis or online.

you@vsis.online:~ $ Posts; Categories; Tags;

Running a mail server on NetBSD for fun

NetBSD email installation

This is the way I set up a mail server in NetBSD. It uses base Postfix, Dovecot and other packages from pkgsrc. Beware that I’m not an expert on this topic, and security issues could be there. Also, this is suited for personal use. I wouldn’t use this for enterprise.

In the enterprise, usually there’s no “mail server” but a group of servers with services like MTA (message transfer agent), MSA (message submission agent) and MDA (message delivery agent). So, there’s a “mail system” instead of just a single server.

Personal use

I’m running a personal e-mail server. It’s fun and have some advantages, like creating whatever aliases I want, and having control over my data. And, of course, learning a thing or two in the process.

Running e-mail is not that complex, but require a solid knowledge in different areas of sysadmin stuff: DNS, TLS, networking, and of course SMTP and mail stuff. There’s lot of moving parts and it’s easy to misconfigure something. For that reason, I’m probably forgetting something here.

I’m using an external smart-host to send to Big E-Mail providers, so I’m not flagged as spam. But that part is optional, and I can stop using this third-party service in any moment without losing data or control.

In this simple setup, I’m using Postfix, Dovecot, Spamassasin and OpenDKIM.

Dovecot authenticates users using local /etc/passwd file. No SQL database is needed, but this only supports one domain.

Also, e-mail users are also UNIX shell users, which can be a huge security issue. Just not too concerning in my homelab.

Why?

This is part of my quest to learn NetBSD. I could easily deploy some docker-based solution. Or follow yet another Linux-based tutorial on the topic.

By manually deploying some services, like e-mail, I feel that I understand the tools I’m working with. Then I can work on automation of the task. That’s why this is not an Ansible playbook or a shell script.

In this post I’m not explaining Postfix or Dovecot configuration. I recommend reading the documentation of those projects. The goal of this post is follow my way to manually deploy an e-mail server, which helps to understand how to do stuff on NetBSD.

Installation of packages

Postfix comes with base NetBSD. The rest needs to be installed via pkgin.

pkgin install spamassassin spamass-milter dovecot dovecot-pigeonhole opendkim

Postfix configuration

The content of /etc/postfix/master.cf looks like this:

pickup    unix  n       -       n       60      1       pickup
cleanup   unix  n       -       n       -       0       cleanup
qmgr      unix  n       -       n       300     1       qmgr
tlsmgr    unix  -       -       n       1000?   1       tlsmgr
rewrite   unix  -       -       n       -       -       trivial-rewrite
bounce    unix  -       -       n       -       0       bounce
defer     unix  -       -       n       -       0       bounce
trace     unix  -       -       n       -       0       bounce
verify    unix  -       -       n       -       1       verify
flush     unix  n       -       n       1000?   0       flush
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
smtp      unix  -       -       n       -       -       smtp
relay     unix  -       -       n       -       -       smtp
        -o syslog_name=postfix/$service_name
showq     unix  n       -       n       -       -       showq
error     unix  -       -       n       -       -       error
retry     unix  -       -       n       -       -       error
discard   unix  -       -       n       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       n       -       -       lmtp
anvil     unix  -       -       n       -       1       anvil
scache    unix  -       -       n       -       1       scache
postlog   unix-dgram n  -       n       -       1       postlogd

smtp inet n - y - - smtpd
  -o content_filter=spamassassin
submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_tls_auth_only=yes
  -o smtpd_enforce_tls=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_sender_restrictions=reject_sender_login_mismatch
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject_unauth_destination
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
spamassassin unix -     n       n       -       -       pipe
  user=spamd argv=/usr/pkg/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient}

The important part is that submission service requires TLS connections from client and reject non-authenticated e-mail, so we don’t become an open relay.

Domain Configuration

We can either edit /etc/postfix/main.cf, or set config values with postconf -e. I’d rather edit the file and reload the service, but for scripting postconf can be useful.

Here we define the domain name. Replace example.com with a proper name.

postconf -e "myhostname = example.com"
postconf -e "mail_name = example.com"
postconf -e "mydomain = example.com"
postconf -e 'mydestination = $mydomain, mail, localhost.localdomain, localhost, localhost.$mydomain'

In most places a host named like mail.example.com is used, even if the mail domain is example.com In those cases myhostname should be mail.example.com, but mail_name and mydomain should be the shorter example.com.

TLS Configuration

I’m using Let’s Encrypt to get my TLS certificates. That setup is not covered here. Let’s assume that you already have a TLS certificate for example.com.

postconf -e "smtpd_tls_key_file=/usr/pkg/etc/letsencrypt/live/example.com/privkey.pem"
postconf -e "smtpd_tls_cert_file=/usr/pkg/etc/letsencrypt/live/example.com/fullchain.pem"
postconf -e 'smtpd_tls_security_level = may'
postconf -e 'smtp_tls_security_level = may'
postconf -e 'smtpd_tls_auth_only = yes'
postconf -e 'smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'
postconf -e 'smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'
postconf -e 'smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'
postconf -e 'smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'
postconf -e 'inet_interfaces = all'

Variables named smtp_ are meant to define the behavior of Postfix as an SMTP client. While the ones like smtpd_ the behavior of the SMTP server.

With this config, TLS is optional for everyone, except for authenticating. There it’s mandatory.

HELO, sender, relay and recipient restrictions

More restrictions to avoid unauthorized e-mail. We allow our authenticated users and servers with FQDN and reverse DNS sending to us.

postconf -e "smtpd_sender_login_maps = regexp:/etc/postfix/login_maps"
postconf -e 'smtpd_sender_restrictions = reject_sender_login_mismatch, permit_sasl_authenticated, permit_mynetworks, reject_unknown_reverse_client_hostname, reject_unknown_sender_domain'
postconf -e 'smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unknown_recipient_domain'
postconf -e 'smtpd_relay_restrictions = permit_sasl_authenticated, reject_unauth_destination'
postconf -e 'smtpd_helo_required = yes'
postconf -e 'smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname'

The content for /etc/postfix/login_maps should look like this.

/^(.*)@example\.com$/ ${1}

We are gonna configure Dovecot to use our Unix users and passwords. But those don’t have the @example.com part in their names. Here we are defining a map where any user@example.com is mapped as user, so we can use local authentication.

Privacy

Some headers may give away sender IP address. Let’s define /etc/postfix/header_checks to ignore them.

postconf -e "header_checks = regexp:/etc/postfix/header_checks"
if [ ! -f /etc/postfix/headers_cheks ]; then
	echo """
/^Received:.*/     IGNORE
/^X-Originating-IP:/    IGNORE
	""" > /etc/postfix/headers_cheks
fi

Smart-host configuration

This part is optional. We can use a smart-host to relay our messages if our IP address have low reputation.

postconf -e 'smtp_use_tls = yes'
postconf -e 'smtp_sender_dependent_authentication = yes'
postconf -e 'smtp_sasl_auth_enable = yes'
postconf -e 'smtp_sasl_security_options = noanonymous'
postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
postconf -e 'smtp_always_send_ehlo = yes'
postconf -e 'transport_maps = hash:/etc/postfix/transport'
postconf -e 'smtp_tls_CAfile = /etc/openssl/certs/ca-certificates.crt'

The file /etc/postfix/sasl_passwd have the credentials of your account in the smart-host. Something like this.

[smarthost.example]:2525	user:p4ssw0rd

The brackets in [smarthost.example] means that Postfix should not look for the MX DNS registry. The A registry is needed for that name. The :2525 is the TCP port where we should connect.

The /etc/postfix/transport file should look like this.

example.com                local
some_user@somehost.example relay:[smarthost.example]:2525
gmail.com                  relay:[smarthost.example]:2525
hotmail.com                relay:[smarthost.example]:2525
*                          smtp

This file is used to determine how to send messages to different domains. We can either use a relay, our smart-host, or smtp which means that we send the message directly.

The first column indicates the destination. It can be a domain or a specific e-mail address. The wildcard * means any address and any domain.

The second column indicates the method. Local delivery, a relay with the address of the smart-host or send directly via SMTP.

If I had to send a very important message to some_user@somehost.example I can use the smart-host, to avoid get flagged as spam. You should note that the privacy of the message is lost: the smart-host needs to read the whole mail to relay it.

Once those maps are ready, we should run postmap to have them in binary (fast) format.

postmap /etc/postfix/transport
postmap /etc/postfix/sasl_passwd

Dovecot configuration

Here we configure Sieve and Dovecot. The first step is to have a spam user. We are gonna run our spam filter with that. Then we compile the Sieve rules.

grep -q '^spamd:' /etc/passwd || useradd -g =uid -m -s /sbin/nologin spamd
mkdir -p /var/sieve
sievec /var/sieve/default.sieve
chown -R spamd:spamd /var/sieve

The contents of /var/sieve/default.sieve are something like this.

require ["fileinto", "mailbox"];
if header :contains "X-Spam-Flag" "YES"
{
	fileinto "Junk";
}

The rule says: if the header X-Spam-Flag is set to YES, mail goes to the Junk folder. Spamassasin is gonna set that header if it believes the message is spam.

The content of /usr/pkg/etc/dovecot/dovecot.conf is something like this.

ssl = required
ssl_cert = </usr/pkg/etc/letsencrypt/live/example.com/fullchain.pem
ssl_key = </usr/pkg/etc/letsencrypt/live/example.com/privkey.pem
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
auth_mechanisms = plain login
auth_username_format = %n

protocols = imap

# Search for valid users in /etc/passwd
userdb {
	driver = passwd
}
#Fallback: Use plain old PAM to find user passwords
passdb {
	driver = pam
}

# Our mail for each user will be in ~/Mail, and the inbox will be ~/Mail/Inbox
# The LAYOUT option is also important because otherwise, the boxes will be \`.Sent\` instead of \`Sent\`.
mail_location = maildir:~/Mail:INBOX=~/Mail/Inbox:LAYOUT=fs
namespace inbox {
	inbox = yes
	mailbox Drafts {
	special_use = \Drafts
	auto = subscribe
}
	mailbox Junk {
	special_use = \Junk
	auto = subscribe
	autoexpunge = 30d
}
	mailbox Sent {
	special_use = \Sent
	auto = subscribe
}
	mailbox Trash {
	special_use = \Trash
}
	mailbox Archive {
	special_use = \Archive
}
}

# Here we let Postfix use Dovecot's authentication system.
service auth {
  unix_listener /var/spool/postfix/private/auth {
	mode = 0660
	user = postfix
	group = postfix
  }
}

protocol lda {
  mail_plugins = $mail_plugins sieve
}

protocol lmtp {
  mail_plugins = $mail_plugins sieve
}

protocol pop3 {
  pop3_uidl_format = %08Xu%08Xv
  pop3_no_flag_updates = yes
}

plugin {
	sieve = ~/.dovecot.sieve
	sieve_default = /var/sieve/default.sieve
	sieve_dir = ~/.sieve
	sieve_global_dir = /var/sieve/
}

This configuration is for Dovecot 2.3. In 2.4 Dovecot has breaking changes that makes old configuration files obsolete. If you are running Dovecot 2.4, you need to fix this file accordingly.

Let’s configure Postfix to use Dovecot for authentication and delivery, and Spamassasin as filter.

cp -v /etc/pam.d/imap /etc/pam.d/dovecot

postconf -e 'smtpd_sasl_security_options = noanonymous, noplaintext'
postconf -e 'smtpd_sasl_tls_security_options = noanonymous'
postconf -e 'milter_default_action = accept'
postconf -e 'milter_protocol = 6'
postconf -e 'smtpd_milters = inet:localhost:12301'
postconf -e 'non_smtpd_milters = inet:localhost:12301'
postconf -e 'mailbox_command = /usr/pkg/libexec/dovecot/deliver'
postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'smtpd_sasl_type = dovecot'
postconf -e 'smtpd_sasl_path = private/auth'

OpenDKIM configuration

DKIM is used to sign parts of the mail with a key pair, whose public part is in a TXT DNS record. So other servers can verify the signature to make sure the message comes from your server and have not been forged.

This is the content of /usr/pkg/etc/opendkim.conf

Canonicalization	relaxed/simple
Domain	example.com
InternalHosts refile:/etc/postfix/dkim/trustedhosts
KeyTable file:/etc/postfix/dkim/keytable
Selector		default
SigningTable	refile:/etc/postfix/dkim/signingtable
Socket			inet:12301@localhost
Syslog			Yes

Let’s generate a key pair if we don’t have one.

mkdir -p /etc/postfix/dkim/example.com
if [ ! -f /etc/postfix/dkim/example.com/default.private ]; then
	opendkim-genkey -D "/etc/postfix/dkim/example.com" -d example.com -s default
	chown -R root:opendkim /etc/postfix/dkim/
	chmod -R g+r /etc/postfix/dkim/example.com/
fi

The contents of /etc/postfix/dkim/trustedhosts. We only trust localhost, basically.

127.0.0.1
::1

The contents of /etc/postfix/dkim/keytable are our DNS record, the domain, selector name and path of the secret key.

default._domainkey.example.com example.com:default:/etc/postfix/dkim/example.com/default.private

It is possible to have multiple key pairs per domain. Hence the selectors. Those selector indicates what DNS record have the public key to verify the signature. Because our setup is very simple, we only have one default selector.

The contents of /etc/postfix/dkim/signingtable. We tell OpenDKIM what pair of address/selector we should use. In this case, all messages are signed with default selector.

*@example.com default._domainkey.example.com

RC configuration

The /etc/rc.conf file should have the following services.

# email
postfix=YES
dovecot=YES
spamass_milter=YES
opendkim=YES
spamd=YES

DNS registries

Now we can update the DNS registries needed to send and receive messages.

default._domainkey.example.com    TXT     v=DKIM1; k=rsa; ${PUBLIC_KEY}"
dmarc.example.com                 TXT     v=DMARC1; p=reject; rua=mailto:postmaster@example.com; fo=1"
example.com                       TXT     v=spf1 mx a -all"
example.com                       MX      example.com
example.com                       A       ${IP_ADDR}
example.com                       AAAA    ${IP6_ADDR}

You can find the DKIM public key in /etc/postfix/dkim/example.com/default.txt

In Summary

I didn’t dig too much on what those configurations are. I recommend you to read the actual documentation of those projects. Here I just put in place the manual steps to deploy a mail server on NetBSD. Which is a very fun way to learn this OS.

There’s more stuff needed to have a functional mail server. Monitoring, backups, etc. None of that is covered here.

See also