< the skull

NEWT AND OPENBSD
================
2026-06-28

The scenario
============

The idea was simple: spin up an OpenBSD VM on Proxmox to host a small, static
site generated with an SSG. No monstrous stack, no Docker, no panel full of
abstractions in the middle. Just a clean and predictable system, with httpd
serving static files, the way OpenBSD likes it.

On the public side, I already had a structure running on a VPS: Pangolin,
Traefik, and Gerbil. Their job is to receive internet traffic and forward it to
internal services on my network. And the most natural connector for that, inside
the Pangolin ecosystem, is Newt.

The domain would be theskull.org. The OpenBSD VM became mercurio, with the IP
192.168.50.73 on the local network. There was also calisto, an existing machine
on the network, at 192.168.50.46. That detail seemed minor at first and ended up
being the key to everything.

The first decision: was OpenBSD a good idea?
============================================

For Docker, no. For a static site, yes. That distinction matters because a lot
of people mix the two things and then blame the operating system. OpenBSD is a
poor choice for stacking modern containers, ready-made stacks, and compose files
for everything. But for serving a static site with httpd, pf, and a small
configuration, it is excellent.

I created a very modest VM: one CPU, little memory, a simple disk, and VirtIO
networking. No X, no frills. The system came up, I left sshd enabled, created
the right user, adjusted permissions with doas, and moved on to what seemed to
be the main step: installing Newt.

The wrong attempt: installing Newt directly on OpenBSD
======================================================

Pangolin offers a nice and tempting command to install Newt, the kind of command
that usually solves everything on Linux in ten seconds:

curl -fsSL https://static.pangolin.net/get-newt.sh | bash

But I was on OpenBSD, and that is where things stopped. Newt works very well in
supported environments, especially Linux. On OpenBSD, it is not that
straightforward. Since the official installer did not solve it, I tried to
compile it manually from the repository.

pkg\_add curl git go gmake

git clone https://github.com/fosrl/newt.git

cd newt

go build -o newt .

The compilation did start. Go downloaded the dependencies, flooded the screen
with modules, and almost gave me hope. Then it stopped on a very clear error:

package github.com/fosrl/newt

imports github.com/fosrl/newt/clients/permissions: build constraints

exclude all Go files in /home/skull/newt/clients/permissions

It was not a missing package or a typo. It was drier than that: in that part of
the code, the available Go files were excluded by OpenBSD build constraints.
There was no valid implementation being compiled for the platform. It was not
the time to force it by shouting at the terminal.

The failure was not technical. It was architectural. I had gotten stuck on the
idea that the connector needed to run inside the OpenBSD VM itself. But Newt
does not need to be on the same server as the final service. It only needs to be
somewhere that can see the internal service.

That was when it clicked: on another occasion, I had already connected internal
services to Pangolin, and I suspected that this went through a machine called
calisto. So I checked.

hostname

uname -a

ip a

which newt

ps aux | grep -i newt | grep -v grep

systemctl status newt --no-pager

It was all there. Calisto was Debian, IP 192.168.50.46, running Newt as a
systemd service. The active process was pointing to the Pangolin endpoint at
pango.forsak.ing. The tunnel already existed. I was trying to build a new bridge
over a river that already had one.

The correct architecture
========================

The solution became much cleaner when I stopped trying to push Newt into
OpenBSD. The right design was this:

Internet

-> Pangolin / Traefik / Gerbil on the VPS

-> Newt running on calisto

-> http://192.168.50.73:80 on mercurio/OpenBSD

Calisto was already the network connector and could reach mercurio through the
LAN. So Pangolin only needed a resource pointing to mercurio's internal IP,
using calisto as the site. Direct, without trying to turn OpenBSD into Linux.

On mercurio, the OpenBSD side was the easiest part. I created the site directory
inside the standard httpd environment:

mkdir -p /var/www/htdocs/mercurio

I put in a test index.html and a minimal configuration in /etc/httpd.conf:

server "default" {

listen on \* port 80

root "/htdocs/mercurio"

}

I validated the configuration:

httpd -n

And enabled the service:

rcctl enable httpd

rcctl start httpd

From mercurio, the server was up. From calisto, the test also passed:

curl -I http://192.168.50.73

It returned 200 OK, with Server: OpenBSD httpd. That was the sign that the path
from calisto to mercurio was working.

Then came the most irritating part. Opening https://theskull.org in the browser,
the page loaded without an error, the certificate was valid, and everything was
white. It is the kind of bug that tricks you, because it looks like DNS, looks
like proxying, looks like Pangolin, looks like anything except the obvious.

I tested it with curl for real instead of just staring at the browser:

curl -v http://192.168.50.73/

curl -v https://theskull.org/

In both cases it returned 200 OK. TLS was right, theskull.org pointed to the
correct VPS IP (84.247.140.162), and the traffic reached OpenBSD. But one line
revealed the crime:

Content-Length: 0

It was not DNS, Pangolin, Traefik, Gerbil, or Newt. OpenBSD was serving a valid
response, but it was empty. The white page was, literally, a page with no
content.

The final fix
=============

The fix was to recreate index.html properly inside the directory served by
httpd:

<!doctype html>

<html>

<head>

<meta charset="utf-8">

<title>The Skull</title>

</head>

<body>

<h1>The Skull is alive on OpenBSD</h1>

<p>Served by the mercurio VM through OpenBSD httpd.</p>

</body>

</html>

I restarted httpd, repeated the tests, and then the content appeared.
theskull.org started loading the HTML served by mercurio, passing through
calisto and Pangolin.

The most obvious lesson: not everything needs to run on the same machine. Newt
did not have to be on OpenBSD. It had to be on a machine that could see OpenBSD,
and calisto was already doing that job.

The more important lesson is another one. When a tool does not properly support
a platform, insisting on it becomes technical fetishism. Compiling Newt on
OpenBSD seemed elegant, but it was unnecessary. The right move was to use
OpenBSD for what it does well: serve the site in a simple and clean way.

And then there is that lesson that always comes back to humiliate me: test layer
by layer. DNS, TLS, proxy, connector, internal service, in that order. In the
end, the error was in the dumbest possible place: a file served by httpd with
zero bytes of useful content.

Mercurio remained a minimalist OpenBSD VM, serving a static site with httpd.
Calisto remained the Newt connector for the network. And Pangolin kept handling
public exposure, with Traefik and the certificate working.

The final architecture stayed small and easy to maintain. OpenBSD did not become
the host for a stack that does not suit it, Linux kept the connector that was
already working, and theskull.org started coming from the internet, crossing
Pangolin, entering through calisto, and ending on a page served by OpenBSD
httpd.

The solution was not to install more things. It was to stop installing things
where they did not need to be.


< the skull