This website is deliberately built around a very small architecture.
The objective was not to create another application platform. It was to build a web environment that is easy to understand, easy to isolate and difficult to break.
The architecture
The public web server runs inside a dedicated OpenBSD virtual machine hosted by a Raspberry Pi 5.
The Raspberry Pi acts as the edge layer, while the OpenBSD virtual machine provides the actual HTTPS service from a dedicated virtual network.
The OpenBSD virtual machine has deliberately limited resources:
- 1 vCPU
- 1 GB RAM
- 20 GB virtual disk
The website itself does not require an application server, database or server-side JavaScript runtime.
Hugo generates static HTML, CSS and JavaScript. OpenBSD httpd only has to serve those files.
The Raspberry Pi 5 as the edge layer
The Raspberry Pi 5 does more than simply host the OpenBSD virtual machine.
It also acts as the first security layer between incoming Internet traffic and the web server.
The host firewall is managed using UFW, with only the traffic required by the infrastructure explicitly permitted.
A separate nftables-based reputation filter uses the Spamhaus blocklist to reject known malicious IP addresses before their traffic reaches the OpenBSD server.
Traffic that passes these controls is forwarded using DNAT to the dedicated virtual network containing the OpenBSD VM.
This keeps the public-facing server separate from the Raspberry Pi host itself: the Pi performs the edge filtering and forwarding, while OpenBSD remains responsible for serving the website.
OpenBSD and PF
OpenBSD provides a second, independent filtering layer using its native packet filter, PF.
The web server exposes only the services that are actually required. Administrative SSH access is restricted to the internal network, while HTTPS traffic arriving from the edge is subject to additional PF controls.
For HTTPS, PF monitors the number and rate of TCP connections generated by individual sources.
Sources exceeding the configured thresholds can automatically be placed into a persistent PF table:
https_abusers
Traffic from addresses in that table is then blocked before reaching httpd.
These controls are deliberately implemented at the network layer. PF is not trying to analyse URLs or application behaviour; its role is to detect abnormal TCP connection patterns and enforce limits close to the network stack.
The resulting path therefore has multiple independent controls:
Internet
↓
External Router
↓
Internal Gateway
↓
Raspberry Pi 5
├── UFW
├── Spamhaus / nftables reputation filtering
└── DNAT
↓
Dedicated Virtual Network
↓
OpenBSD
├── PF connection controls
└── httpd
The two filtering layers have different responsibilities.
The Raspberry Pi can reject sources already known to be malicious, while OpenBSD independently controls the traffic that actually reaches the web server.
Why not a traditional DMZ?
This architecture is deliberately not presented as a traditional DMZ.
A conventional DMZ would normally place public-facing systems in a separate physical or logical security zone, with explicit filtering between the Internet-facing network, the DMZ and the internal network.
That would provide cleaner network-level separation and would be the preferred approach for a larger production environment or for infrastructure hosting multiple public services.
This environment has a different scale and purpose.
The OpenBSD server is isolated inside a dedicated libvirt virtual network behind the Raspberry Pi 5. Only explicitly forwarded web traffic can reach it, while administrative access remains restricted to the internal network.
The OpenBSD VM also applies its own PF policy independently of the Raspberry Pi edge controls.
Adding another physical network segment and another firewall zone would certainly be possible, but for a single-purpose static website it would increase infrastructure complexity without providing a proportionate operational benefit.
The objective is therefore not to reproduce an enterprise DMZ at home.
It is to maintain clear isolation, minimal exposure and multiple independent filtering layers while keeping the architecture small enough to remain completely understandable.
Why OpenBSD?
The web server has a very small responsibility.
It receives HTTP and HTTPS requests and returns static content.
That makes OpenBSD a particularly interesting fit: the operating system, packet filter and web server form a compact environment without requiring a large software stack.
httpd is intentionally small. It does not attempt to provide the extensive feature set of larger web servers, but for a static Hugo website that is largely an advantage.
The runtime remains simple:
OpenBSD
├── PF
└── httpd
↓
Static files
The VM is also segregated from the rest of the host infrastructure.
If the web server is compromised, the service is not running directly on the Raspberry Pi host and does not share its operating system environment.
Performance
The small architecture does not imply particularly small serving capacity.
A synthetic test was executed using 400 concurrent HTTPS connections.
The VM sustained approximately:
- 2,000 requests / second
- 14 MB/s
- 120,000 requests / minute
At that point the single virtual CPU reached full utilisation.
There were no read errors, write errors or timeouts during the test.
For a personal vintage computing website, this is substantially more capacity than is realistically required.
The performance test was therefore less about finding the maximum possible throughput and more about verifying that the intentionally constrained VM still had a very large margin compared with the expected workload.
Development versus runtime
Another design principle is separation between the tools used to build the site and the software required to run it.
Hugo is the publishing system.
OpenBSD httpd is the runtime.
The site is developed separately and Hugo generates the final static content. Once generated, the result is simply a directory containing HTML, CSS, JavaScript and image files.
The production server does not need the development environment, a database, Node.js or an application framework to answer a web request.
That separation also keeps deployment straightforward:
Source
↓
Hugo build
↓
Static files
↓
OpenBSD httpdÍ
The runtime has very little state and very few moving parts.
That is intentionally boring.
And for infrastructure, boring is often good.