Hosting a HUGO site on OpenBSD.Amsterdam with OpenBSD, httpd, and relayd


Intended Audience

This guide is intended for anyone who wishes to setup a hugo website runing on OpenBSD using httpd and relayd.

Document Scope

It is assumed that you have some basic UNIX admin skills and that you have already setup an OpenBSD server to your liking. General setup and administration is outside the scope of this document. Instead, we will focus on setting up OpenBSD’s httpd server, relayd, SSL certificates, and so on.

OpenBSD.Amsterdam - A short plug for my hosting provider

First, I would like to say that I wasn’t asked do this nor do I get anything from anyone to say this. This server is hosted on OpenBSD.Amsterdam. They have really been a pleasure to work with, and they give back to The OpenBSD Foundation and at under 6 euros per month it’s a great value. If you really care about OpenBSD and want to see the project prosper, then I think donations or using services where part of your money goes to the project is a great start.

OpenBSD Amsterdam gives back 10 euros per VM and 15 euros per VM on every renewal to The OpenBSD Foundation.

OpenBSD Amsterdam - https://openbsd.amsterdam

The OpenBSD Foundation - https://www.openbsdfoundation.org

acme-client, httpd, and relayd

The OpenBSD project has some beautifully designed software and tools. One lovely piece of software is httpd. It is a very simple, very lightweight http server which is based on relayd. It has basic functionality and isn’t too feature rich. It’s lack of features is intentional. This is why we will need to pair it with relayd. Many things can be accomplished with relayd. We will be using it to manage headers and so on. This will be discussed later in this document. The acme-client is OpenBSD’s own client for generating and keeping Let’s Encrypt certificates up-to-date. We will also be discussing this later in this document.

For now let’s have a look at the official documentation:

httpd manpage - https://man.openbsd.org/httpd.8

httpd.conf manpage - https://man.openbsd.org/httpd.conf.5

relayd manpage - https://man.openbsd.org/relayd.8

relayd.conf manpage - https://man.openbsd.org/relayd.conf.5

acme-client manpage - https://man.openbsd.org/acme-client.1

acme-client.conf manpage - https://man.openbsd.org/acme-client.conf.5

Required Software

Most of the required software is in OpenBSD base ( httpd, relayd ).

hugo–extended

You will need to install hugo. We will be setting it up and configuring it later but let’s go ahead and get the prerequisites done.

/usr/sbin/pkg_add -vi hugo--extended

httpd

Fire up your favorite editor ( I will be discussing in other articles why you should be using VI ) and edit the /etc/httpd.conf file. Here you will want to enter the basics just to test your setup.

server "example.org" {
  listen on * port 80
  root "/htdocs/www.example.org"
}

server "www.example.org" {
  listen on * port 80
  block return 301 "http://www.example.org$REQUEST_URI"
}

Save your config.

The httpd daemon is chrooted to /var/www by default. It is there where we will need to create the document root directory structure.

mkdir -p /var/www/htdocs/example.org

Let’s check our configs to make sure they are sane.

httpd -n

You should see something like: configuration ok

The httpd daemon can be enabled and started as follows:

rcctl enable httpd
rcctl start httpd

You may try placing an index.html file or something in your document root structure to test if the server is working. That is actually the only reason for this portion of the document. To test functionality of the httpd sever and to ensure that your PF rules are allowing traffic. Ideally you will want to bind the httpd server to specific interfaces / IPs and we will do that later.

acme-client

Next we will be getting those sweet SSL certificates so that our site can actually be useful in the 21st century. For that we will be using OpenBSD’s easy to use acme-client.

As aforementioned we will not be covering setting up DNS in this guide. That said please double-check to make sure that you have proper CAA records setup for your domain. These CAA records allow your domain SSL certs to be managed by Let’s Encrypt.

example.org. 300 IN   CAA   0 issue "letsencrypt.org"

You may check your domain as follows:

/usr/bin/dig CAA example.org @9.9.9.9

Let’s edit our /etc/acme-client.conf with our favorite editor and setup something like this:

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

domain example.org {
	domain key "/etc/ssl/private/example.org.key"
	domain full chain certificate "/etc/ssl/example.org.fullchain.pem"
	sign with letsencrypt
}

Create the directory structure:

    mkdir -p -m 755 /var/www/acme

Update the /etc/httpd.conf file to handle verification requests from Let’s Encrypt. It should be similar to this: NOTE: We are moving our listen port to localhost so you will need to create a rdr rule in your pf.conf.

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

server "example.org" {
	listen on 127.0.0.1 port 80
	location "/.well-known/acme-challenge/*" {
		root "/acme"
		request strip 2
	}
	root "/htdocs/example.org/public"
	directory auto index
}

### http -> https ###
server "example.org" {
	listen on 127.0.0.1 port 80
	block return 301 "https://$HTTP_HOST$REQUEST_URI"
}

Check the configuration and restart httpd:

# httpd -n
configuration ok
# rcctl restart httpd
httpd(ok)
httpd(ok)
#

Now, let’s run the acme-client to create new account and domain keys.

# acme-client -v example.org
...
acme-client: /etc/ssl/www.example.crt: created
acme-client: /etc/ssl/www.example.pem: created
#

To renew the certificates automagically we create a script /usr/local/sbin/acme.sh with the following contents:

#!/bin/ksh
/usr/sbin/acme-client example.com 2>/dev/null 2>&1 && /usr/sbin/rcctl reload httpd 2>/dev/null 2>&1

NOTE: It might be a good idea to add a logger entry or some sort of logging to know how things are going.

Then we add it a cronjob to keep the SSL certs up-to-date:
### Keep SSL Certs Up-To-Date ### 
0       0       *       *       *       /usr/local/sbin/acme.sh

relayd

Now that we have our SSL certs we can actually setup httpd.conf to communicate with relayd ( where our actual ports 443 and 80 will be listening ).

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

server "example.org" {
	listen on 127.0.0.1 port 8080
	location "/.well-known/acme-challenge/*" {
		root "/acme"
		request strip 2
	}
	root "/htdocs/example.org/public"
	directory auto index
}

### http -> https ###
server "example.org" {
	listen on 127.0.0.1 port 8181
	block return 301 "https://$HTTP_HOST$REQUEST_URI"
}

Now we setup relayd.conf as follows:


EXT4 = "XX.XX.XX.XX" ### <-- INET4 INTERFACE ( YOUR IP GOES HERE )
#EXT6 = "XXXX:XXXX:XXXX:XXX:X:X:X:XXX" ### <-- INET6 INTERFACE

table <LOCAL> { 127.0.0.1 }

log state changes
log connection

http protocol "HTTP_FILTER" {

 ### DEBUG ###
 # Uncomment during testing to return HTTP 403 Forbidden when request is blocked.
 # Comment out and relayd will close connection with TCP FIN ( for scanners and crap ) 
 #return error

 http headerlen 4096
 tls ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256"
 tls edh
 tls keypair "example.org"
 match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
 match request header set "X-Forwarded-SPort" value "$REMOTE_PORT"
 match request header set "X-Forwarded-DPort" value "$SERVER_PORT"
 match response header remove "Server" value "*"
 match header log "Host"
 match header log "X-Forwarded-For"
 match header log "User-Agent"
 match header log "X-Req-Status"
 match url log
 match response header set "Referrer-Policy" value "same-origin"
 match response header set "Referrer-Policy" value "no-referrer"
 match response header set "X-Frame-Options" value "deny"
 match response header set "X-XSS-Protection" value "1; mode=block"
 match response header set "Content-Security-Policy" value "default-src 'none'; style-src 'self'; img-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"
 match response header set "Permissions-Policy: microphone=(), camera=()"
 match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
 match response header set "X-Content-Type-Options" value "nosniff"
 match response header set "Cache-Control" value "max-age=86400"
 match request tag "BAD_METHOD"
 match request method GET tag "OK_METH"
 match request method HEAD tag "OK_METH"
 block request quick tagged "BAD_METHOD"
 match request header "Host" value "example.org" tag "OK_REQ"
 match request header "Host" value "www.example.org" tag "OK_REQ"
 block request quick tagged "OK_METH" tag "BAD_HH"
 block tag "BAD_REQ"
 pass request tagged "OK_REQ"
}

relay "IPV4_WEB_HTTPS" {
 listen on $EXT4 port 443 tls
 protocol "HTTP_FILTER"
 forward to <LOCAL> port 8080
}

### IP6 ###
#relay "IPV6_WEB_HTTPS" {
# listen on $EXT6 port 443 tls
# protocol "HTTP_FILTER"
# forward to <LOCAL> port 8080
#}

relay "IPV4_WEB_HTTP" {
 listen on $EXT4 port 80 
 protocol "HTTP_FILTER"
 forward to <LOCAL> port 8181
}