Migrating from nginx to OpenBSD's httpd and relayd

Written By: Jake Bauer | Posted: 2021-02-17 | Last Updated: 2021-02-17
The OpenBSD logo.
This logo is subject to the license at: openbsd.org

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.