OpenBSD Server

Initial Setup

Installing OpenBSD

One thing that might stop most people from using OpenBSD on a VPS specifically is the fact that most hosting providers don’t allow you to custom operating systems besides the ones that they offer. Luckily, my VPS provider Frantech1, allows me to upload my own ISO images in order for you to install any OS that I like. So make sure that your VPS provider actually allows you to run OpenBSD.

Go ahead and install OpenBSD normally. Here are a few tips when installing OpenBSD for a server:

- Make sure to start sshd by default (otherwise you can’t login)
- Create a user for your system
- Make sure you allow root ssh login (we’ll need it temporarily)
- You can delete the swap, /usr/X11R6, /usr/src/, and /usr/obj partitions when partitioning your disk as we don’t need them (make sure to use that free space though)
- Make sure you select to install all sets unless you know what you’re doing

Congrats! You now have a basic OpenBSD installation.

Configuring

First thing is first, we need root privileges. So run this in order to connect as the root user:


ssh root@example.com

where example.com is your domain name. As the root user we need to create the /etc/doas.conf file and add the wheel group to it. We can do so by running:


echo "permit persist :wheel" > /etc/doas.conf

We can now exit our current ssh session in order to login as the user we created when we installed OpenBSD.

Let’s first update our system by running these commands using doas:

doas syspatch
doas pkg_add -Uu

The syspatch command applies any necessary patches to your system and pkg_add -Uu just updates packages. Speaking of adding packages, you can use pkg_add in order to install your favorite text editor (you’ll need it). While we are at it, let’s go ahead and disable ssh logins for root since we don’t need it anymore. Edit /etc/ssh/sshd_config and add this line to it:

PermitRootLogin no

Finally, let’s create the file /etc/sysctl.conf that will contain system-wide settings. We will put this in /etc/sysctl.conf:

kern.maxproc=8192
kern.maxfiles=32768
kern.maxthread=16384
kern.shminfo.shmall=536870912
kern.shminfo.shmmax=2147483647
kern.shminfo.shmmni=4096

Please note that these settings are for my system and will chage from system to system. As a rule of thumb:

kern.shminfo.shmmax should be set to half of your maximum RAM (in bytes)
kern.shminfo.shmall should be set to kern.shminfo.shmmax divided by 4096 (4096 is the page size in OpenBSD).

These are just a few settings that will get a bit more performance out of our OpenBSD system. It just increases the maximum number of processes able to run at once as well as the maximum amount of shared memory. You can find a detailed explanation of these settings by looking at the man page for sysctl (hint: run man 2 sysctl).

Stay Tuned

That’s just the beginning of setting up an OpenBSD server, but keep a close eye on my blog as I will upload the next part of this guide somewhat soon which will be about setting up a static web server using httpd…assuming that I get around to writing it eventually.

OpenBSD Server: httpd

Setup httpd

Let’s go ahead and create a very simple http server using httpd just to make sure that everything is working properly. We can make a config file for httpd at /etc/httpd.conf like so:

server "example.com" {
    listen on * port 80
    alias "www.example.com"
    root "/htdocs/example.com"
}

types {
    include "/usr/share/misc/mime.types"
}

This will create a web server for “example.com” (make sure to replace “example.com” with your domain name) on port 80 and will redirect traffic from “www.example.com” to “example.com”. Next we need to create /var/www/htdocs/example.com to host our files:

mkdir -p /var/www/htdocs/example.com

Let’s just put a very basic webpage at /var/www/htdocs/example.com/index.html to serve as our homepage.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Example webpage</title>
    <meta charset="utf-8">
  </head>
  <body>
    <h1>This is an example webpage</h1>
    <p>Looks like it works!</p>
  </body>
</html>

Now, in order to make sure that our httpd config file is correct and does not have any errors, we can run this command to check it:

doas httpd -n

If your configuration is correct, then it will spit out configuration ok and you can move on. If not, then it will spit out what error you have to fix in the /etc/httpd.conf file. Finally we can enable httpd on boot and start it using the rcctl command:

doas rcctl enable httpd
doas rcctl start httpd

Congrats! You now have a web page on the internet at “example.com”!

Getting a certificate

Now that we verified that a normal web server works, we can go ahead and setup ACME in order to obtain a certificate in order to allow HTTPS connects to our website. First we need to create a file named /etc/acme-client.conf and add this to our file:

authority letsencrypt {
    api url "https://acme-v02.api.letsencrypt.org/directory"
    account key "/etc/acme/letsencrypt-privkey.pem"
}

domain example.com {
    # Allows an alias
    alternative names { www.example.com }
    domain key "/etc/ssl/private/example.com.key"
    domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
    sign with letsencrypt
}

The first section defines Let’s Encrypt as our certificate authority. The second section defines a domain to fetch a certificate for. It will place our domain key at /etc/ssl/private/example.com.key and our certificate at /etc/ssl/example.com.fullchain.pem (remember to replace “example.com” with your domain).

Now we have to edit /etc/httpd.conf in order to ACME to generate a certificate for our domain:

server "example.com" {
    listen on * port 80
    alias "www.example.com"
    root "/htdocs/example.com"
    
    location "/.well-known/acme-challenge/*" {
        request strip 2
        root "/acme"
    }
}

types {
    include "/usr/share/misc/mime.types"
}

Now we can run this command in order to generate a certificate for our domain:

acme-client -v example.com

Setup HTTPS

Now that we have our certificate, let’s go ahead and edit our /etc/httpd.conf file in order to use HTTPS for our web server:

server "example.com" {
    listen on * tls port 443
    alias "www.example.com"
    root "/htdocs/example.com"
    
    tls {
        certificate "/etc/ssl/example.com.fullchain.pem"
        key "/etc/ssl/private/example.com.key"
    }
    
    location "/.well-known/acme-challenge/*" {
        request strip 2
        root "/acme"
    }
}

server "example.com" {
    listen on * port 80
    alias "www.example.com"
    block return 301 "https://example.com$REQUEST_URI"
}

types {
    include "/usr/share/misc/mime.types"
}

Finally we can restart httpd and go to “example.com” and we will have a secure connection to the website.

Automatically renew Certificates

The final part of this post is going to take care of lose ends by automatically renewing our certificate before it expires. Most certificates expire after only a few months, so we want to automatically renew them. In order to do so we can make a new file at /etc/weekly.local. Any commands that we put in this file will run once a week, so lets put this in /etc/weekly.local:

# Check for new certificates every week
acme-client -v example.com
rcctl restart httpd

OpenBSD Server: smtpd

Some Needed Packages

First, we need to install some extra packages for our email server to work the way we want it to. We will need rspamd to filter out spam, dovecot in order to use IMAP and or POP3 to view our emails, and we will need dovecot-pigeonhole to help sort our emails:

doas pkg_add rspamd-- dovecot-- dovecot-pigeonhole-- opensmtpd-filter-rspamd

DNS Records

In order for our email server to properly work and also to not get filtered or marked as spam, we will need to add some DNS records.

MX Record

A MX record is required to tell other SMTP servers where to deliver emails sent to our address. For our MX record we can have our emails redirected to mail.example.com. When creating an MX record, this is what it should look like:

Record Type     Host        Value        MX Priority    TTL
     V            V           V                V         V
    MX            @    mail.example.com        0        3600

Our record type should (obviously) be MX, our host should be empty (which is sometimes represented as an @ symbol), our value is where our mail should go which is mail.example.com, our MX priority should just be 0 for now, and our TTL is set to 3600 seconds (or 1 hour).

For reference, this is how my MX record is setup for brycevandegrift.xyz:

SPF Record

Our SPF record is vital in order to detect spam. It defines what servers can send emails from a domain. In our case, only mail.example.com will be sending emails on our behalf, so we can set our SPF record accordingly:

Record Type     Host        Value        TTL
     V            V           V           V
    TXT           @     v=spf1 mx all    3600

Our SPF record is just a normal TXT record that has the string “v=spf1 mx all” in it which allows emails to be sent in reference to our MX record.

DKIM Record

We need a DKIM record in order to cryptographically sign and verify the emails we send. Without DKIM your emails will NOT be accepted by most email servers and will be marked as spam. In order to use DKIM we will need to generate our private and public keys on our system. We can generate our keys and give them the correct permissions like so:

# First enter root session
su

umask 077
install -d -o root -g wheel -m 755 /etc/mail/dkim
install -d -o root -g _rspamd -m 775 /etc/mail/dkim/private
openssl genrsa -out /etc/mail/dkim/private/example.com.key 2048
openssl rsa -in /etc/mail/dkim/private/example.com.key -pubout -out /etc/mail/dkim/example.com.pub
chgrp _rspamd /etc/mail/dkim/private/example.com.key /etc/mail/dkim/private/
chmod 440 /etc/mail/dkim/private/example.com.key
chmod 775 /etc/mail/dkim/private/

# Exit root session
exit

Now that we successfully generated our private and public DKIM keys, we need to put our public DKIM key into our DKIM record. We can get our DKIM public key as a single line by running this awk command:

doas awk '/PUBLIC/ { $0="" } { printf ("%s",$0) } END { print }' /etc/mail/dkim/example.com.pub

We can now copy that string and put it into our DKIM record.

Record Type        Host                    Value                  TTL
     V               V                       V                     V
    TXT      dkim._domainkey   v=DKIM1;k=rsa;p=<YOUR DKIM KEY>    3600

Just replace with the string you just copied and your DKIM record is good to go.

DMARC Record

We now need to add a DMARC record to our DNS records. DMARC is just another way of preventing email spoofing and spam by telling receiving email servers what to do with potentially invalid emails. Here is a good default to use for you DMARC record:

Record Type        Host                                Value                                  TTL
     V               V                                   V                                     V
    TXT              @     v=DMARC1;p=reject;rua=mailto:dmarc@example.com;sp=reject;aspf=r;   3600

Wow, the value for this record is quite long, let’s break down what it means. p=reject and sp=reject just means to immediately discard any invalid emails, aspf=r means that any email sent from any subdomain of example.com is valid, and rua=dmarc@example.com tells where the mail server receives DMARC reports (don’t worry, you shouldn’t care about DMARC reports, they are mostly useless).

PTR Record

Finally, our last record! All you need to do is set a PTR record (also called a Reverse DNS pointer) that points to mail.example.com. That’s it. This is yet another mechanism to prevent spam. Please note that there are different ways to set a PTR record, most ways involve finding a “Reverse DNS” option for your VPS/hosting provider and using that.

Email Configuration

At last, we can now configure the setting for our email server. Our first order of business is to get a valid certificate for our mail.example.com subdomain. This process should be somewhat familiar if you read my last article. Let’s first append this to the end of our /etc/acme-client.conf:

domain mail.example.com {
    domain key "/etc/ssl/private/mail.example.com.key"
    domain full chain certificate "/etc/ssl/mail.example.com.fullchain.pem"
    sign with letsencrypt
}

Now we just need to add this to /etc/httpd.conf in order to get a certificate for any subdomain of example.com:

server "*" {
    listen on * port 80
    
    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2
    }
    
    location * {
        block return 302 "https://$HTTP_HOST$REQUEST_URI"
    }
}

Now we just run this command to generate a certificate for mail.example.com:

doas acme-client -v mail.example.com

Finally, we shouldn’t forget to automatically renew our certificates, so change /etc/weekly.local as follows:

# Check for new certificates every week
acme-client example.com
acme-client mail.example.com

rcctl restart httpd smtpd dovecot

SPAM

The first program that we should configure is rspamd. Only a few things need to be set in order for it to work properly. Go ahead and create a file at /etc/rspamd/local.d/dkim_signing.conf and add this to it:

# This should always be true
allow_username_mismatch = true;

# Allow rspamd to sign with our DKIM key
domain {
    example.com {
        path = "/etc/mail/dkim/private/example.com.key";
        selector = "dkim";
    }
}

This is entirely optional, but if you want more performance you can enable and start redis as a cache backend:

doas rcctl enable redis
doas rcctl start redis

Now enable and start rspamd:

doas rcctl enable rspamd
doas rcctl start rspamd

SMTP

Now let’s configure OpenSMTPD in order to send and receive emails. We will need to create a config file at /etc/mail/smtpd.conf and configure it like so:

# Set locations to certificates
pki example.com cert "/etc/ssl/mail.example.com.fullchain.pem"
pki example.com key "/etc/ssl/private/mail.example.com.key"
pki example.com dhe auto

# Delimiter for email addresses
smtp sub-addr-delim '_'

# Sets rspamd as our filter
filter rspamd proc-exec "filter-rspamd"

# Sets ports to use for mail transfer
listen on all port 25 tls pki "example.com" filter "rspamd"
listen on all port 465 smtps pki "example.com" auth mask-src filter "rspamd"
listen on all port 587 tls-require pki "example.com" auth mask-src filter "rspamd"

# Load mail aliases
table aliases file:/etc/mail/aliases

# Actions for mail delievery
action "local" lmtp "/var/dovecot/lmtp" alias <aliases>
action "outbound" relay

# Actions for recieving mail
match from any for domain "example.com" action "local"
match from local for local action "local"

# Actions for sending mail
match from any auth for any action "outbound"
match from local for any action "outbound"

Now we need to place our mail delievery address in /etc/mail/mailname:

mail.example.com

And now we can test our smtpd config by running:

doas smtpd -n -f /etc/mail/smtpd.conf

If everything is all good then you can restart smtpd:

doas rcctl restart smtpd

IMAP/POP3

The last part of our email server is dovecot which allows us to view our emails using IMAP and or POP3. In order to use our certificates we will need to configure /etc/dovecot/conf.d/10-ssl.conf like so:

ssl_cert = </etc/ssl/mail.example.com.fullchain.pem
ssl_key = </etc/ssl/private/mail.example.com.key
ssl_dh = </etc/dovecot/dh.pem

We need to generate a Diffie-Hellman key in order to make each TLS connection unique (these commands might take a while to generate a key):

doas openssl dhparam -out /etc/dovecot/dh.pem 4096
doas chown _dovecot:_dovecot /etc/dovecot/dh.pem
doas chmod 400 /etc/dovecot/dh.pem

Now we need to configure our mailbox and some basic settings for dovecot. Edit /etc/dovecot/conf.d/10-mail.conf so it resembles the following:

# Where to put user maildir
mail_location = maildir:~/Maildir

namespace inbox {
    inbox = yes
}

mmap_disable = yes
first_valid_uid = 1000
mail_plugin_dir = /usr/local/lib/dovecot

protocol !indexer-worker {
}

mbox_write_locks = fcntl

Let’s edit /etc/dovecot/conf.d/20-lmtp.conf in order to allow sieve and mail plugins:

protocol lmtp {
    mail_plugins = $mail_plugins sieve
}

We now meed to modify /etc/dovecot/conf.d/15-mailboxes.conf and add this inside the namespace inbox { section of the file in order to create a Spam folder:

mailbox Spam {
    auto = create
    special_use = \Junk
}

If you don’t plan on using IMAP and only wish to use POP3 for viewing/sending emails then this last step is entirely optional, however I encourage users to use IMAP instead of POP3. To enable IMAP we just need to edit /etc/dovecot/conf.d/20-imap.conf like so:

protocol imap {
    mail_plugins = $mail_plugins imap_sieve
    mail_max_userip_connections = 25
}

Sieve and Filtering

Finally, the last part of setting up dovecot is configuring sieve in order to filter and group emails properly. Go ahead and edit /etc/dovecot/conf.d/90-plugin.conf so it looks like the following:

plugin {
    sieve_plugins = sieve_imapsieve sieve_extprograms
    
    # Moving to Spam
    imapsieve_mailbox1_name = Spam
    imapsieve_mailbox1_causes = COPY
    imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve
    
    # Moving from Spam
    imapsieve_mailbox2_name = *
    imapsieve_mailbox2_from = Spam
    imapsieve_mailbox2_causes = COPY
    imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve
    
    sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
    
    sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
}

This configuration allows sieve to “learn” what types of emails are spam and what types of messages are not spam (sometimes called “ham” 🍖) depending on where you move them. In order to make these filters functional we need to create two sieve scripts. The first one is /usr/local/lib/dovecot/sieve/report-spam.sieve. Let’s create it and add the following to it:

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.user" "*" {
    set "username" "${1}";
}

pipe :copy "sa-learn-spam.sh" [ "${username}" ];

The second filter is /usr/local/lib/dovecot/sieve/report-ham.sieve and should look like this:

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.mailbox" "*" {
    set "mailbox" "${1}";
}

if string "${mailbox}" "Trash" {
    stop;
}

if environment :matches "imap.user" "*" {
    set "username" "${1}";
}

pipe :copy "sa-learn-ham.sh" [ "${username}" ];

Now we will create two shell scripts for these filters. The first one is /usr/local/lib/dovecot/sieve/sa-learn-ham.sh:

#!/bin/sh
exec /usr/local/bin/rspamc -d "${1}" learn_ham

The second one is /usr/local/lib/dovecot/sieve/sa-learn-spam.sh:

#!/bin/sh
exec /usr/local/bin/rspamc -d "${1}" learn_spam

And we need to make them executable:

doas chmod +x /usr/local/lib/dovecot/sieve/sa-learn-spam.sh /usr/local/lib/dovecot/sieve/sa-learn-ham.sh

Now we need to compile the sieve filters to actually use them:

doas sievec /usr/local/lib/dovecot/sieve/report-spam.sieve
doas sievec /usr/local/lib/dovecot/sieve/report-ham.sieve

Lastly, let’s enable and start dovecot:

doas rcctl enable dovecot
doas rcctl start dovecot

Done

Congrats! You now have your own email server running on OpenBSD! You can now login to your email server using your preferred email client. If you don’t have a preferred email client then you can use Thunderbird if you want a GUI or aerc if you want to view emails from the terminal.

You can now enjoy using email knowing that your inbox won’t be monitored. 😎