https://citizen428.net/blog/self-hosting-static-site-openbsd-httpd-relayd/ * Home * About * Publications * Talks Self-hosting a static site with OpenBSD, httpd, and relayd Jul 08, 2022 - openbsd --------------------------------------------------------------------- My blog gets generated with Hugo, which I'm generally happy with. Until recently, I hosted the static files on Netlify but now decided to get my own little server again. There are two main reasons for this: 1. I actually missed doing some sysadmin work. 2. The Internet was supposed to be a federated system and I don't want to outsource everything to a few tech giants. Operating system choice OpenBSD has always been one of my favorite (server) operating systems, for reasons that are nicely summarized at Why OpenBSD rocks. Most people seem to be drawn to it because of the promise of enhanced security (which others find debatable) but I primarily enjoy it for being a simple yet pretty full-featured system out of the box. So when I decided to go back to hosting my own site, I went for a VM hosted with OpenBSD Amsterdam who donate part of each subscription to the OpenBSD Foundation. SSL certificates Since I didn't mind a short downtime for my personal blog, I started by pointing my A and AAAA records at their new IPs and added a CAA record for Let's Encrypt: $ dig citizen428.net CAA +short 0 issue "letsencrypt.org" OpenBSD comes with its own acme-client, configured via /etc/ acme-client.conf (man page): authority letsencrypt { api url "https://acme-v02.api.letsencrypt.org/directory" account key "/etc/acme/letsencrypt-privkey.pem" } domain citizen428.net { alternative names { www.citizen428.net } domain key "/etc/ssl/private/citizen428.net.key" domain full chain certificate "/etc/ssl/citizen428.net.crt" sign with letsencrypt } First, we set up Let's Encrypt as a certificate authority, then we set up the certificates for my domain. Note the line that says domain full chain certificate "/etc/ssl/citizen428.net.crt" , we'll circle back to that in a bit. Web server OpenBSD also comes with its own minimal web server, httpd. It's very easy to configure (man page) and my initial configuration looked something like this: server "citizen428.net" { listen on * port 80 location * { block return 301 "https://$HTTP_HOST$REQUEST_URI" } } server "citizen428.net" { listen on * tls port 443 tls { certificate "/etc/ssl/citizen428.net.pem" key "/etc/ssl/private/citizen428.net.key" } hsts log style combined root "/htdocs/citizen428.net" location "/.well-known/acme-challenge/*" { root "/acme" request strip 2 } } This redirects all HTTP traffic to HTTPS and configures the SSL certificates and some other things, like the document root and the location for the Let's Encrypt HTTP challenge. So far so good, this setup scored an A+ at SSL Labs. Alas, the result on Security Headers was a lot less positive, I think my initial score was a D or something. Adding a relay Since httpd is kept simple on purpose, it doesn't allow us to set the relevant security headers. Enter relayd, "a daemon to relay and dynamically redirect incoming connections to a target host." We can use this to terminate TLS, forward requests to a local web server listening on port 8080, and set some response headers (abbreviated for clarity). Here's my /etc/relayd.conf (man page): include "/etc/ips.conf" table { 127.0.0.1 } http protocol https { tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:..." tls keypair "citizen428.net" match request header append "X-Forwarded-For" value "$REMOTE_ADDR" match request header append "X-Forwarded-Port" value "$REMOTE_PORT" match response header set "Referrer-Policy" value "same-origin" match response header set "X-Frame-Options" value "deny" 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 "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload" match response header set "Content-Security-Policy" value "default-src 'none'; ..." match response header set "Permissions-Policy" value "accelerometer=(), .." match response header set "Cache-Control" value "max-age=86400" return error pass } relay wwwtls { listen on $ipv4 port 443 tls protocol https forward to port 8080 } relay www6tls { listen on $ipv6 port 443 tls protocol https forward to port 8080 } Of course, we also have to reconfigure httpd. We no longer terminate TLS and we need to listen on port 8080 where relayd will be relaying to: server "citizen428.net" { listen on 127.0.0.1 port 8080 root "/htdocs/citizen428.net" location "/.well-known/acme-challenge/*" { root "/acme" request strip 2 } } # Redirect www to naked domain server "www.citizen428.net" { listen on 127.0.0.1 port 8080 block return 301 "$HTTP_HOST$REQUEST_URI" } # Redirect http to https server "citizen428.net" { alias "www.citizen428.net" listen on * port 80 block return 301 "$HTTP_HOST$REQUEST_URI" } (Re-)start both services and bingo, we are scoring an A on Security Headers again. However, there was one small issue, my SSL Labs score was now capped at a B, with the following explanatory message: This server's certificate chain is incomplete. Grade capped to B No bueno, what to do? After some googling I found this blog post which pointed me in the right direction: relayd looks for certificate chains in /etc/ssl/private/name:port.key and /etc/ssl/name:port.crt, falling back to /etc/ssl/private/name.key and /etc/ssl/name.crt respectively. My original acme-client.conf did save the full chain with a .pem extension, whereas the .crt file only contained the certificate for the specific domain. There probably would have been more elegant ways to solve this, but the easiest solution was to just store the full chain in the .crt file as mentioned above: domain full chain certificate "/etc/ssl/citizen428.net.crt" Compressed HTTP responses A couple days after my OpenBSD setup went live, I was delighted to find out that httpd can serve compressed files since OpenBSD 7.1. So I added an extra step to my Makefile which I also use to deploy the site with rsync: clean: rm -r public build: clean hugo gzip: build find public/ -type f ! -name '*.png' -exec gzip -9k "{}" \; ... .PHONY: serve clean build gzip deploy The find command looks for everything that's not a PNG, compresses it with the highest compression (-9) and keeps the original file around (-k). To make use of this we need to update httpd.conf to include the gzip-static directive: server "citizen428.net" { listen on $local port 8080 root "/htdocs/citizen428.net" gzip-static # Newly added location "/.well-known/acme-challenge/*" { root "/acme" request strip 2 } } Odds and ends Since I don't want to worry about forgetting certificate renewals, I added the following to /etc/daily.local (man page): next_part "Refreshing Let's Encrypt certificates" acme-client citizen428.net && rcctl reload relayd This will check if a new certificate is available and restart relayd (where we're terminating TLS connections) if necessary. I also set up a very basic firewall with pf, primarily for blocking SSH brute-force attempts: table persist set skip on lo block quick from block return # block stateless traffic pass # establish keep-state pass quick proto tcp from any to any port ssh \ flags S/SA keep state \ (max-src-conn 5, max-src-conn-rate 5/30, \ overload flush global) # Port build user does not need network block return out log proto {tcp udp} user _pbuild Last but not least, I added a .forward file (man page) in my user's home directory so the mails generated by /etc/daily and the daily security scan get forwarded to my real address where I'm more likely to actually read them. Summary Overall this was a fairly quick and painless migration, and I'm very happy with the outcome. Not only am I now fully in charge of my own site again, it still scores an A+ on SSL Labs, an A on Security Headers, and an A+ on the Mozilla Observatory. It's also still eligible for HSTS preload and generally scores well on PageSpeed Insights. Not too shabby for a little VM without a CDN. :-) Resources The following blog posts came in handy at various times and while I already credited some of them in the text I wanted to explicitly list them here one more time: * 0 Dependency Websites with OpenBSD & AsciiDoc * Set up a web server with httpd(8) and relayd(8) on OpenBSD * OpenBSD, Let's Encrypt, and Full Certificate Chains * Reduce httpd web server bandwidth usage by serving compressed files Feedback Discuss on Twitter or send me an email. --------------------------------------------------------------------- previous pageEasy assertion helpers in Go FizzBuzz in GNU makenext page --------------------------------------------------------------------- (c) 2018-2022 Michael Kohl | Original Content CC BY-NC-SA | Font Awesome Icons CC BY Hosted on OpenBSD Amsterdam.