Web server setup with OpenBSD

by Jochem Kossen in articles

As a hobby, I host my website on my own server. It’s currently running the OpenBSD operating system. OpenBSD comes with batteries included. Among other things, it comes with a web (http / https) server, a transparent proxy and load balancer, a mail (SMTP) server, the OpenSSH shell server and a packet filter (“firewall”). Everything you need to host your website yourself. All nicely integrated. All for free.

OpenBSD is very consistent, lightweight and probably the most secure functional operating system available. Maintenance is a breeze, so it’s great for serving purposes.

Want to setup your own OpenBSD box? Then it can be helpful to see how other people have configured their machines.

Below you can find most of my configuration files.

By the way, I owe credit to Derek Sivers. While setting up OpenBSD and searching information about it I came across his website. He has quite some sensible things to say that rhyme with things I want to accomplish.

You might want to read his Tech Independence tutorial on how to set up your own server.

Doas

Doas is a replacement for sudo developed by the OpenBSD developers. It allows you to execute commands as another user. I’ve set it up to allow users in the wheel group to execute any command as root.

/etc/doas.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# $OpenBSD: doas.conf,v 1.1 2016/09/03 11:58:32 pirofti Exp $
# Configuration sample file for doas(1).
# See doas.conf(5) for syntax and examples.

# Non-exhaustive list of variables needed to build release(8) and ports(7)
#permit nopass setenv { \
#    FTPMODE PKG_CACHE PKG_PATH SM_PATH SSH_AUTH_SOCK \
#    DESTDIR DISTDIR FETCH_CMD FLAVOR GROUP MAKE MAKECONF \
#    MULTI_PACKAGES NOMAN OKAY_FILES OWNER PKG_DBDIR \
#    PKG_DESTDIR PKG_TMPDIR PORTSDIR RELEASEDIR SHARED_ONLY \
#    SUBPACKAGE WRKOBJDIR SUDO_PORT_V1 } :wsrc

# Allow wheel by default
permit persist keepenv :wheel

Web

OpenBSD includes acme-client which I use to create and manage my SSL certificates from Let’s Encrypt. ACME stands for “Automatic Certificate Management Environment”.

For the Web server I use OpenBSD’s included httpd. It works fine for serving plain html. For more complex needs it supports the FastCGI protocol.

I also use relayd as a proxy server. It allows me to (among other things) add CSP (Content Security Policy).

acme-client

Certificate configuration

/etc/acme-client.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#
# $OpenBSD: acme-client.conf,v 1.4 2020/09/17 09:13:06 florian Exp $
#
authority letsencrypt {
	api url "https://acme-v02.api.letsencrypt.org/directory"
	account key "/etc/acme/letsencrypt-privkey.pem"
}

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

domain jkossen.nl {
	alternative names { www.jkossen.nl }
	domain key "/etc/ssl/private/jkossen.nl.key"
	domain full chain certificate "/etc/ssl/jkossen.nl.fullchain.pem"
	sign with letsencrypt
}

To request your certificate, first set up httpd and relayd as below since it serves the acme challenge, then execute:

1
$ doas acme-client -v jkossen.nl

Check certificates daily

/etc/daily.local:

1
2
3
next_part "Refreshing Let's Encrypt certificates:"

acme-client jkossen.nl && rcctl reload relayd

TODO: There’s a good reason to do this, but I forgot …

1
2
$ cd /etc/ssl
$ doas ln -s jkossen.nl.fullchain.pem jkossen.nl.crt

httpd

/etc/httpd/httpd.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# $OpenBSD: httpd.conf,v 1.22 2020/11/04 10:34:18 denis Exp $

#
# jkossen.nl
#
server "jkossen.nl" {
	listen on 127.0.0.1 port 8080
	root "/jkossen.nl/public"
	log style forwarded
	location "/.well-known/acme-challenge/*" {
		root "/acme"
		request strip 2
	}
	location match "/posts/(.*)" {
		block return 301 "https://$HTTP_HOST/%1"
	}
	location "/*" {
		directory auto index
	}
}

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

server "www.jkossen.nl" {
	listen on 127.0.0.1 port 8080
	log style forwarded
	block return 301 "https://jkossen.nl$REQUEST_URI"
}

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

	# by default httpd serves ascii, not utf-8
	"application"/"rss+xml; charset=utf-8"			rss
	"text"/"html; charset=utf-8"				html htm shtml
	"text"/"plain; charset=utf-8"				txt
	"text"/"xml; charset=utf-8"				xml
	"text"/"markdown; charset=utf-8"			md
}

relayd

/etc/relayd/relayd.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ipv4="192.168.2.10"
ipv6="2a02:a442:243a:1:221:86ff:fe9e:c2c4"

table <local> { 127.0.0.1 }

http protocol https {
	tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"

	tls keypair "jkossen.nl"

	match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
	match request header append "X-Forwarded-Port" value "$REMOTE_PORT"

	match response header set "Referrer-Policy" value "same-origin"
	match response header set "X-Frame-Options" value "deny"
	match response header set "X-XSS-Protection" value "1; mode=block"
	match response header set "X-Content-Type-Options" value "nosniff"
	match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
	match response header set "Content-Security-Policy" value "default-src 'self'"
	match response header set "Permissions-Policy" value "accelerometer=()"
	match response header set "Cache-Control" value "max-age=86400"

	return error
	pass
}

relay wwwtls {
	listen on $ipv4 port 443 tls
	protocol https
	forward to <local> port 8080
}

relay www6tls {
	listen on $ipv6 port 443 tls
	protocol https
	forward to <local> port 8080
}

E-mail

I use mailbox.org for my e-mail needs, and have OpenBSD set up to relay mail through them.

You don’t need to install packages. OpenBSD comes with OpenSMTPD installed.

/etc/mail/smtpd.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#	$OpenBSD: smtpd.conf,v 1.14 2019/11/26 20:14:38 gilles Exp $

# This is the smtpd server system-wide configuration file.
# See smtpd.conf(5) for more information.

table aliases file:/etc/mail/aliases
table secrets file:/etc/mail/secrets

listen on socket

# To accept external mail, replace with: listen on all
#
listen on lo0

action "local_mail" mbox alias <aliases>
action "mailbox" relay host smtps://account1@smtp.mailbox.org auth <secrets>
#action "outbound" relay

# Uncomment the following to accept external mail for domain "example.org"
#
# match from any for domain "example.org" action "local_mail"
match from local for local action "local_mail"
#match mail-from "@jkossen.nl" for any action "mailbox"
match from local for any action "mailbox"
#match from local for any action "outbound"

/etc/mail/secrets

Create this file containing your mailbox.org credentials.

1
account1 me@mydomain.com:myPa$$w0rd

Replace me@mydomain.com with your mailbox.org user name (which should be your full e-mail address). Replace myPa$$w0rd with your password.

NOTE: account1 in smtps://account1@smtp.mailbox.org in smtpd.conf refers to account1 in /etc/mail/secrets.

If you change it, change both.

Set permissions

Make sure only root and smtpd can read your account information and password.

1
2
$ doas chown root:_smtpd /etc/mail/secrets
$ doas chmod 0640 /etc/mail/secrets