Migrating from nginx to OpenBSD’s httpd and relayd
Author: Jake Bauer | Published: 2021-02-17

The configuration specified in this blog post is out of date. Please refer to OpenBSD Server Details for my updated configuration.
Having set up my mail server on OpenBSD, I’ve been very satisfied with the cohesiveness of the operating system; it has been a breeze to administrate. Since certbot just stopped working randomly on my previous server running Debian 10 and nginx, I took it as an opportunity to try out OpenBSD for hosting my website and reverse proxy. OpenBSD includes two daemons written by the OpenBSD developers—httpd and relayd—for just those purposes. They also provide acme-client as an alternative to certbot. All of this was done on OpenBSD 6.8.
Below is my httpd configuration. This contains configurations for renewing the
TLS certificate as well as serving both www.paritybit.ca and
ftp.paritybit.ca with redirects as needed. If I wanted to,
I could also split these into separate config files and use the include
directive.
types {
include "/usr/share/misc/mime.types"
}
# For certificate renewal
server "paritybit.ca" {
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location * {
block return 302 "https://paritybit.ca$REQUEST_URI"
}
}
server "paritybit.ca" {
listen on * port 8080
location * {
block return 302 "https://www.paritybit.ca$REQUEST_URI"
}
}
# WWW.PARITYBIT.CA
server "www.paritybit.ca" {
listen on * port 8080
root "/paritybit.ca"
location "/" {
request rewrite "/html/home.html"
}
location match "/.*%.html" {
request rewrite "/html/$REQUEST_URI"
}
location match "/([^%.]+)$" {
request rewrite "/html/%1.html"
}
}
server "www.paritybit.ca" {
listen on * port 80
location * {
block return 302 "https://www.paritybit.ca$REQUEST_URI"
}
}
# FTP.PARITYBIT.CA
server "ftp.paritybit.ca" {
listen on * port 8080
root "/ftp.paritybit.ca"
directory auto index
}
server "ftp.paritybit.ca" {
listen on * port 80
location * {
block return 302 "https://ftp.paritybit.ca$REQUEST_URI"
}
}
In the above configuration, there are two location match
directives in the
www.paritybit.ca server. The first matches any
request for a path ending in .html
and rewrites the request to serve the
webpages from the html
subdirectory as opposed to trying to find them in the
root folder of the website.
The second matches any request which doesn’t have a file extension and appends
.html
to the requested resource path. This allows me to replicate nginx’s
try_files
command where one can tell it to search for files which look like
$DOCUMENT_URI.html
and it means that users don’t have to type out the .html
extension when visiting a page on my site.
Below is my relayd configuration. I run multiple services from one IP so I need to reverse proxy incoming connections to various services on my network. As with nginx’s reverse proxying, relayd can handle the TLS connections to each of my services. I could also reverse proxy the connections to port 80 and redirect them using relayd, but I felt it was simpler to just let the webserver handle those directly.
The reverse proxy for Gemini at the bottom of the configuration is just for accessing it within my network because of my internal DNS configuration.
ext_addr = 10.0.0.20
table <pleroma> { 10.0.0.7 }
table <git> { 10.0.0.11 }
table <matrix> { 10.0.0.16 }
table <www> { 127.0.0.1 }
table <gemini> { 10.0.0.21 }
# TLS proxy all home services
http protocol "httpsproxy" {
tcp {nodelay, sack, backlog 128}
tls keypair "paritybit.ca"
return error
match header set "X-Client-IP" \
value "$REMOTE_ADDR:$REMOTE_PORT"
match header set "X-Forwarded-For" \
value "$REMOTE_ADDR"
match header set "X-Forwarded-By" \
value "$SERVER_ADDR:$SERVER_PORT"
match response header remove "Server"
match response header set "X-Frame-Options" \
value "SAMEORIGIN"
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 "Referrer-Policy" \
value "strict-origin"
match response header set "Content-Security-Policy" \
value "default-src 'none'; \
base-uri 'self'; \ form-action 'self' https://duckduckgo.com/; \
img-src 'self' data: https:; \
media-src 'self' https:; \
style-src 'self' 'unsafe-inline'; \
font-src 'self'; \
script-src 'self' 'unsafe-inline'; \
connect-src 'self' wss://pleroma.paritybit.ca; \
upgrade-insecure-requests;"
match response header set "Strict-Transport-Security" \
value "max-age=31536000; includeSubDomains"
match response header set "Permissions-Policy" \
value "accelerometer=(none), camera=(none), \
geolocation=(none), gyroscope=(none), \
magnetometer=(none), microphone=(none), \
payment=(none), usb=(none), \
ambient-light-sensor=(none), autoplay=(none)"
pass request quick header "Host" value "git.paritybit.ca" \
forward to <git>
pass request quick header "Host" value "matrix.paritybit.ca" \
forward to <matrix>
pass request quick header "Host" value "pleroma.paritybit.ca" \
forward to <pleroma>
pass request quick header "Host" value "ftp.paritybit.ca" \
forward to <www>
pass request quick header "Host" value "www.paritybit.ca" \
forward to <www>
pass request quick header "Host" value "paritybit.ca" \
forward to <www>
block
}
relay "reverseproxy" {
listen on $ext_addr port 443 tls
protocol httpsproxy
forward to <git> port 80 check http "/" code 200
forward to <matrix> port 8008 check http "/" code 302
forward to <pleroma> port 8080 check http "/" code 400
forward to <www> port 8080 check http "/" code 302
}
#For Matrix
http protocol "matrix" {
tcp {nodelay, sack, backlog 128}
tls keypair "paritybit.ca"
return error
match header set "X-Client-IP" \
value "$REMOTE_ADDR:$REMOTE_PORT"
match header set "X-Forwarded-For" \
value "$REMOTE_ADDR"
match header set "X-Forwarded-By" \
value "$SERVER_ADDR:$SERVER_PORT"
pass
}
relay "matrixrevprox" {
listen on $ext_addr port 8448 tls
protocol matrix
forward to <matrix> port 8008 check tcp
}
relay gemini {
listen on $ext_addr port 1965
forward to <gemini> port 1965 check tcp
}
There is a lot of extra configuration for the HTTP services for setting things like Content Security Policy and other security headers (what a mess the Web has become…). I used the Pleroma installation guide for OpenBSD as a reference for the CSPs needed for that service.
As usual, the tools provided by the OpenBSD developers are a breeze to configure and administrate. Plus the comprehensive, accurate, and complete documentation provided with the system means that I don’t have to scour the internet for help only to find outdated tutorials or complicated documentation.