I want to clearly state this is 0 dependencies on the server. The server does not need AsciiDoc installed to operate. AsciiDoc is for the client to use. The server operates with 0 `pkg_add`s

I have fallen in love with a recent combination of software to make good looking websites, and having an easy to manage web server. I’m a minimalist in many ways. Really, I find that it makes my life easier. I like to keep my blog up here and don’t want to deal with database updates, language exploits, weird migrations, and so on. I also manage my church’s website. I can’t be having complex solutions that require large amounts of maintenance at multiple times a month because of a dozen pieces of software requiring version upgrades or what have you. Sure, some people have needs for elaborate content management systems, but sometimes you only need a simple site too, and why overcomplicate it?

This is why I like my current combo:

  1. OpenBSD

  2. AsciiDoc

OpenBSD is a simple operating system with simple management needs. I also like AsciiDoc because it is very easy to write content with (like right now for this blog), the output looks nice and clean, and it’s basic script away from being simple HTML/CSS.


Why do I like OpenBSD for this job? I like OpenBSD because it comes with batteries included

  • webserver

  • firewall

  • acme client for SSL certs

  • openrsync for backups

I also like its strong focus on security, as well as how easy it is to maintain the server with the way I am demonstrating here. Really, the only commands someone has to know to do upkeep is:

  1. syspatch

  2. reboot

  3. sysupgrade

Next, I will talk about how I configured my server to run a simple, static site.

Firewall Configuration

Firewall configuration only requires you edit the /etc/pf.conf file. This is what I needed to make my webserver work.

table <bruteforce> persist

set skip on lo0

block in log all
pass out on egress

pass in on egress inet proto tcp from any to (egress) port { http, https } keep state
pass in on egress inet6 proto tcp from any to (egress) port { http, https } keep state

pass proto tcp from any to (egress) port ssh \
     keep state (max-src-conn 15, max-src-conn-rate 5/3, \
     overload <bruteforce> flush global)

pass in on egress inet6 proto icmp6 all icmp6-type { echoreq routeradv neighborsol neighboradv }
pass in on egress inet proto icmp icmp-type { echoreq unreach }

block return in on ! lo0 proto tcp to port 6000:6010
block return out log proto {tcp udp} user _pbuild

Enable these rules with pfctl -f /etc/pf.conf

acme-client configuration

OpenBSD comes with a built-in acme client that you can use to generate new SSL certificates. I get mine from Let’s Encrypt. I then create a cron job under the root user to run every so often to see if there is a renewal that can happen, and then reload.

Here is my configuration for the /etc/acme-client.conf file

domain {
       domain key "etc/ssl/private/"
       domain certificate "/etc/ssl/"
       domain full chian certificate "/etc/ssl/"
       sign with letsencrypt

Then I add the cron job as the root user with crontab -e

30 0 * * 1 acme-client && rcctl reload httpd

Webserver Configuration

I use OpenBSD’s built in HTTP server, which does its job well. They have a file at /etc/examples/httpd.conf that you can use and mock up. Here is mine

server "" {
       listen on * tls port 443
       hsts preload
       tls {
           certificate "/etc/ssl/"
           key "/etc/ssl/private/"
           ciphers "HIGH:!aNULL"

       location "/*" {
           root "/htdocs/"

With the SSL cert and the web server configuration set, you can start and enable the server:

rcctl enable httpd; rcctl start httpd


You can use doas to make superuser configurations without always being root. Edit the doas.conf file and add

permit persist <username> as root


So, you might be asking why I chose AsciiDoc. I like AsciiDoc because it is a simple markup language that lets me focus on my writing and layout. Markdown can do this too, and is more common than AsciiDoc. However, the fact that there isn’t a standard Markdown and I don’t like any of the rendered output. With AsciiDoc, they give a simple and clean default result in HTML with CSS. I also like that there are other themes I can use readily, or I can make my own. But I end up using the default theme and the dark theme that is available.

With this in mind, it makes it easy for anyone to see what is written and figure out what does what since the syntax is simple. And, if you use editors like Visual Studio Code or AsciiDocFX, you get a nice preview. For me, I use vim. Here is an example of what the Webserver Configuration section looks like:

=== Webserver Configuration

I use OpenBSD's built in HTTP server, which does its job well. They have a file at `/etc/examples/httpd.conf`
that you can use and mock up. Here is mine

server "" {
       listen on * tls port 443
       hsts preload
       tls {
           certificate "/etc/ssl/"
           key "/etc/ssl/private/"
           ciphers "HIGH:!aNULL"

       location "/*" {
           root "/htdocs/"

With the SSL cert and the web server configuration set, you can start and enable the server:

`rcctl enable httpd; rcctl start httpd`
The source section starts with a \ so as to not interpret it literally here :)

CSS Styles

To get the best out of CSS styles if I am not using the builtin, I like to do what I am doing here for my site:


How I Generate My AsciiDoc

I use a simple script to pull together my content, as well as generate my AsciiDoc into HTML format. I also want to note that I did create some extra custom HTML for my page to be inserted so I could offer a navbar, since in some cases I have come by the Table of Contents feature of AsciiDoc is good, but other times people wanted a standard navbar.

#!/usr/bin/env sh


rm -rf "$DEST"
mkdir "$DEST"

# For any images you may have
cp -r "$CWD/images" "$DEST/"
# For the CSS to use with adoc
cp -r "$CWD/css" "$DEST/"
# In case you add any extra JS to run
cp -r "$CWD/js" "$DEST/"
# Any custom HTML you may insert into your adoc files
cp -r "$CWD/asciidoc/html" "$DEST/"
# I had a zipfile of favicons, this could just be changed do copying a favicon.ico
unzip "$CWD/" -d "$DEST/"

# Generate index file
asciidoctor -D "$DEST" "$CWD/asciidoc/index.adoc"

adocfiles=$(find asciidoc -type f -name "*.adoc" | xargs)

for f in $adocfiles; do
    relative_path=$(echo "${f%/*}/")
    mkdir -p "$CWD/$relative_path"
    asciidoctor -D "$DEST/$relative_path" $f

cd "$DEST"
mv asciidoc/* ./
rmdir asciidoc
cd -

So, I will be in my terminal at this point and be in the root of my working path, where I have my asciidoc folder and my script. Then, when I want to, I run sh ./ to generate the site. At that point, I have a newly made dir.

Uploading Site Content

With a created site, it is time to upload your work. I tend to use sftp or scp to do this. You could even do rsync if you wanted.

Swapping in Your New Site

Now with your content uploaded, presumably to your home directory, you can either run a few commands, or use a script like this to

  1. Change ownership of the new website content

  2. Change permissions

  3. Move the old site

  4. Move the new one

Something like this, assuming you have your web content at $HOME

#!/usr/bin/env sh

chown -R root:wheel $HOME/
find $HOME/ -type f -exec chmod 644 {} \;
find $HOME/ -type d -exec chmod 755 {} \;
mv /var/www/htdocs/ /var/www/htdocs/$(date "+%Y-%m-%d-%H%M%S")
mv $HOME/ /var/www/htdocs

This will move the old site away as a backup, then put the new one in place.

Wrap Up

With all of this, if you can install OpenBSD, you can make a simple solution for basic static websites that are also easy to create since you don’t have to learn any (or very little) HTML, no CSS, no heavy site creator to get in the way, and little command line knowledge to configure and maintain.


I had previously mentioned backups, so I won’t forget this one. You may find backups "unnecessary" since you should theoretically always have your site content on a local computer. However, if you want to backup your configurations and such, you can accomplish this with scp or openrsync.

Even though I don’t have any secret content, I do like to encrypt my backups. So this is something you can do when creating yours.

This is more or less what my backup script looks like:

#!/usr/bin/env sh

key="$(cat $keyfile)"

name="$backupdir/$(date +'%F_%T')_<optional backup name here>"
mkdir -p $name
mkdir -p $name/etc

# copy files to backup directory

cp /etc/{$etc_files} $name/etc/


for file in $other_files; do
	dir=$(echo "${file%/*}")
	mkdir -p $bkp_dir
	cp $file $bkp_dir/

cp -r /var/www/htdocs $name/

# Compress and encrypt
tar -czf $name.tar.gz $name
openssl enc -aes-256-cbc -md sha512 -pbkdf2 -iter 100000 -salt -in $name.tar.gz -out $name.tar.gz.enc -pass file:$keyfile

# Clean up
rm $name.tar.gz
rm -rf $name

result=$(openrsync -a --delete $backupdir 2>&1 > /dev/null)

# Delete files older than 30 days
find $backupdir -mtime +30 -delete

Bonus Bonus

I like to know when my backups are working. So, I want to get notified when a backup is failing. In the previous backup script, you can insert a conditional to say "if the backup happened, submit a pass. If it didn’t for some reason, send a fail". I do this to NodePing with their PUSH checks. Which, if you can make a POST request and format some JSON, you can submit any data.

My method has been to just submit an empty JSON object {"data":{}} to accomplish my needs. One issue though, doing a POST request with OpenBSD from just base isn’t quite as cut and dry. I reached out, and the 2 most common answers were to write some Perl, which, that’s too much for me, or use nc(1). I opted to use nc.

I created a PUSH check on NodePing and got the checktoken and checkid for the POST request. The nc command looks like:

head='POST /v1?id=<checkid>&checktoken=<checktoken> HTTP/1.1\r\nHost:\r\nContent-Type: application/json\r\nContent-Length: 11\r\n\r\n{"data":{}}'
echo -ne $head | nc -c 443

Bonus X3

I appreciate knowing when a syspatch is available. OpenBSD provides a handy syspatch -c flag that will output any patches available. I submit this info to my Matrix server, but of course you could create another NodePing check or anywhere else you want to do a POST request with any sort of data to a service that will notify you.

My script basically looks like


if [ ! -z "$(syspatch -c)" ]; then
	body="{\"body\":\"$(hostname) syspatch available\"}"
	contentlength=$(expr $(echo "$body" | wc -m) - 1)

	head="POST /web/path/post/somewhere HTTP/1.1\r\nHost:\r\nContent-Type: application/json\r\nContent-Length: $contentlength\r\n\r\n$body"

	echo -ne "$head" | nc -c 443

Next create a daily cron job. We can call this /root/

0 2 * * * /bin/sh /root/

Now you should be notified if there are any updates available for your OpenBSD release.


Now, you have:

  • A running OS with high security

  • Easy OS management, really only needing to run syspatch, reboot, and sysupgrade

  • A nice, clean static website that really only requires simple syntax to know to write

  • Backups with openrsync

  • Backup notifications with NodePing and nc (definitely optional)

  • Update notices

Since it’s not often that a website update happens on some of my sites, I really only need to keep track of running syspatch and upgrading if a new release comes out. Then, any updates that DO need to happen are a basic edit, compile, upload, and put the new site content in place.

EDIT 2022-06-29

I didn’t state clearly enough that my 0-dependency claim is you don’t need to install anything on the server. The server does not need AsciiDoc. The client will, but my point is that the server doesn’t need any pkg_add and thus, only requires syspatch, reboot, and sysupgrade to maintain the host, and no needing to worry about maintaining other software on top of the OpenBSD base install. :)