Web server setup with OpenBSD

by Jochem Kossen

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 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.


# $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 { \

# Allow wheel by default
permit persist keepenv :wheel


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 security headers such as a CSP (Content Security Policy).


Certificate configuration


# $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:

  $ doas acme-client -v jkossen.nl

Check certificates daily


  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 …

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



# $OpenBSD: httpd.conf,v 1.22 2020/11/04 10:34:18 denis Exp $

# jkossen.nl
server "jkossen.nl" {
    listen on 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 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




table <local> { }

http protocol https {

    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

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


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.


#       $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>

# 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 from local for any action "mailbox"


Create this file containing your mailbox.org credentials.

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.

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