Using OpenBSD to Host a Website

By B.E. Alejandro


Context and motivation

I decided to migrate my website’s hosting to OpenBSD as a deliberate exercise in control, simplicity, and security. It wasn’t a decision based on performance, commercial scalability, or extreme automation, but on fully understanding the system.

OpenBSD forces you to understand every configuration file, every active service, and every permission. It doesn’t hide complexity: it exposes it. That was the central motivation behind this migration.

The goal was clear: an auditable, predictable, and minimal server.

Infrastructure and VM

The server runs on a low-cost rented virtual machine (VPS), sufficient for static content and minimal services.

General VM specs:

The provider isn’t relevant: OpenBSD doesn’t depend on host-specific features. The system stays portable and reproducible.

Choosing the stack

The stack was chosen deliberately to reduce the attack surface:

No nginx, no Docker, no web panels, no Node in production. Just base-system binaries and text files.

Migrating to OpenBSD

The migration included:

Clear separation between: the system (/etc), content (/var/www), and keys (/etc/ssl, /etc/acme). Nothing was left “at default” without understanding it first.

Web server with httpd

The web server was configured using native httpd, with no external modules.

Simplified base configuration:

server "nezzontli.xyz" {
    listen on * port 80
    root "/htdocs"

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

server "nezzontli.xyz" {
    listen on * tls port 443
    root "/htdocs"

    tls {
        certificate "/etc/ssl/nezzontli.xyz.fullchain.pem"
        key "/etc/ssl/private/nezzontli.xyz.key"
    }
}

HTTPS with acme-client

OpenBSD ships acme-client in base, which removes external dependencies.

Actual flow:

  1. Configure httpd for HTTP only
  2. Resolve DNS correctly (A records pointing at the VM)
  3. Run: doas acme-client -v nezzontli.xyz
  4. Verify certificate creation in /etc/ssl
  5. Enable TLS in httpd
  6. Restart the service

No external scripts are used. Renewal is handled through the system’s cron.

Problems encountered (and their solutions)

1. 404 error on the ACME challenge

Cause: httpd wasn’t correctly serving /var/www/acme.

Solution:

doas mkdir -p /var/www/acme
doas chown -R web:web /var/www

2. DNS not propagated

During the first attempt, the domain resolved to multiple old IPs: dig nezzontli.xyz +short

Solution: wait for full propagation and remove conflicting records.

3. Confusion around DNSSEC

DNSSEC is not a requirement for Let’s Encrypt HTTP-01. DNSSEC is independent of TLS. It wasn’t enabled at this stage.

Building the site with Zola

The site is 100% static.

Basic flow:

zola build
doas rsync -a --delete public/ /var/www/htdocs/

There’s no runtime generation. No writes from the web server.

Tor mirror (.onion)

A Tor Hidden Service was set up as a mirror of the site.

/etc/tor/torrc:

HiddenServiceDir /var/tor/hidden_service/
HiddenServicePort 80 127.0.0.1:80

httpd listens only on localhost for Tor:

server "*" {
    listen on 127.0.0.1 port 80
    root "/tor"
}

Dedicated Tor build:

zola build --base-url http://<onion>.onion
doas rsync -a --delete public/ /var/www/tor/

Tor Browser integration (Onion-Location):

<meta http-equiv="onion-location" content="http://<onion>.onion">

Deployment automation

A single script for clearnet + Tor:

#!/bin/sh
set -e

ONION_URL="http://<onion>.onion"

git pull

zola build
doas rsync -a --delete public/ /var/www/htdocs/

zola build --base-url "$ONION_URL"
doas rsync -a --delete public/ /var/www/tor/

Installed at: /usr/local/bin/update-site

Access control: SSH + YubiKey

Remote access with no persistent passwords:

The private key never touches disk.

Version control and signing

Repository hosted on GitHub:

The server doesn’t build from remote hooks: deployment is deliberate and manual.

Project structure

site/
├── config.toml
├── content/
├── templates/
├── static/
├── public/
└── scripts/

On the server:

/var/www/
├── htdocs/
├── tor/
└── acme/

What’s left to do

Actual remaining work:

Core idea

This project isn’t about convenience or development speed.

It’s about:

OpenBSD doesn’t speed up the work. It eliminates silent failures. That’s the value.