Newt and OpenBSD
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.
Member discussion