httpd and relayd on OpenBSD

I wanted to host some websites on an OpenBSD VM. I had some static content as well as some web apps. I also wanted to handle traffic for certain well-known paths separately (for Let's Encrypt). I reviewed the manual pages and it sounded like httpd would be a good option for serving static content while relayd was what I needed for filtering and forwarding traffic to the right places.

Name resolution

In experimenting and testing my configuration, I wanted to have particular hostnames resolve to localhost so I could see how my setup was working. I learned that /etc/resolv.conf controls the search order for gethostbyname(3). On other systems I've used, this is controlled by /etc/nsswitch.conf. Unless a different order is given in /etc/resolv.conf, DNS is searched first, then /etc/hosts. I added lookup file bind to /etc/resolv.conf to have /etc/hosts searched first while I was experimenting so I could put some hostnames in /etc/hosts to have them resolve to localhost.


httpd serves static content over HTTP. It can also do FastCGI and TLS. I don't plan to use FastCGI (my web apps include their own HTTP servers) and relayd will handle TLS termination since it's out front.

I created /etc/httpd.conf and started out with a couple of macros: one for the local interface and one for the port I wanted to use for HTTPS redirects.


I added a server section for handling ACME challenges from Let's Encrypt. This should work for any server name.

server "*" {
        listen on $local port 4220
        location "/.well-known/acme-challenge/*" {
                root "/acme"
                request strip 2
        location "*" {
                block drop

I also added a server section for a static site I wanted to serve:

server "" {
        listen on port $static_sites
        root "/htdocs/"

Many clients try HTTP if no protocol is specified, and it's common for folks to type www in front of a web address out of habbit. I added a section to handle redirects to the canonical host name and protocol for these cases. The manual page says that you can use patterns in the server name and I found this to be true, but it seems like capture groups aren't currently implemented in this context. I think if I had capture groups I could simplify this part of the configuration. Until then, I think I'll need to add an additional server sections for each hostname.

server "" {
        alias ""
        listen on $local port $https_redirect
        block return 301 "$DOCUMENT_URI"

DOCUMENT_URI gives the path without any query string. If you'd like to include the query string, you might prefer REQUEST_URI.

I noticed in testing that other host names would also get the redirect shown above. It seems like the first server section for an address and port combination serves as the default when SNI doesn't get a match. To handle that case, I added a catch-all section listening on the same address and port that just drops the connection.

server "*" {
        listen on $local port $https_redirect

I needed a place to put the files for the static site and I wanted to be able to update the site by sftp without logging in as the superuser, so I created a directory and changed the ownership to a non-privileged user. The directory needs to be under /var/www because (by default) httpd chroots to /var/www.

mkdir /var/www/htdocs/
chown user.user /var/www/htdocs/

I can add more static sites to this configuration by adding additional server sections and creating the directories for them.

Something else I noticed in testing is that my browser wanted to download and save PDF files rather than open them in-browser. I figured maybe they were being served with a generic content type header. Checking the manual page page for the httpd configuration file confirmed that a few file extensions are automatically converted to specific content types by default, but PDF is not included. I added a section to the configuration file to duplicate the defaults and add an entry for PDF files and a few other file types that I commonly use. I can add additional file types to this as needed.

types {
        text/css                 css
        text/html                html htm
        text/plain               txt
        image/gif                gif
        image/jpeg               jpeg jpg
        image/png                png
        application/javascript   js
        application/xml          xml    
        image/ ico
        application/pdf          pdf   
        image/svg+xml            svg
        video/mp4                mp4

Finally, I had to start up httpd. I also wanted it to start automatically if the VM should get happen to get restarted. rcctl(8) is used to control system services, both for enabling or disabling as well as starting, stopping, reloading, and such.

rcctl enable httpd
rcctl start httpd

And if you're fussing around with the configuration, you can reload it with:

rcctl reload httpd

relayd part one

relayd can relay HTTP connections. It can do TLS termination, high-availability pools with health-checks and lots of other neat stuff. I mostly wanted to use it to relay connections to different servers depending on the host name specified by the client.

I created /etc/relayd.conf and made sure it had permissions 0600 (as suggested by security(8)).

touch /etc/relayd.conf
chmod 0600 /etc/relayd.conf

I started out /etc/relayd.conf with a couple of macros for the public and private interfaces, with that plan that I'd have relayd listen on the public interface and communicate with the servers on the private (local) interface.


I set up a few pools that all point to the private interface. I wanted separate pool names (even though they point to the same address) so I could configure each one with a different port in the relay sections.

table <acme-challenge> { $private }
table <https-redirect> { $private }
table <static-sites> { $private }
table <hitcounter> { $private }

Most incoming traffic should be HTTPS, but I also wanted to relay HTTP traffic in a couple of cases: to redirect from HTTP to HTTPS and to handle ACME challenges from Let's Encrypt. I'm used to the OpenBSD manual pages being top-notch, but I found the page for relayd.conf a little thin in a couple of spots. After some puzzling, I figured out that the filter “action” (block, match, or pass) isn't the only action that will be taken if the filter matches: several of the items listed as "filter parameters" (label, no, tag, forward to, and others) represent additional actions to be taken only if the filter matches.

I put together a filter rule to identify ACME challenges (for any host) and forward them to the httpd port I had set up for handling them. This goes into a protocol section that I can reference from the relay section.

http protocol "http" {
        pass request quick path "/.well-known/acme-challenge/*" \
                forward to <acme-challenge>

The relay section lists the port for HTTPS redirects first: this is the default. If the filter rule detects and ACME challenge, it will override this default and the connection will be relayed to the ACME challenge port.

relay "http" {
        listen on $public port http
        protocol "http"
        forward to <https-redirect> port 4221
        forward to <acme-challenge> port 4220

This was enough configuration to let me start up relayd and use it along with httpd for responding to ACME challenges.

rcctl enable relayd
rcctl start relayd


At this point, I figured I had enough of a setup to use acme-client to request certificates. At the same time, I couldn't press forward much farther in configuring relayd without certificates.

I checked with my DNS host and made sure that I had address records for each host name set up and pointing at my server.

I set up /etc/acme-client.conf with a section for Let's Encrypt and a section for each host name.

authority letsencrypt {
        account key "/etc/acme/letsencrypt-privkey.pem"
        api url ""

domain "" {
        domain key "/etc/ssl/private/"
        domain full chain certificate "/etc/ssl/"

domain "" {
        domain key "/etc/ssl/private/"
        domain full chain certificate "/etc/ssl/"

That and running acme-client for each host name was enough to get me a Let's Encrypt account and certificates.

Let's Encrypt certificates have a short expiration window to encourage automated renewal. Automation is as simple as adding a line to crontab(5) for each certificate:

28 4 * * * /usr/sbin/acme-client
28 4 * * * /usr/sbin/acme-client

I'll need to add such a line for each certificate. If I end up with a lot, maybe a small script will be a better way to go. I chose to have acme-client check my certificates every day in the early morning. A little while after the certificates are checked (and possibly updated), I ask relayd to reload its configuration file so it can pick them up (I don't think it will notice otherwise, please drop me a line if I'm mistaken).

48 4 * * * /usr/sbin/relayctl reload

relayd part two

With a certificate available, I was able to add a relay section to relayd.conf for relaying HTTPS connections. By default connections will be relayed to httpd for serving static sites. Filter rules in the protocol section can forward to web app pools based on host name or other criteria. I also added directives for the Let's Encrypt certificates to the protocol section. I can add additional certificates here if I want to serve content for more host names.

I wanted to add cache control headers for my static sites, but httpd doesn't (yet?) seem to support that. So, I added a couple more directives to the protocol stanza. The first one tags any requests that get past the directives for web apps. The second directive catches the responses for requests that were tagged (the manual page was a little fuzzy here on whether the tags stuck from request to response, but it seems to work) and sets the cache control header. This way web apps can do their own thing for cache control, but I get a little something for static sites too.

http protocol "https" {
	tls keypair ""
	tls keypair ""
	pass request quick header "host" value "" \
		forward to <hitcounter>
        match request tag "static"
        match response tagged "static" header set "cache-control" value \

relay "https" {
	listen on $public port https tls
	protocol "https"
	forward to <static-sites> port 4222
	forward to <hitcounter> port 4200

I would like to be able to compress some of my static files, particularly favicon.ico and any HTML or text documents. httpd doesn't have support for dynamic compression (and likely won't, since I guess it's a security mess), but I'm hoping for future support for static compression.

A quick reload put the configuration into effect

rcctl reload relayd

Uploading static sites

I've been using rsync to deploy static site content. On the server side, we have openrsync so it's necessary to specify the path to the remote rsync binary when calling rsync locally or to install rsync server-side using pkg_add.

rsync --rsync-path=/usr/bin/openrsync -r --delete . lucy:/var/www/htdocs/

In closing

I hope that you found this helpful. If this is the kind of thing you're into, you may enjoy my other work. If you have any questions or comments, please feel free to drop me an e-mail.

Aaron D. Parks
Parks Digital LLC
4784 Pine Hill Drive, Potterville, Michigan