{
  "version": "https://jsonfeed.org/version/1",
  "title": "Ian's Digital Garden",
  "home_page_url": "https://ianwwagner.com/",
  "feed_url": "https://ianwwagner.com//tag-openbsd.json",
  "description": "",
  "items": [
    {
      "id": "https://ianwwagner.com//a-tls-terminating-reverse-proxy-with-openbsd.html",
      "url": "https://ianwwagner.com//a-tls-terminating-reverse-proxy-with-openbsd.html",
      "title": "A TLS Terminating Reverse Proxy with OpenBSD",
      "content_html": "<p>In my <a href=\"overview-of-my-new-homelab-setup.html\">last post</a>,\none of the things I glossed over was the TLS termination setup.\nWhile there are many ways of doing this, and half a dozen tutorials on most of them,\nyou may remember that my setup on the cloud VPS is explicitly trying\nto stay within the base OpenBSD distribution as much as possible.\nAnd it delivers, even for this use case!</p>\n<h1><a href=\"#httpd\" aria-hidden=\"true\" class=\"anchor\" id=\"httpd\"></a><code>httpd</code></h1>\n<p>You may recall that I'm using <code>httpd</code> for serving plain HTTP traffic.\nIn fact, pretty much all that <code>httpd</code> does\nis respond to ACME challenges.\nACME is the protocol that Let's Encrypt and other CAs use to issue and renew certificates,\nwithout requiring human involvement and 20 mins of cursing while you search for the right\n<code>openssl</code> invocation.</p>\n<p>The first step to setting this up is <code>httpd.conf</code>,\nwhich is dead simple, so I won't give much commentary.\nJust substitute your domain(s) and IPs.</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-nginx\"><a-c># /etc/httpd.conf\n</a-c>\n<a-f>server</a-f> <a-s>&quot;blog.ianwwagner.com&quot;</a-s> <a-p>{</a-p>\n    <a-f>alias</a-f> <a-s>&quot;ianwwagner.com&quot;</a-s>\n    <a-s>alias</a-s> <a-s>&quot;www.ianwwagner.com&quot;</a-s>\n    <a-s>listen</a-s> <a-s>on</a-s> <a-n>46.23.95.102</a-n> <a-s>port</a-s> <a-n>80</a-n>\n    <a-s>listen</a-s> <a-s>on</a-s> <a-s>2a03</a-s>:<a-n>6000</a-n>:<a-s>95f2</a-s>:<a-n>627</a-n>::<a-n>80</a-n> <a-s>port</a-s> <a-n>80</a-n>\n\n    <a-s>location</a-s> <a-s>&quot;/.well-known/acme-challenge/*&quot;</a-s> <a-p>{</a-p>\n        <a-c># NB: httpd runs chrooted to /var/www by default,\n</a-c>        <a-c># so this is /var/www from root&#39;s perspective\n</a-c>        <a-f>root</a-f> <a-s>&quot;/acme&quot;</a-s>\n        <a-s>request</a-s> <a-s>strip</a-s> <a-n>2</a-n>\n    <a-p>}</a-p>\n\n    <a-f>location</a-f> * <a-p>{</a-p>\n        <a-f>block</a-f> <a-s>return</a-s> <a-n>302</a-n> <a-s>&quot;https://</a-s><a-v>$HTTP_HOST$REQUEST_URI</a-v><a-s>&quot;</a-s>\n    <a-p>}</a-p>\n<a-p>}</a-p>\n\n<a-f>server</a-f> <a-s>&quot;matrix.ianwwagner.com&quot;</a-s> <a-p>{</a-p>\n    <a-f>listen</a-f> <a-s>on</a-s> <a-n>46.23.95.102</a-n> <a-s>port</a-s> <a-n>80</a-n>\n    <a-s>listen</a-s> <a-s>on</a-s> <a-s>2a03</a-s>:<a-n>6000</a-n>:<a-s>95f2</a-s>:<a-n>627</a-n>::<a-n>6167</a-n> <a-s>port</a-s> <a-n>80</a-n>\n\n    <a-s>location</a-s> <a-s>&quot;/.well-known/acme-challenge/*&quot;</a-s> <a-p>{</a-p>\n        <a-f>root</a-f> <a-s>&quot;/acme&quot;</a-s>\n        <a-s>request</a-s> <a-s>strip</a-s> <a-n>2</a-n>\n    <a-p>}</a-p>\n\n    <a-f>location</a-f> * <a-p>{</a-p>\n        <a-f>block</a-f> <a-s>return</a-s> <a-n>302</a-n> <a-s>&quot;https://</a-s><a-v>$HTTP_HOST$REQUEST_URI</a-v><a-s>&quot;</a-s>\n    <a-p>}</a-p>\n<a-p>}</a-p></code></pre>\n<p>Note that both of the domains in this setup are listening on port 80.\n<code>httpd</code> disambiguates via the <code>Host</code> header.</p>\n<h1><a href=\"#acme-client-configuration\" aria-hidden=\"true\" class=\"anchor\" id=\"acme-client-configuration\"></a><code>acme-client</code> configuration</h1>\n<p>The above handles the plain HTTP side,\nbut this is really just in service to the ACME client.\nOpenBSD also has this in the base system: <a href=\"https://man.openbsd.org/acme-client.1\"><code>acme-client(1)</code></a>!\nConfiguration is predictably simple:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-nginx\"><a-c># /etc/acme-client.conf\n</a-c>\n<a-f>authority</a-f> <a-s>letsencrypt</a-s> <a-p>{</a-p>\n    <a-f>api</a-f> <a-s>url</a-s> <a-s>&quot;https://acme-v02.api.letsencrypt.org/directory&quot;</a-s>\n    <a-s>account</a-s> <a-s>key</a-s> <a-s>&quot;/etc/secrets/letsencrypt.key&quot;</a-s>\n<a-p>}</a-p>\n\n<a-f>domain</a-f> <a-s>blog.ianwwagner.com</a-s> <a-p>{</a-p>\n    <a-f>alternative</a-f> <a-s>names</a-s> <a-p>{</a-p> <a-f>ianwwagner</a-f><a-s>.com</a-s> <a-s>www.ianwwagner.com</a-s> <a-p>}</a-p>\n    <a-f>domain</a-f> <a-s>key</a-s> <a-s>&quot;/etc/secrets/blog.ianwwagner.com.key&quot;</a-s> <a-c># NB: acme-client supports ecdsa, but relayd is RSA-only\n</a-c>    <a-s>domain</a-s> <a-s>full</a-s> <a-s>chain</a-s> <a-s>certificate</a-s> <a-s>&quot;/etc/ssl/blog.ianwwagner.com.crt&quot;</a-s>\n    <a-s>sign</a-s> <a-s>with</a-s> <a-s>letsencrypt</a-s>\n<a-p>}</a-p>\n\n<a-f>domain</a-f> <a-s>matrix.ianwwagner.com</a-s> <a-p>{</a-p>\n    <a-f>domain</a-f> <a-s>key</a-s> <a-s>&quot;/etc/secrets/matrix.ianwwagner.com.key&quot;</a-s>\n    <a-s>domain</a-s> <a-s>full</a-s> <a-s>chain</a-s> <a-s>certificate</a-s> <a-s>&quot;/etc/ssl/matrix.ianwwagner.com.crt&quot;</a-s>\n    <a-s>sign</a-s> <a-s>with</a-s> <a-s>letsencrypt</a-s>\n<a-p>}</a-p></code></pre>\n<p>I found it interesting (neutral) that while <code>acme-client</code> supports ECDSA, <code>relayd</code> only supports RSA certs.\nA more typical config will have the keys in <code>/etc/ssl/private</code> or similar,\nbut I'm using an encrypted volume that I'll explain in a future post so keep that in mind.\nOther than that, I think the config is pretty self-explanatory.</p>\n<h1><a href=\"#generating-the-initial-certificates\" aria-hidden=\"true\" class=\"anchor\" id=\"generating-the-initial-certificates\"></a>Generating the initial certificates</h1>\n<p>Now we're all set to generate the first certs.\nFirst, enable <code>httpd</code>:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-shell\"><a-c># Enable and start the HTTP daemon</a-c>\n<a-f>rcctl</a-f> enable httpd\n<a-f>rcctl</a-f> start httpd</code></pre>\n<p>Then, trigger the initial cert generation:</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-shell\"><a-c># Trigger acme-client</a-c>\n<a-f>acme-client</a-f> <a-co>-v</a-co> blog.ianwwagner.com\n<a-f>acme-client</a-f> <a-co>-v</a-co> matrix.ianwwagner.com</code></pre>\n<p>I'd recommend adding this to your <code>crontab</code>.\nIn the process of writing this, I learned just how nonstandard crontab is;\nspecifically how OpenBSD includes a nonstandard <code>~</code> for randomizing components!</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">~ * * * * mount | grep -q /etc/secrets &amp;&amp; acme-client blog.ianwwagner.com &amp;&amp; rcctl reload relayda\n~ * * * * mount | grep -q /etc/secrets &amp;&amp; acme-client matrix.ianwwagner.com &amp;&amp; rcctl reload relayd\n</code></pre>\n<h1><a href=\"#setting-up-relayd-with-multiple-domains\" aria-hidden=\"true\" class=\"anchor\" id=\"setting-up-relayd-with-multiple-domains\"></a>Setting up <code>relayd</code> with multiple domains</h1>\n<p>And now for the last stage: setting up <code>relayd</code> with multiple domains.\nThis part was by far the least intuitive, so I'll have a lot more commentary in the config.</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-nginx\"><a-c># /etc/relayd.conf\n</a-c>\n<a-c># Tables; these are somewhat specific to my configuration, but you can adapt.\n# I&#39;m running varnish (vinyl) cache locally,\n# and my homelab backend is on the class B private subnet\n</a-c><a-f>table</a-f> &lt;vinyl&gt; <a-p>{</a-p> <a-f>127</a-f>.0.0.1 <a-p>}</a-p>\ntable &lt;<a-f>bsdcube</a-f>&gt; <a-p>{</a-p> <a-f>172</a-f>.16.42.2 <a-p>}</a-p>\n\n<a-c># --- Protocols ---\n# Three protocols because each relay can only forward to tables declared\n# in its block, and any table referenced by a protocol &quot;match ... forward to&quot;\n# MUST appear in the relay. Using one shared protocol everywhere would force\n# blog-only relays to declare the bsdcube table (and vice versa).\n# Opinions on how to improve this are VERY welcome!\n</a-c>\n<a-c># Shared HTTPS protocol for the IPv4 :443 listener.\n#\n# Blog and Matrix share the same IPv4 address:\n# - TLS SNI selects the certificate\n# - an HTTP Host match selects the backend\n# - requests that do not match a more specific rule use the relay&#39;s main\n#   forward-to table\n</a-c><a-f>http</a-f> <a-s>protocol</a-s> <a-s>&quot;https&quot;</a-s> <a-p>{</a-p>\n    <a-f>match</a-f> <a-s>request</a-s> <a-s>header</a-s> <a-s>append</a-s> <a-s>&quot;X-Forwarded-For&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$REMOTE_ADDR</a-v><a-s>&quot;</a-s>\n    <a-s>match</a-s> <a-s>request</a-s> <a-s>header</a-s> <a-s>append</a-s> <a-s>&quot;X-Real-IP&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$REMOTE_ADDR</a-v><a-s>&quot;</a-s>\n    <a-s>match</a-s> <a-s>request</a-s> <a-s>header</a-s> <a-s>append</a-s> <a-s>&quot;X-Forwarded-By&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$SERVER_ADDR</a-v><a-s>:</a-s><a-v>$SERVER_PORT</a-v><a-s>&quot;</a-s>\n\n    <a-s>match</a-s> <a-s>response</a-s> <a-s>header</a-s> <a-s>remove</a-s> <a-s>&quot;Server&quot;</a-s>\n\n    <a-c># Multiple keypairs enable SNI; relayd serves the right cert\n</a-c>    <a-c># based on the ClientHello server_name extension.\n</a-c>    <a-s>tls</a-s> <a-s>keypair</a-s> <a-s>&quot;blog.ianwwagner.com&quot;</a-s>\n    <a-s>tls</a-s> <a-s>keypair</a-s> <a-s>&quot;matrix.ianwwagner.com&quot;</a-s>\n\n    <a-c># Host match overrides the relay&#39;s default (first) forward-to table.\n</a-c>    <a-c># Two forms are matched because RFC 7230 allows the Host header to\n</a-c>    <a-c># include a port number ;)\n</a-c>    <a-c># Unmatched hosts fall through to &lt;vinyl&gt;.\n</a-c>    <a-s>match</a-s> <a-s>request</a-s> <a-s>header</a-s> <a-s>&quot;Host&quot;</a-s> <a-s>value</a-s> <a-s>&quot;matrix.ianwwagner.com&quot;</a-s>     <a-s>forward</a-s> <a-s>to</a-s> &lt;bsdcube&gt;\n    match request header <a-s>&quot;Host&quot;</a-s> <a-s>value</a-s> <a-s>&quot;matrix.ianwwagner.com:443&quot;</a-s> <a-s>forward</a-s> <a-s>to</a-s> &lt;bsdcube&gt;\n<a-p>}</a-p>\n\n<a-c># Blog-only (dedicated blog IPv6 address)\n</a-c>http protocol <a-s>&quot;blog_https&quot;</a-s> <a-p>{</a-p>\n    <a-f>match</a-f> <a-s>request</a-s> <a-s>header</a-s> <a-s>set</a-s> <a-s>&quot;X-Forwarded-For&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$REMOTE_ADDR</a-v><a-s>&quot;</a-s>\n    <a-s>match</a-s> <a-s>request</a-s> <a-s>header</a-s> <a-s>set</a-s> <a-s>&quot;X-Real-IP&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$REMOTE_ADDR</a-v><a-s>&quot;</a-s>\n    <a-s>match</a-s> <a-s>request</a-s> <a-s>header</a-s> <a-s>set</a-s> <a-s>&quot;X-Forwarded-By&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$SERVER_ADDR</a-v><a-s>:</a-s><a-v>$SERVER_PORT</a-v><a-s>&quot;</a-s>\n\n    <a-s>match</a-s> <a-s>response</a-s> <a-s>header</a-s> <a-s>remove</a-s> <a-s>&quot;Server&quot;</a-s>\n\n    <a-s>tls</a-s> <a-s>keypair</a-s> <a-s>&quot;blog.ianwwagner.com&quot;</a-s>\n<a-p>}</a-p>\n\n<a-c># Matrix-only (dedicated matrix IPv6 :443 and federation :8448)\n</a-c><a-f>http</a-f> <a-s>protocol</a-s> <a-s>&quot;matrix_https&quot;</a-s> <a-p>{</a-p>\n    <a-f>match</a-f> <a-s>request</a-s> <a-s>header</a-s> <a-s>set</a-s> <a-s>&quot;X-Forwarded-For&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$REMOTE_ADDR</a-v><a-s>&quot;</a-s>\n    <a-s>match</a-s> <a-s>request</a-s> <a-s>header</a-s> <a-s>set</a-s> <a-s>&quot;X-Real-IP&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$REMOTE_ADDR</a-v><a-s>&quot;</a-s>\n    <a-s>match</a-s> <a-s>request</a-s> <a-s>header</a-s> <a-s>set</a-s> <a-s>&quot;X-Forwarded-By&quot;</a-s> <a-s>value</a-s> <a-s>&quot;</a-s><a-v>$SERVER_ADDR</a-v><a-s>:</a-s><a-v>$SERVER_PORT</a-v><a-s>&quot;</a-s>\n\n    <a-s>match</a-s> <a-s>response</a-s> <a-s>header</a-s> <a-s>remove</a-s> <a-s>&quot;Server&quot;</a-s>\n\n    <a-s>tls</a-s> <a-s>keypair</a-s> <a-s>&quot;matrix.ianwwagner.com&quot;</a-s>\n<a-p>}</a-p>\n\n<a-c># --- Relays ---\n# One block per listen address...\n</a-c>\n<a-c># Port 443: shared IPv4 (SNI selects the certificate; Host selects the backend).\n# The relay&#39;s main table is &lt;vinyl&gt;; the protocol&#39;s\n# Host match can route Matrix traffic to &lt;bsdcube&gt;.\n</a-c><a-f>relay</a-f> <a-s>&quot;https4&quot;</a-s> <a-p>{</a-p>\n    listen <a-s>on</a-s> <a-n>46.23.95.102</a-n> <a-s>port</a-s> <a-n>443</a-n> <a-s>tls</a-s>\n    <a-s>protocol</a-s> <a-s>&quot;https&quot;</a-s>\n    <a-s>forward</a-s> <a-s>to</a-s> &lt;vinyl&gt; port 8080\n    forward to &lt;bsdcube&gt; port <a-n>6167</a-n>\n<a-p>}</a-p>\n\n<a-c># Port 443: blog IPv6\n</a-c><a-f>relay</a-f> <a-s>&quot;blog_tls6&quot;</a-s> <a-p>{</a-p>\n    <a-f>listen</a-f> <a-s>on</a-s> <a-s>2a03</a-s>:<a-n>6000</a-n>:<a-s>95f2</a-s>:<a-n>627</a-n>::<a-n>80</a-n> <a-s>port</a-s> <a-n>443</a-n> <a-s>tls</a-s>\n    <a-s>protocol</a-s> <a-s>&quot;blog_https&quot;</a-s>\n    <a-s>forward</a-s> <a-s>to</a-s> &lt;vinyl&gt; port 8080\n<a-p>}</a-p>\n\n<a-c># Port 443: matrix IPv6\n</a-c>relay <a-s>&quot;matrix_tls6&quot;</a-s> <a-p>{</a-p>\n    <a-f>listen</a-f> <a-s>on</a-s> <a-s>2a03</a-s>:<a-n>6000</a-n>:<a-s>95f2</a-s>:<a-n>627</a-n>::<a-n>6167</a-n> <a-s>port</a-s> <a-n>443</a-n> <a-s>tls</a-s>\n    <a-s>protocol</a-s> <a-s>&quot;matrix_https&quot;</a-s>\n    <a-s>forward</a-s> <a-s>to</a-s> &lt;bsdcube&gt; port 6167\n<a-p>}</a-p>\n\n<a-c># Port 8448: matrix federation (IPv4)\n</a-c>relay <a-s>&quot;matrix_fed4&quot;</a-s> <a-p>{</a-p>\n    listen <a-s>on</a-s> <a-n>46.23.95.102</a-n> <a-s>port</a-s> <a-n>8448</a-n> <a-s>tls</a-s>\n    <a-s>protocol</a-s> <a-s>&quot;matrix_https&quot;</a-s>\n    <a-s>forward</a-s> <a-s>to</a-s> &lt;bsdcube&gt; port 6167\n<a-p>}</a-p>\n\n<a-c># Port 8448: matrix federation (IPv6)\n</a-c><a-f>relay</a-f> <a-s>&quot;matrix_fed6&quot;</a-s> <a-p>{</a-p>\n    listen <a-s>on</a-s> <a-s>2a03</a-s>:<a-n>6000</a-n>:<a-s>95f2</a-s>:<a-n>627</a-n>::<a-n>6167</a-n> <a-s>port</a-s> <a-n>8448</a-n> <a-s>tls</a-s>\n    <a-s>protocol</a-s> <a-s>&quot;matrix_https&quot;</a-s>\n    <a-s>forward</a-s> <a-s>to</a-s> &lt;bsdcube&gt; port 6167\n<a-p>}</a-p></code></pre>\n<p>It's a bit more that I wish were necessary, but on the other hand,\nit's definitely not rocket science!\nHopefully someone finds this useful;\nno need for Caddy or other packages with the OpenBSD base!</p>\n",
      "summary": "",
      "date_published": "2026-04-19T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "homelab",
        "OpenBSD",
        "networking"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//overview-of-my-new-homelab-setup.html",
      "url": "https://ianwwagner.com//overview-of-my-new-homelab-setup.html",
      "title": "Overview of my New Homelab Setup",
      "content_html": "<p>For various reasons, I recently decided to start running a homelab again.\nIn this post I'll give a broad overview of the architecture I settled on,\nand why I made specific choices.\nI'll try to keep things relatively high level here,\nand we'll dive into the implementation details in future posts.</p>\n<h1><a href=\"#my-requirements\" aria-hidden=\"true\" class=\"anchor\" id=\"my-requirements\"></a>My requirements</h1>\n<p>Let's start from the beginning with what I'm trying to achieve.\nI want to host multiple services, some of them internet-accessible,\nfrom a box on my desk running FreeBSD.\nSpecifically FreeBSD, because I find it has an uncommon trio of properties:</p>\n<ul>\n<li>It's (really) easy to administer.</li>\n<li>It's rock solid stable.</li>\n<li>It has a huge collection of up-to-date packages.</li>\n</ul>\n<p>It's surprisingly rare to find all 3 in a system.\nThe last 2 in particular are often mutually exclusive.</p>\n<p>I already have the hardware (an old NUC, which I've named <code>bsdcube</code> even though it's not a cube),\nand my home internet is rock solid, but I don't have a static IP.\nEven if I did, I'm not sure I want to run some services on a residential IP,\nor to invite attacks on my home network.</p>\n<h1><a href=\"#broad-architecture-overview\" aria-hidden=\"true\" class=\"anchor\" id=\"broad-architecture-overview\"></a>Broad architecture overview</h1>\n<p>The architecture that I chose keeps <code>bsdcube</code> on my home network behind the NAT firewall.\nTo make the server accessible from the internet,\nI use a WireGuard tunnel to a remote host that is internet-accessible.\nPF rules on the accessible server route certain traffic over the tunnel and block the rest.</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\"> +----------+       +---------+                         +---------+\n | Internet |------&gt;| proxbox |&lt;---WireGuard-Tunnel----&gt;| bsdcube |\n +----------+       +---------+                         +---------+\n</code></pre>\n<h1><a href=\"#choosing-a-host-for-proxbox\" aria-hidden=\"true\" class=\"anchor\" id=\"choosing-a-host-for-proxbox\"></a>Choosing a host for <code>proxbox</code></h1>\n<p>For my internet-accessible box, which I dubbed <code>proxbox</code>,\nI wanted a reliable host that supported a BSD.\nI was initially planning on getting a FreeBSD box for the job.\nI even had a box set up, as described in <a href=\"setting-up-a-wireguard-tunnel-on-freebsd-15.html\">my post on the subject</a>.\n(I guess I'll need to write a follow-up now!)</p>\n<p>I was somewhat discouraged by the poor support of FreeBSD on most cloud providers.\nThere is ironically a split where a lot of the mega-hosts like AWS have tier 1 support,\nbut there's nothing in the middle.\nMany of the midrange hosts which had good FreeBSD VM support now require a tedious manual install process.\nVultr seemed to be the only host that had at least some level of support,\nbut they were not my first choice.</p>\n<p>Then my friend <a href=\"https://andrewzah.com/blog/\">Andrew</a> mentioned <a href=\"https://openbsd.amsterdam/\">OpenBSD Amsterdam</a>.\nIt was love at first sight.\nI had, somehow (despite being a FreeBSD using since around 2002) never actually used OpenBSD.\nBut I loved their pitch: a community-focused host that donates a large part of every purchase\nto the OpenBSD foundation.</p>\n<p>The onboarding was fast and easy.\nI filled out the form, and within 2 hours I had an email with connection instructions!\nI hadn't even been given an invoice yet!\nReally fantastic service.</p>\n<h1><a href=\"#proxbox-setup-using-the-openbsd-base-system\" aria-hidden=\"true\" class=\"anchor\" id=\"proxbox-setup-using-the-openbsd-base-system\"></a><code>proxbox</code> setup using the OpenBSD base system</h1>\n<p>Now let's talk about the guts of <code>proxbox</code>.\nSurprisingly, almost everything running on this box is part of the OpenBSD base system!</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner\">                            +-------+                                                     \n  /--\\      +-----HTTP-----&gt;| httpd |                                                     \n /    \\ ----+               |       |                                                     \n \\ PF /                     +-------+               +---------+                      \n  \\__/  ----+               +--------+  +---blog---&gt;| Varnish |--+   +---------------+\n            |               | relayd |--+           |         |  +--&gt;| WG -&gt; bsdcube |\n            +-----HTTPS----&gt;|        |--+           +---------+  +--&gt;|               |\n                            +--------+  |                        |   +---------------+\n                                        +-------matrix-----------+                   \n</code></pre>\n<p>It all starts with PF, the OpenBSD Packet Filter.\nIn addition to rejecting invalid / unwanted traffic,\nit's responsible for routing the legit packets to the right service.</p>\n<h2><a href=\"#serving-https-with-httpd8-and-relayd8\" aria-hidden=\"true\" class=\"anchor\" id=\"serving-https-with-httpd8-and-relayd8\"></a>Serving HTTP(S) with <code>httpd(8)</code> and <code>relayd(8)</code></h2>\n<p>An HTTP server sits on the box for handling the ACME challenges\n(e.g. from <a href=\"https://letsencrypt.org/\">Let's Encrypt</a>) to get a TLS cert.\nOpenBSD has <a href=\"https://man.openbsd.org/httpd.8\"><code>httpd(8)</code></a>,\nnot to be confused with the Apache <code>httpd</code>.\nIt can serve static files and FastCGI, but it's not a reverse proxy.</p>\n<p>Normally I'd reach for something like nginx, HAProxy, or Caddy,\nbut OpenBSD also has an excellent (load balancing) reverse proxy in the base system already:\n<a href=\"https://man.openbsd.org/relayd.8\"><code>relayd(8)</code></a>.</p>\n<p><code>relayd</code> in my setup is responsible for 2 things:\nrouting to the correct backend (e.g. based on SNI),\nand TLS termination.\nI'll write a longer post about this soon,\nincluding how to configure <a href=\"https://man.openbsd.org/acme-client.1\"><code>acme-client(1)</code></a>to get certs automatically,\nand configuring <code>relayd</code> to terminate TLS for multiple domains.</p>\n<h2><a href=\"#varnish-vinyl-cache\" aria-hidden=\"true\" class=\"anchor\" id=\"varnish-vinyl-cache\"></a>Varnish (Vinyl) cache</h2>\n<p>Since my blog is just a static site,\nit would be quite reasonable to host it with <code>httpd</code> on <code>proxbox</code>.\nThis <em>would</em> result in faster response times,\nbut I decided to host it on <code>bsdcube</code> to make the setup simpler.\nThe clear separation of concerns, with <code>bsdcube</code> being the final source of truth\nfor all data and services makes it easier for me to maintain.\nI'll never have to touch <code>proxbox</code> outside of routine system maintenance or adding a new service port.</p>\n<p>That said, I'm not going to just blindly forward <em>every single request</em> over to my home network.\nI've used <a href=\"https://vinyl-cache.org/\">Varnish Cache</a> (now rebranded as Vinyl)\nfor over a decade in various professional contexts.\nIt does an excellent job at absorbing traffic spikes,\nand smoothing over issues when the backend goes flaky.\nSince I'm tunneling everything over the internet,\nhalfway around the world,\nthis seems like table stakes.</p>\n<p>(Fun fact: the <a href=\"https://phk.freebsd.dk/\">author</a> of Varnish is <em>also</em> the author of FreeBSD's jails;\nhe's a legend in the FreeBSD community, and should be more widely known.)</p>\n<h1><a href=\"#the-middle-wireguard\" aria-hidden=\"true\" class=\"anchor\" id=\"the-middle-wireguard\"></a>The Middle: WireGuard</h1>\n<p>Whatever route a legitimate packet takes,\nif it needs to talk to a backend service,\nthis happens over a WireGuard tunnel.</p>\n<p>I could have done this with something like Tailscale or Cloudflare Warp,\nbut I wanted something that wasn't reliant on a &quot;free&quot; external service.\nSuch services are rarely free forever.\nRolling your own tunnel with a proven technology is <a href=\"setting-up-a-wireguard-tunnel-on-freebsd-15.html\">surprisingly easy</a>,\nand my intentional choice of a somewhat offbeat rest of the stack made the decision a no-brainer.\nBoth FreeBSD and OpenBSD have WireGuard support built-in at the kernel level already.</p>\n<p>PF rules on <em>both</em> sides of the tunnel ensure that only certain traffic passes through.\nI will probably write a post on this later as well as there's quite a lot here.</p>\n<h1><a href=\"#bsdcube\" aria-hidden=\"true\" class=\"anchor\" id=\"bsdcube\"></a><code>bsdcube</code></h1>\n<p>Let's look at the other side of the tunnel now.\n<code>bsdcube</code> sits on my home network behind a NAT firewall.\nIt only accepts traffic over the tunnel for specific service ports.\nAnd those ports are mapped to jailed services with a limited view of the system.\nIn case you're not familiar with FreeBSD, jails are a FreeBSD-native containerization technology\nwhich has been around for over 25 years (that's a long time before Docker popularized the term).\nWhile they have many differences with Linux containers,\nthat's a reasonable mental model for understanding the rest of this post.</p>\n<h2><a href=\"#appjail\" aria-hidden=\"true\" class=\"anchor\" id=\"appjail\"></a>AppJail</h2>\n<p>The base system has <code>jail(8)</code> as a basic management facility.\nThis does technically have everything you need,\nbut it requires enough steps that,\njust as in the Linux container world,\nmost people rely on higher level tools for management and orchestration.</p>\n<p>There are over a dozen such tools for FreeBSD,\neach with a different take on things.\nThat's both cool (healthy ecosystem!) and bewildering for newcomers to the space ;)</p>\n<p>I ended up settling on <a href=\"https://appjail.readthedocs.io/en/latest/\">AppJail</a> for <code>bsdcube</code>,\nat least for now,\nsince it has extensive documentation,\nsupport for features I'm curious about later (like Linux jails),\nand a declarative, composable configuration format.</p>\n<h3><a href=\"#creating-jails-with-makejail-files\" aria-hidden=\"true\" class=\"anchor\" id=\"creating-jails-with-makejail-files\"></a>Creating jails with <code>Makejail</code> files</h3>\n<p>Here's a sample of a <code>Makejail</code> file.\nAs you might guess, it's a set of instructions for building a jail.\nIt looks a bit like a Dockerfile.</p>\n<pre class=\"marmite-code\"><code class=\"marmite-code-inner language-dockerfile\"><a-c># blog/Makejail</a-c>\n\n<a-c># Include directives let you separate out sections of your config across files</a-c>\n<a-c># and reuse common sections.</a-c>\nINCLUDE options/options.makejail\nINCLUDE options/network.makejail\n\n<a-c># Install nginx from the FreeBSD package repository</a-c>\nPKG nginx\n\n<a-c># A sysrc directive.</a-c>\n<a-c># This is the equivalent of enabling a service on a Linux box with systemd</a-c>\nSYSRC nginx_enable=YES\n\n<a-c># Copies nginx.conf from the current directory into the specified path in the jail.</a-c>\nCOPY nginx.conf /usr/local/etc/nginx/nginx.conf\n\n<a-c># Install Marmite, the static site generator I use for my blog.</a-c>\n<a-c># Despite being relatively obscure, there&#39;s also an up-to-date package for this!</a-c>\nPKG marmite\n\nINCLUDE buildsite.makejail\n\nSERVICE nginx start</code></pre>\n<h2><a href=\"#jail-lifecycle-management\" aria-hidden=\"true\" class=\"anchor\" id=\"jail-lifecycle-management\"></a>Jail lifecycle management</h2>\n<p>&quot;Scripted&quot; setups like this can get pretty unwieldy if you're not careful.\nAnd for me, one of the worst parts of CLI tools is remembering the half dozen switches\nto get a particular task done.</p>\n<p>To solve this, I structured my jail deployments as a git repository\nwhere each subdirectory is a self-contained service.\nIn my setup, there are <strong>no dependencies between services</strong>.\nI'm deploying full applications/services in isolated jails,\neven when I could theoretically have shared services (e.g., a Postgres database).\nAs such, it's easy to add a <a href=\"https://just.systems/man/en/introduction.html\"><code>justfile</code></a> to each directory\nwith deployment recipes so I don't have to remember them.</p>\n<p>The usual invocation is something like <code>just freshjail</code> to (re)create a jail.\nAnd for jails like my blog, where the <em>application</em> has no need to restart\nto get an update, I have recipe(s) that perform the necessary updates against the running jail.</p>\n<h1><a href=\"#conclusion-or-whats-next\" aria-hidden=\"true\" class=\"anchor\" id=\"conclusion-or-whats-next\"></a>Conclusion (or, what's next)</h1>\n<p>That was a lot... and there's still a lot I didn't cover!\nLike tips on the AppJail host setup, PF, handling multiple hosts with SNI via <code>relayd</code>, and a lot more.\nSo stay tuned for the next post,\nand give a shout on Mastodon if you have any feedback.</p>\n<p>In the meantime, I've uploaded the AppJail part of my setup\nto a <a href=\"https://codeberg.org/ianthetechie/appjails/\">public repo on Codeberg</a>.\nHopefully they are useful for others!</p>\n",
      "summary": "",
      "date_published": "2026-04-11T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "homelab",
        "FreeBSD",
        "OpenBSD",
        "networking",
        "jails",
        "containerization"
      ],
      "language": "en"
    }
  ]
}