OpenBSD - Configure a Static Website

Using relayd, httpd, pf and acme-client - the openbsd stack

If you need to deploy a static site to your OpenBSD, and if you have relayd and httpd as your stack, then this post lists the config and commands needed to get the website up and running.

No wait. I could care less about you. This post is for me in case I forget things about my websites and servers. It may or may not help you. Spend more time reading at your own risk.

Domain Setup

Point the new domain to your server’s IP.

It can be done in 2 ways:

1) The My Way

It’s by changing the domain’s nameservers to point to that of the server’s hosting provider, and then managing the DNS from the latter.

If my domain is in NameCheap.com and my server is in Vultr.com, then I’d go to NameCheap.com and change my domain’s nameservers to these:

ns1.vultr.com
ns2.vultr.com

And then I’d go to the DNS section of Vultr.com and then “Add a New Domain”. It’ll ask to link the domain to a server that’s already there. Do it.

Then it’ll automatically create an A record for the domain pointing to the server’s IP. You can then create other DNS records for the domain from here as well. I create my CNAMEs here. I don’t use the wildcard CNAME record. I create one for each subdomain I want, explicitly.

Using this way means I can see the link between the domains and the servers closely.

2) The Other Way

In NameCheap.com, create an A record for the domain pointing to the server’s IP. And create the CNAME’s too there if you need them. That’s it. This looks easy, but I buy domains from many places depending on the price, and then use vultr od DigitalOcean only for servers. So getting used to a single interface to do DNS things saves time.


Relayd Setup

I use relayd to listen to traffic from the external world and then point them to the httpd server process that’s running at localhost. Here’s the relayd setup.

The /etc/relayd.conf file:

log state changes
log connection errors
prefork 5

table <httpd> { 127.0.0.1 }

http protocol "https" {
  tls keypair "example.com"
  return error
  match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
  match request header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"

  # test in https://securityheaders.com
  match response header remove "Server"
  match response header append "Strict-Transport-Security" value "max-age=31536000"
  match response header append "X-Frame-Options" value "SAMEORIGIN"
  match response header append "X-XSS-Protection" value "1; mode=block"
  match response header append "X-Content-Type-Options" value "nosniff"
  match response header append "Referrer-Policy" value "strict-origin"
  match response header append "Content-Security-Policy" value "default-src https: 'unsafe-inline'"
  match response header append "Permissions-Policy" value "accelerometer=(none), camera=(none), geolocation=(none), gyroscope=(none), magnetometer=(none), microphone=(none), payment=(none), usb=(none)"

  pass request quick header "Host" value "example.com" forward to <httpd>
}

relay "https" {
  listen on 0.0.0.0 port 443 tls
  protocol https
  forward to <httpd> port 8080
}

What it does? - listens on the external interface on the ssl port for incoming traffic - tells relayd to look for the tls private key and certificate at the standard location (/etc/ssl) and the filenames being that of the domain’s name. - sets some standard security headers in the response - forwards the request to the httpd server process listening on port 8080 (we’ll see it configured that way below)


Httpd Setup

Here’s /etc/httpd.conf:

prefork 5

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

server "default" {
  listen on 127.0.0.1 port 8080
  block drop
}

include "/etc/httpd.d/example.com.conf"

And here’s /etc/httpd.d/example.com.conf:

server "example.com" {
  listen on * port 80

  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }

  location * {
    block return 301 "https://$HTTP_HOST$REQUEST_URI"
  }
}

server "example.com" {
  listen on 127.0.0.1 port 8080

  root "/htdocs/example.com"
  gzip-static
  default type text/html
}

What it does? - creates a server process that’s listening for traffic only from localhost on port 80800 - forces http requests to come back as https

NOTE: Make sure the /var/www/htdocs/example.com directory is created and chown’ed to your user, and chmod’ed to 755.


HTTPS Certificate management with acme-client

Here’s /etc/acme-client.conf:

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"
}

include "/etc/acme-client.d/example.com.conf"

And here’s /etc/acme-client.d/example.com.conf:

domain example.com {
  alternative names { www.example.com }
  domain key "/etc/ssl/private/example.com:443.key"
  domain full chain certificate "/etc/ssl/example.com:443.crt"
  sign with letsencrypt
}

Automatic Cert Renewal Management

Now you need to create new certificate for the domain with Let’s Encrypt.

Here’s the command:

acme-client example.com

To renew it automatically, put this in a script and add to root’s crontab to run it weekly.

Here’s the script at /usr/local/sbin/cert-renew:

#!/bin/sh
acme-client example.com
rcctl restart relayd

And here’s the line you need to put into root’s crontab:

11      3       *       *       5       /usr/local/sbin/cert-renew

Add this to cron using crontab -e. This will run the renewal script every Friday at 3:11 am.

Firewall management with PF

Make sure only ports 22 (ssh), 80 (http), 443 (https) are open for traffic and all others are closed.

Here’s /etc/pf.conf:

# network interface
if="vio0"

# skip filtering on loopback interface. It's trusted.
set skip on lo

# block all incoming traffic
block drop all

# allow outgoing traffic only on vio0
pass out on $if

# This rule allows incoming TCP traffic to the vio0 interface on specific ports (22, 80, 443).
# The flags S/SA part means that it will only pass packets with the SYN or SYN-ACK flags set, which are part of the TCP handshake process.
# The keep state part means that PF will keep track of the state of the connections and allow related packets through without needing additional rules.
# 22: SSH (Secure Shell)
# 80: HTTP (Hypertext Transfer Protocol)
# 443: HTTPS (Hypertext Transfer Protocol Secure)
pass quick proto tcp from any to $if port { 22, 80, 443 } flags S/SA keep state

Restart the services

Restart relayd and httpd with:

doas rcctl restart relayd httpd

Congrats on the new website!