<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Network on Andrew's Memory Blog</title><link>https://andrewmemory.acornwall.net/tags/network/</link><description>Recent content in Network on Andrew's Memory Blog</description><generator>Hugo -- gohugo.io</generator><image><url>https://andrewmemory.acornwall.net/img/rss_image.png</url><title>Network on Andrew's Memory Blog</title><link>https://andrewmemory.acornwall.net/</link></image><language>en</language><managingEditor>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</managingEditor><webMaster>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</webMaster><copyright>Copyright 2009--2025</copyright><lastBuildDate>Wed, 08 Apr 2026 23:14:09 -0700</lastBuildDate><atom:link href="https://andrewmemory.acornwall.net/tags/network/index.xml" rel="self" type="application/rss+xml"/><item><title>Uploading to the webserver from Forgejo</title><link>https://andrewmemory.acornwall.net/blog/2026-04-08-uploading-from-forgejo-to-the-webserver/</link><pubDate>Wed, 08 Apr 2026 23:14:09 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2026-04-08-uploading-from-forgejo-to-the-webserver/</guid><description>&lt;p&gt;To get Hugo up to the webserver, I had to scp the files up there using my keyfile. For that, I started with the image that I gave the &lt;code&gt;docker&lt;/code&gt; tag, which is one of the default Forgejo images.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Setting up variables
&lt;div id="setting-up-variables" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#setting-up-variables" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;First I set up a bunch of variables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WEBSERVER_SSH_USERNAME&lt;/code&gt;: my username on the webserver&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WEBSERVER_SSH_HOST&lt;/code&gt;: the hostname of the webserver I upload to&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WEBSERVER_HTML_DIR&lt;/code&gt;: the directory where the webserver files get sent&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class="relative group"&gt;Setting up secrets
&lt;div id="setting-up-secrets" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#setting-up-secrets" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;I ended up with two secrets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SSH_PRIV_KEY&lt;/code&gt;: my private key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SSH_KNOWN_HOSTS&lt;/code&gt;: I also needed to set up my &lt;code&gt;~/.ssh/known_hosts&lt;/code&gt; so that I wouldn&amp;rsquo;t get errors when connecting via scp. I temporarily threw away my existing &lt;code&gt;deploy.yaml&lt;/code&gt; and built a new one that did a little (very little) ssh using sftp:&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;.forgejo/workflows/deploy.yaml&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;push]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deployscp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;docker&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo dir &amp;gt; ftpbatch
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo bye &amp;gt;&amp;gt; ftpbatch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;setup ssh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; mkdir -p ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0700 ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;${{ secrets.SSH_PRIV_KEY }}&amp;#34; &amp;gt; ~/.ssh/id_rsa_webserver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0600 ~/.ssh/id_rsa_webserver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ssh-keyscan ${{ vars.WEBSERVER_SSH_HOST }} &amp;gt;&amp;gt; ~/.ssh/known_hosts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; cat ~/.ssh/known_hosts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;push public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo about to sftp
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; sftp -i ~/.ssh/id_rsa_webserver -b ./ftpbatch ${{ vars.WEBSERVER_SSH_USERNAME }}@${{ vars.WEBSERVER_SSH_HOST }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo did sftp&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;The first time I ran that, I copied the output of the &lt;code&gt;cat ~/.ssh/known_hosts&lt;/code&gt; line into the secret &lt;code&gt;SSH_KNOWN_HOSTS&lt;/code&gt;. Then I updated &lt;code&gt;deploy.yaml&lt;/code&gt; to use the secret instead:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;.forgejo/workflows/deploy.yaml&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;push]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deployscp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;docker&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo dir &amp;gt; ftpbatch
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo bye &amp;gt;&amp;gt; ftpbatch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;setup ssh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; mkdir -p ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0700 ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;${{ secrets.SSH_PRIV_KEY }}&amp;#34; &amp;gt; ~/.ssh/id_rsa_webserver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0600 ~/.ssh/id_rsa_webserver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;${{ secrets.SSH_KNOWN_HOST }}&amp;#34; &amp;gt; ~/.ssh/known_hosts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0600 ~/.ssh/known_hosts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;push public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo about to sftp
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; sftp -i ~/.ssh/id_rsa_webserver -b ./ftpbatch ${{ vars.WEBSERVER_SSH_USERNAME }}@${{ vars.WEBSERVER_SSH_HOST }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo did sftp&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I was hoping that Forgejo would preserve the line separators that I pasted into the secret dialog. It did! Nice!&lt;/p&gt;
&lt;h2 class="relative group"&gt;Add Hugo back to the build and really upload
&lt;div id="add-hugo-back-to-the-build-and-really-upload" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#add-hugo-back-to-the-build-and-really-upload" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;With a working sftp (and presumably a working ssh) and a good private key, I could add the &lt;code&gt;hugo&lt;/code&gt; step back. It packages up the build, then the &lt;code&gt;docker&lt;/code&gt; image retrieves the build and uploads the important bit. Here&amp;rsquo;s the whole enchilada:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;.forgejo/workflows/deploy.yaml&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;push]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;buildhugo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;hugo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;hugo version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;submodules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;recursive&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;run hugo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;hugo --gc --minify&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fix up RSS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cp ./public/index.xml ./public/rss.xml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;stash public files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://code.forgejo.org/forgejo/upload-artifact@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;./public/ &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deployscp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;docker&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;buildhugo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;grab public files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://code.forgejo.org/forgejo/download-artifact@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;setup ssh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; mkdir -p ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0700 ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;${{ secrets.SSH_PRIV_KEY }}&amp;#34; &amp;gt; ~/.ssh/id_rsa_webserver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0600 ~/.ssh/id_rsa_webserver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#34;${{ secrets.SSH_KNOWN_HOST }}&amp;#34; &amp;gt; ~/.ssh/known_hosts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; chmod 0600 ~/.ssh/known_hosts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;push public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ssh -i ~/.ssh/id_rsa_webserver ${{ vars.WEBSERVER_SSH_USERNAME }}@${{ vars.WEBSERVER_SSH_HOST }} &amp;#39;rm -rf /home/${{ vars.WEBSERVER_SSH_USERNAME }}/${{ vars.WEBSERVER_HTML_DIR }}/*&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; scp -r -i ~/.ssh/id_rsa_webserver ./public/* ${{ vars.WEBSERVER_SSH_USERNAME }}@${{ vars.WEBSERVER_SSH_HOST }}:/home/${{ vars.WEBSERVER_SSH_USERNAME }}/${{ vars.WEBSERVER_HTML_DIR }}/&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;This makes it look easy. It wasn&amp;rsquo;t. I was nervous about the process, and started with &lt;code&gt;echo ssh -i ~/...&lt;/code&gt; and &lt;code&gt;echo scp -r -i ~/...&lt;/code&gt; instead of actually running the ssh and scp commands. I made sure things looked right in the Actions window on the Forgejo server before I did the scp for real, and it took me a couple more deep breaths before I did the ssh (which does a &lt;code&gt;rm -rf&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Ultimately, I had to run things 70 times — I counted — before I got a working deploy. Lots of &lt;code&gt;echo&lt;/code&gt; and &lt;code&gt;ls -al&lt;/code&gt; to make sure I was oriented. And probably a third of the commits were fixes for this error:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Workflow was not executed due to an error that blocked the execution attempt.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Unable to parse supported events in workflow: yaml: line 25: found a tab character where an indentation space is expected&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I really need to set my Emacs up so it uses only spaces for yaml files. Esc-X untabify was my best friend.&lt;/p&gt;
&lt;p&gt;Now, I have a website that updates automatically when I push the code. Not when I commit — I&amp;rsquo;m still trying to remember that &lt;code&gt;git push origin&lt;/code&gt; after publishing. I think I&amp;rsquo;ll commit this and do that now.&lt;/p&gt;</description></item><item><title>Running Hugo from the runner on Forgejo</title><link>https://andrewmemory.acornwall.net/blog/2026-04-08-running-hugo-from-the-runner/</link><pubDate>Wed, 08 Apr 2026 22:33:06 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2026-04-08-running-hugo-from-the-runner/</guid><description>&lt;p&gt;I&amp;rsquo;m taking a shortcut here. With the benefit of foresight which is really hindsight, I &lt;a href="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/" &gt;set my runner up to run Hugo already&lt;/a&gt; with the label &lt;code&gt;hugo&lt;/code&gt;. So this is really about the automation I used to do the build. I started with attempts to install Hugo using &lt;code&gt;apk add&lt;/code&gt; with the Docker image. That was a side quest that led nowhere.&lt;/p&gt;
&lt;p&gt;Then when I decided to split things up, it took me a while to find the right image. The &lt;a href="https://hub.docker.com/r/gohugoio/hugo" target="_blank" rel="noreferrer"&gt;official Hugo Docker image&lt;/a&gt; luckily it includes Node.js so it can run the &lt;code&gt;checkout&lt;/code&gt; and &lt;code&gt;upload-artifact&lt;/code&gt; actions.&lt;/p&gt;
&lt;p&gt;However, you can&amp;rsquo;t do everything on the Hugo image. That means the build has to be split up into two parts. This part is only for running Hugo. Luckily, when it runs, Hugo can store the files it built (&lt;code&gt;./public/&lt;/code&gt;) in an artifact, so it&amp;rsquo;s not &lt;em&gt;too&lt;/em&gt; hard to get them later for uploading.&lt;/p&gt;
&lt;p&gt;You also need to check out the right thing. For Hugo with Blowfish installed, that means checking out submodules.&lt;/p&gt;
&lt;p&gt;If you want to avoid the shortcut and do what I really did, check out the &lt;a href="https://forgejo.org/docs/latest/user/actions/quick-start/" target="_blank" rel="noreferrer"&gt;Forgejo Actions quick start guide&lt;/a&gt; and build a &amp;ldquo;hello world&amp;rdquo; action first.&lt;/p&gt;
&lt;p&gt;This is the file I ended up creating in my local repo:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;.forgejo/workflows/deploy.yaml&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;push]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;buildhugo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;hugo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;hugo version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;submodules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;recursive&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;run hugo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;hugo --gc --minify&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fix up RSS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cp ./public/index.xml ./public/rss.xml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;stash public files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://code.forgejo.org/forgejo/upload-artifact@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;./public/ &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;This checks out the git repo with submodules, runs &lt;code&gt;hugo --gc --minify&lt;/code&gt; on that, copies index.xml to rss.xml because I started with Astro and didn&amp;rsquo;t want to have to migrate everyone&amp;rsquo;s links, then uploads the &lt;code&gt;./public/&lt;/code&gt; directory with the name &lt;code&gt;public&lt;/code&gt; to &lt;a href="https://forgejo.org/docs/next/user/actions/advanced-features/#artifacts" target="_blank" rel="noreferrer"&gt;Forgejo&amp;rsquo;s artifact repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve done that, you can:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add .forgejo/workflows/deploy.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push origin&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;That triggers the automation that will (first time) download the Hugo image, then build the website, then stash it in an artifact called &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But that&amp;rsquo;s only half the story for &lt;code&gt;deploy.yaml&lt;/code&gt;. &lt;a href="https://andrewmemory.acornwall.net/blog/2026-04-08-uploading-from-forgejo-to-the-webserver/" &gt;Read about uploading to the webserver here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Setting up a Forgejo runner for Hugo and others</title><link>https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/</link><pubDate>Thu, 02 Apr 2026 22:16:05 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/</guid><description>&lt;p&gt;I spent a lot of time messing around because I thought the runner was the thing that ran my actions. Nope, it&amp;rsquo;s the thing that runs the Docker image that builds your images. That makes life easier. Here&amp;rsquo;s how I set up mine:&lt;/p&gt;
&lt;h2 class="relative group"&gt;Get the runner token
&lt;div id="get-the-runner-token" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#get-the-runner-token" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;You need the runner token to configure a runner. So do that first. You need to be an admin to set up the runner.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Under the profile menu on the far right, click on &amp;ldquo;Site administration.&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Under &amp;ldquo;Admin settings&amp;rdquo; expand Actions to click on &amp;ldquo;Runners&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="The Admin settings panel. Actions has been expanded so you can see Runners"
width="255"
height="467"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/admin-runner.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/admin-runner.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/admin-runner.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/admin-runner.webp"&gt;&lt;/figure&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Over on the right, click the &amp;ldquo;Create a new runner&amp;rdquo; button. This opens a dialog with a token in it. Copy that token to a text editor.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="A dialog titled &amp;ldquo;How to start a runner&amp;rdquo;. The registration token text field has a copy button to the right of it"
width="282"
height="243"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/create-runner.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/create-runner.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/create-runner.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/create-runner.webp"&gt;&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Setting up a runner
&lt;div id="setting-up-a-runner" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#setting-up-a-runner" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;I spent a lot of time messing around because I thought the runner was the thing that ran my actions. Nope, it&amp;rsquo;s the thing that runs the Docker image that builds your images. That makes life easier.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make a directory for the runner, put the Dockerfile in it, and grab the image. These instructions look at lot like the ones at the &lt;a href="https://forgejo.org/docs/latest/admin/actions/runner-installation/#oci-image-installation" target="_blank" rel="noreferrer"&gt;OCI image installation&lt;/a&gt; instructions on the Forgejo website.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /data/shared/forgejo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://code.forgejo.org/forgejo/runner/raw/branch/main/Dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm data.forgejo.org/forgejo/runner:11 forgejo-runner --version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Create a &lt;code&gt;setup.sh&lt;/code&gt; in &lt;code&gt;runner/&lt;/code&gt; that looks like this. (Make sure to update the chown line to use the UID and GID you specified in the Forgejo docker-compose.yaml):&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;setup.sh&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /data/shared/forgejo/runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -e
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p data/.cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R 2000:2000 data
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;775&lt;/span&gt; data/.cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod g+s data/.cache&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Edit the Dockerfile to use your user info. Look for the USER line and put in your UID and GID:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Docker" data-lang="Docker"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;2000:2000&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Run setup.sh&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bash ./setup.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Create a docker-compose.yaml. Based on the Forgejo instructions, it should look like this (but remember to change the user: to your UID and GID). Yeah, I know it should be a different UID and GID but I&amp;rsquo;m lazy:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;docker-compose.yaml&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Docker" data-lang="Docker"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;version: &lt;span class="s1"&gt;&amp;#39;3.8&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt;services:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; docker-in-docker:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; image: docker:dind&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; container_name: &lt;span class="s1"&gt;&amp;#39;docker_dind&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; privileged: &lt;span class="s1"&gt;&amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; command: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dockerd&amp;#39;&lt;/span&gt;, &lt;span class="s1"&gt;&amp;#39;-H&amp;#39;&lt;/span&gt;, &lt;span class="s1"&gt;&amp;#39;tcp://0.0.0.0:2375&amp;#39;&lt;/span&gt;, &lt;span class="s1"&gt;&amp;#39;--tls=false&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; restart: &lt;span class="s1"&gt;&amp;#39;unless-stopped&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; runner:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; image: &lt;span class="s1"&gt;&amp;#39;data.forgejo.org/forgejo/runner:11&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; links:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - docker-in-docker&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; depends_on:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; docker-in-docker:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; condition: service_started&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; container_name: &lt;span class="s1"&gt;&amp;#39;runner&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; environment:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; DOCKER_HOST: tcp://docker-in-docker:2375&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; &lt;span class="c1"&gt;# User without root privileges, but with access to `./data`.&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; user: 2000:2000&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; volumes:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - ./data:/data&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; restart: &lt;span class="s1"&gt;&amp;#39;unless-stopped&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; command: &lt;span class="s1"&gt;&amp;#39;/bin/sh -c &amp;#34;while : ; do sleep 1 ; done ;&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt;&lt;span class="c"&gt;# command: &amp;#39;/bin/sh -c &amp;#34;sleep 5; forgejo-runner daemon --config config.yml&amp;#34;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="6"&gt;
&lt;li&gt;Fire up the runner and attach to the shell in the runner image.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it runner /bin/sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="7"&gt;
&lt;li&gt;From within the Docker image shell, run:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;forgejo-runner generate-config &amp;gt; config.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;forgejo-runner register&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="8"&gt;
&lt;li&gt;This will run a script. I used the following values. For the runner labels, what they really want is the label followed by a colon followed by the URL of the docker image, with each entry separated by commas. Since I wanted my runner to run Hugo, I added a label for hugo and a pointer to the Hugo image as well as a &amp;ldquo;docker&amp;rdquo; label which is one of the defaults:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Enter the Forgejo instance URL &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; example, https://next.forgejo.org/&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http://myforgejoserver.acornwall.net:3000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Enter the runner token:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;d41d8cd98f00b204e9800998ecf8427e32d6c11747e037155210
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Enter the runner name &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; empty, use hostname: 3b586057879f&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Enter the runner labels, leave blank to use the default labels &lt;span class="o"&gt;(&lt;/span&gt;comma-separated, &lt;span class="k"&gt;for&lt;/span&gt; example, ubuntu-20.04:docker://node:20-bookworm,ubuntu-18.04:docker://node:20-bookworm&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo:docker://ghcr.io/gohugoio/hugo:v0.152.2,docker:docker://data.forgejo.org/oci/node:20-bullseye&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="8"&gt;
&lt;li&gt;
&lt;p&gt;All that creates a &lt;code&gt;.runner&lt;/code&gt; file in the Docker image, which gets mapped to the &lt;code&gt;data&lt;/code&gt; directory that &lt;code&gt;setup.sh&lt;/code&gt; created.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now, shut the image down.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="10"&gt;
&lt;li&gt;Next, edit the docker-compose.yaml. Comment out the command that just spins, and uncomment the command that does the thing:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;docker-compose.yaml&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Docker" data-lang="Docker"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;...&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt;&lt;span class="c"&gt;# command: &amp;#39;/bin/sh -c &amp;#34;while : ; do sleep 1 ; done ;&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; command: &lt;span class="s1"&gt;&amp;#39;/bin/sh -c &amp;#34;sleep 5; forgejo-runner daemon --config config.yml&amp;#34;&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="11"&gt;
&lt;li&gt;Bring the runner back up:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="12"&gt;
&lt;li&gt;If you did everything right, you should see a runner show up under Admin settings - Actions - Runners.
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="A runner with status Idle, ID 9, name runner, version v11.3.1, type Global, labels hugo and docker, last online time 1 minute ago, and an edit button"
width="800"
height="174"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/runner.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/runner.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/runner.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/images/runner.webp"&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 class="relative group"&gt;Getting the runner to restart on reboot
&lt;div id="getting-the-runner-to-restart-on-reboot" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#getting-the-runner-to-restart-on-reboot" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Another bout with systemd, &amp;lsquo;cause we&amp;rsquo;d like this to start on reboot as well.&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;/etc/systemd/system/forgejo-runner.service&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-SYSTEMD" data-lang="SYSTEMD"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Forgejo Runner&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Requires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forgejo.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c &amp;#34;/usr/bin/docker compose -f /data/shared/forgejo/runner/docker-compose.yaml up --detach&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c &amp;#34;/usr/bin/docker compose -f /data/shared/forgejo/runner/docker-compose.yaml down&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;And then the requisite:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/usr/bin/docker compose -f /data/shared/forgejo/runner/docker-compose.yaml down
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start forgejo-runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status forgejo-runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl stop forgejo-runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; forgejo-runner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start forgejo-runner&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;</description></item><item><title>Installing a minimal Forgejo via Docker on Ubuntu 24.04</title><link>https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/</link><pubDate>Thu, 02 Apr 2026 20:54:05 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/</guid><description>&lt;p&gt;I&amp;rsquo;ve been wanting to play with &lt;a href="https://forgejo.org/" target="_blank" rel="noreferrer"&gt;Forgejo&lt;/a&gt; for a while. It&amp;rsquo;s the open source DevOps platform behind Codeberg and I like the idea of self-hosted automation. I wanted a minimal one — I didn&amp;rsquo;t need mail but I wanted automation on push.&lt;/p&gt;
&lt;p&gt;If you know Docker (the current version of Docker) you&amp;rsquo;ll find it pretty easy to install. Here&amp;rsquo;s how to do it.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Pull Forgejo from Docker
&lt;div id="pull-forgejo-from-docker" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#pull-forgejo-from-docker" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Ubuntu 24.04 doesn&amp;rsquo;t come with the new Docker, or sqlite3. So:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install docker-compose-v2 sqlite3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Grab the Docker image:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker pull codeberg.org/forgejo/forgejo:14&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Create a docker-compose.yaml based on the one at &lt;a href="https://forgejo.org/docs/next/admin/installation/docker/" target="_blank" rel="noreferrer"&gt;forgejo.org/docs/next/admin/installation/docker/&lt;/a&gt;. I changed three places the &lt;code&gt;USER_UID&lt;/code&gt;, &lt;code&gt;USER_GID&lt;/code&gt;, and the path to the volume (I used a hard-coded one rather than &lt;code&gt;./forgejo&lt;/code&gt;. The file I ended up with was:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;docker-compose.yaml&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Docker" data-lang="Docker"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;networks:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; forgejo:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; external: false&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt;services:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; server:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; image: codeberg.org/forgejo/forgejo:14&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; container_name: forgejo&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; environment:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - &lt;span class="nv"&gt;USER_UID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - &lt;span class="nv"&gt;USER_GID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; restart: always&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; networks:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - forgejo&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; volumes:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - /data/shared/forgejo/forjego:/data ← was ./forgejo:/data&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - /etc/localtime:/etc/localtime:ro&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; ports:&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - &lt;span class="s1"&gt;&amp;#39;3000:3000&amp;#39;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;&lt;/span&gt; - &lt;span class="s1"&gt;&amp;#39;222:22&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Bring the image up:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="5"&gt;
&lt;li&gt;
&lt;p&gt;Open a browser on the host&amp;rsquo;s port 3000 (which I&amp;rsquo;ll call myforgejoserver.acornwall.net:3000) to initialize Forgejo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the browser, leave everything as default except the server domain and base URL. That means I&amp;rsquo;m using the default sqlite&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="The onboarding screen. All the defaults are checked except the server domain, which is masked, and the base URL, which is masked except you can see it starts with http and ends with :3000/"
width="800"
height="702"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-1.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-1.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-1.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-1.webp"&gt;&lt;/figure&gt;
&lt;ol start="7"&gt;
&lt;li&gt;I skipped email settings and server/third-party service settings, and entered the administrator account settings, then clicked Install Forgejo:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="More of the onboarding screen. Email settings and server/third party settings are collapsed. The administrator username is andrew, the email address is andrewmemoryblog@gmail.com, and the password is masked. A button at the bottom reads &amp;ldquo;Install Forgejo&amp;rdquo;."
width="800"
height="503"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-2.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-2.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-2.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/onboarding-2.webp"&gt;&lt;/figure&gt;
&lt;ol start="8"&gt;
&lt;li&gt;Next I restarted Forgejo and made sure that the settings stuck. If you have the storage volume wrong in the docker-compose.yaml, as I did the first time, you get to enter everything again after fixing it.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Creating a Git Repo Into Forgejo
&lt;div id="creating-a-git-repo-into-forgejo" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#creating-a-git-repo-into-forgejo" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Once things were set up, I could log in and create a repo in Forgejo. Here&amp;rsquo;s what I did.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Navigate to the + dropdown and click &amp;ldquo;New repository&amp;rdquo;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fill out the form. I called my repo &amp;ldquo;andrewmemory&amp;rdquo; and set my user as the owner. I checked &amp;ldquo;Make repostitory private.&amp;rdquo; I also checked &amp;ldquo;Initialize repository&amp;rdquo; which lead to some issues later, but hey, it looked good at the time. I added both &amp;ldquo;Emacs&amp;rdquo; and &amp;ldquo;Hugo&amp;rdquo; to .gitignore — I didn&amp;rsquo;t realize I could do both the first time I tried. I went with CC-BY-NC-ND-4.0 for the license. Then I clicked &amp;ldquo;Create repository.&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="The repository creation screen. Owner is andrew, repo name is andrewmemory. Make repository private and initialize repository are checked. .gitignore has both Emacs and Hugo, and the license is CC-BY-NC-ND-4.0. A button at the bottom reads &amp;ldquo;Create repository&amp;rdquo;."
width="800"
height="939"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/new-repo.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/new-repo.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/new-repo.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/new-repo.webp"&gt;&lt;/figure&gt;
&lt;p&gt;There, the repo is created!&lt;/p&gt;
&lt;h2 class="relative group"&gt;Verifying your public key
&lt;div id="verifying-your-public-key" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#verifying-your-public-key" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Once the repo was created on Forgejo, I needed to add my private cert to Forgejo. That meant clicking the dropdown on the right with my avatar, clicking &amp;ldquo;Settings,&amp;rdquo; then navigating to &amp;ldquo;SSH / GPG keys&amp;rdquo;. From there I could click &amp;ldquo;Add key&amp;rdquo; under SSH Keys. I pasted my public key in there. Then click &amp;ldquo;Add key.&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once you&amp;rsquo;ve done that, you&amp;rsquo;ve got something like this. Click &amp;ldquo;Verify&amp;rdquo;:
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="A key called Test key. Two buttons are on the right: &amp;ldquo;Remove&amp;rdquo; and &amp;ldquo;Verify&amp;rdquo;"
width="800"
height="73"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/verify-button.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/verify-button.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/verify-button.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/verify-button.webp"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;After clicking verify, you&amp;rsquo;re presented with a token and a string to paste into a terminal. Copy the string, paste it into the terminal, but edit the filename if your private key is not &lt;code&gt;id_ed25519&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -n &lt;span class="s1"&gt;&amp;#39;ee2cd80b5f8f4e95800ddca4d92053998bc3f8e7a9cc1dd60fe30e7ad83b87d6&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; ssh-keygen -Y sign -n myforgejoserver.acornwall.net -f ~/.ssh/id_rsa&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Running that command will dump a wad into your terminal that looks like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;-----BEGIN SSH SIGNATURE-----
U1mIU0lHAAAAAQA
...
&amp;#43;wrNHO9W
-----END SSH SIGNATURE-----&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;ol start="5"&gt;
&lt;li&gt;
&lt;p&gt;Copy the whole wad including the BEGIN and END parts, then paste that into the section that says &amp;ldquo;Armored SSH signature&amp;rdquo; and press the &amp;ldquo;Verify&amp;rdquo; button.
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="A text field that is labeled Armored SSH signature. The text field begins with &amp;mdash;&amp;ndash;BEGIN SSH SIGNATURE&amp;mdash;&amp;ndash; and has random looking characters until the end, where it says &amp;mdash;&amp;ndash;END SSH SIGNATURE&amp;mdash;&amp;ndash;. Below that are Verify and Cancel buttons."
width="800"
height="203"
src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/armored-signature.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/armored-signature.webp 800w, https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/armored-signature.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/images/armored-signature.webp"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you&amp;rsquo;ve done everything right your key is now verified.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 class="relative group"&gt;Connecting the old repo
&lt;div id="connecting-the-old-repo" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#connecting-the-old-repo" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Finally, I could start moving my website (which is in a git repo on disk) into Forgejo. That wasn&amp;rsquo;t terrible, although it took me a couple of tries because I wasn&amp;rsquo;t sure how to do it. Here&amp;rsquo;s what I did.&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; andrewmemory-hugo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git remote rm origin &lt;span class="c1"&gt;# It took me a couple of tries, so I had to remove the old origin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git remote add origin ssh://git@myforgejoserver.acornwall.net:222/andrew/andrewmemory
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git pull --allow-unrelated origin main&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;This resulted in a bunch of merge conflicts in .gitignore that I manually took care of. Once that was done, I could:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push --set-upstream origin main&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Setting Forgejo to restart after reboot
&lt;div id="setting-forgejo-to-restart-after-reboot" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#setting-forgejo-to-restart-after-reboot" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;This is all groovy, but next time I install Linux patches, I&amp;rsquo;m going to have to reboot my machine. So I created a systemd (&lt;a href="https://andrewmemory.acornwall.net/blog/2026-02-15-setting-up-systemd-resolv-conf/" &gt;boo&lt;/a&gt;) service to do that.&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;/etc/systemd/system/forgejo.service&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-SYSTEMD" data-lang="SYSTEMD"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Forgejo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Requires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c &amp;#34;/usr/bin/docker-compose -f /data/shared/forgejo/docker-compose.yaml up --detach&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c &amp;#34;/usr/bin/docker-compose -f /data/shared/forgejo/docker-compose.yaml down&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Then, I made sure it worked and enabled it on boot:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start forgejo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status forgejo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl stop forgejo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; forgejo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start forgejo&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Ok, Forgejo is set up!&lt;/p&gt;</description></item><item><title>Website automation with Forgejo</title><link>https://andrewmemory.acornwall.net/blog/2026-03-29-website-automation-with-forgejo/</link><pubDate>Sun, 29 Mar 2026 21:54:05 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2026-03-29-website-automation-with-forgejo/</guid><description>&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="The Forgejo word mark"
width="300"
height="113"
src="https://andrewmemory.acornwall.net/blog/2026-03-29-website-automation-with-forgejo/images/forgejo-wordmark.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-03-29-website-automation-with-forgejo/images/forgejo-wordmark.webp 800w, https://andrewmemory.acornwall.net/blog/2026-03-29-website-automation-with-forgejo/images/forgejo-wordmark.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-03-29-website-automation-with-forgejo/images/forgejo-wordmark.webp"&gt;&lt;/figure&gt;
&lt;p&gt;If you see this, then my Forgejo web automation is working.&lt;/p&gt;
&lt;p&gt;It took a few steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2026-04-02-installing-forgejo/" &gt;Installing Forgejo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2026-04-02-setting-up-a-forgejo-runner-for-hugo-and-others/" &gt;Setting up the Forgejo runner for Hugo and others&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2026-04-08-running-hugo-from-the-runner/" &gt;Figuring out how to run Hugo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2026-04-08-uploading-from-forgejo-to-the-webserver/" &gt;Figuring out how to upload&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Setting up systemd resolv.conf</title><link>https://andrewmemory.acornwall.net/blog/2026-02-15-setting-up-systemd-resolv-conf/</link><pubDate>Sun, 15 Feb 2026 23:08:05 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2026-02-15-setting-up-systemd-resolv-conf/</guid><description>&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="A meme from Scooby Doo. Fred is taking the mask off a ghost labelled systemd, to discover it&amp;rsquo;s really svchost.exe"
width="300"
height="400"
src="https://andrewmemory.acornwall.net/blog/2026-02-15-setting-up-systemd-resolv-conf/images/systemd.webp"
srcset="https://andrewmemory.acornwall.net/blog/2026-02-15-setting-up-systemd-resolv-conf/images/systemd.webp 800w, https://andrewmemory.acornwall.net/blog/2026-02-15-setting-up-systemd-resolv-conf/images/systemd.webp 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2026-02-15-setting-up-systemd-resolv-conf/images/systemd.webp"&gt;&lt;/figure&gt;
&lt;p&gt;Like every intelligent person, I hate systemd. But Linux seems determined to undo all good things, and Debian has adopted it.&lt;/p&gt;
&lt;p&gt;In my Ubuntu 22.04 install, I&amp;rsquo;d disabled it and gone back to normal resolv.conf resolution. That didn&amp;rsquo;t work for 24.04. So I had to figure out the stupid resolution the systemd way.&lt;/p&gt;
&lt;p&gt;To fix DNS on Ubuntu 24.04 — which, I will note, works perfectly for every other system that gets provisioned from my DHCP server — I had to go into /etc/systemd/resolved.conf and uncomment a bunch of stupid crap.&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;figcaption&gt;&lt;span&gt;&lt;mark&gt;/etc/systemd/resolved.conf&lt;/mark&gt;&lt;/span&gt; &lt;/figcaption&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-systemd" data-lang="systemd"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Resolve]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;DNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;(my DNS server IP address)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Domains&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;(my local domain)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;DNSSEC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;DNSStubListener&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;</description></item><item><title>Fixing buffer bloat on OpenBSD 7.2</title><link>https://andrewmemory.acornwall.net/blog/2025-02-01-fixing-bufferbloat-on-openbsd/</link><pubDate>Sat, 01 Feb 2025 01:28:19 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2025-02-01-fixing-bufferbloat-on-openbsd/</guid><description>&lt;p&gt;I learned a little today about buffer bloat, which is latency introduced when uploading/downloading. After that, I headed straight over to &lt;a href="https:///www.waveform.com/tools/bufferbloat" target="_blank" rel="noreferrer"&gt;WaveForm&amp;rsquo;s buffer bloat test&lt;/a&gt; and got a D.&lt;/p&gt;
&lt;p&gt;Luckily, the WaveForm page pointed me to a solution, which I found at &lt;a href="https://www.pauladamsmith.com/blog/2018/07/fixing-bufferbloat-on-your-home-network-with-openbsd-6.2-or-newer.html" target="_blank" rel="noreferrer"&gt;Paul Smith&amp;rsquo;s blog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Following Paul&amp;rsquo;s instructions, I added a Queue section to my /etc/pf.conf:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;#---------------------------------#
# Queues
#---------------------------------#
queue outq on $ext_if flows 1024 bandwidth 10M max 10M qlimit 1024 default
queue inq on $lan_if flows 1024 bandwidth 225M max 225M qlimit 1024 default&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;then reloaded the rules:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;doas pfctl -n -f /etc/pf.conf &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; doas pfctl -f /etc/pf.conf&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I didn&amp;rsquo;t get an A, but I did move up to a C, which means I&amp;rsquo;ve at least halved the latency problem. There&amp;rsquo;s a warning in a comment on Paul Smith&amp;rsquo;s page:&lt;/p&gt;
&lt;p&gt;&amp;ldquo;This works well if you have OpenBSD acting as a firewall/NAT appliance only, but doesn&amp;rsquo;t work if you have multiple downstream interfaces, or run any services on the OpenBSD server at all (facing either the LAN &lt;del&gt;or&lt;/del&gt; the internet).&amp;rdquo; The commenter suggests an explict parent queue with the whole LAN bandwidth and subqueues for internet and everything else.&lt;/p&gt;
&lt;p&gt;Anyway, I went to &lt;a href="https://speed.cloudflare.com/" target="_blank" rel="noreferrer"&gt;CloudFlare&amp;rsquo;s speed test&lt;/a&gt; and I&amp;rsquo;m &amp;ldquo;great.&amp;rdquo;&lt;/p&gt;</description></item><item><title>Block Ad Sites and Nasties on OpenBSD 7.4</title><link>https://andrewmemory.acornwall.net/blog/2023-10-22-block-ad-sites-and-nasties-on-openbsd-7-4/</link><pubDate>Sun, 22 Oct 2023 22:03:47 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2023-10-22-block-ad-sites-and-nasties-on-openbsd-7-4/</guid><description>&lt;p&gt;One of the benefits of building your own firewall is that you get to decide what you want to block. I&amp;rsquo;d been using a list from &lt;a href="https://pgl.yoyo.org/adservers" target="_blank" rel="noreferrer"&gt;pgl.yoyo.org/adservers&lt;/a&gt;. There&amp;rsquo;s a utility called &lt;a href="https://www.geoghegan.ca/pfbadhost.html" target="_blank" rel="noreferrer"&gt;Pf-badhost&lt;/a&gt; that blocks evil hosts. I wanted to do a little more: block bad IPs (sorry, Cloudflare, I know you hate that) and have better control over when things get updated.&lt;/p&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="A virus/malware hazard icon"
width="335"
height="334"
src="https://andrewmemory.acornwall.net/blog/2023-10-22-block-ad-sites-and-nasties-on-openbsd-7-4/images/virus_malware_hazard_icon.png"
srcset="https://andrewmemory.acornwall.net/blog/2023-10-22-block-ad-sites-and-nasties-on-openbsd-7-4/images/virus_malware_hazard_icon.png 800w, https://andrewmemory.acornwall.net/blog/2023-10-22-block-ad-sites-and-nasties-on-openbsd-7-4/images/virus_malware_hazard_icon.png 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2023-10-22-block-ad-sites-and-nasties-on-openbsd-7-4/images/virus_malware_hazard_icon.png"&gt;&lt;/figure&gt;
&lt;p&gt;So I ultimately decided to adapt some of Pf-badhost to a few scripts I created. First, a script to get the latest list of bad hostnames from pgl.yoyo.org — grab-bad-hosts.sh:&lt;/p&gt;
&lt;h2 class="relative group"&gt;Grabbing Bad Hostnames
&lt;div id="grabbing-bad-hostnames" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#grabbing-bad-hostnames" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;#! /bin/sh
/usr/local/bin/wget -O ./pgl-adhosts.conf &amp;#39;https://pgl.yoyo.org/adservers/serverlist.php?hostformat=unbound&amp;amp;showintro=1&amp;amp;mimetype=plaintext&amp;#39;
grep -v -f /var/unbound/etc/unbound-whitelist-ads.txt pgl-adhosts.conf &amp;gt; unbound-adhosts.conf
grep -f /var/unbound/etc/unbound-whitelist-ads.txt pgl-adhosts.conf &amp;gt; unbound-adhosts-whitelist.conf
echo &amp;#34;In a root shell, run:&amp;#34;
echo &amp;#34;cat unbound-adhosts.conf &amp;gt; /var/unbound/etc/unbound-adhosts.conf&amp;#34;
echo &amp;#34;cat unbound-adhosts-whitelist.conf &amp;gt; /var/unbound/etc/unbound-adhosts-whitelist.conf&amp;#34;
echo &amp;#34;rcctl restart unbound&amp;#34;
echo &amp;#34;(or run root-update-hosts.sh as root)&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;This builds two files: /var/unbound/etc/unbound-adhosts.conf and /var/unbound/etc/unbound-adhosts-whitelists.conf based on a file I created, /var/unbound/etc/unbound-whitelist-ads.txt, which I had to add to make other users happy. /var/unbound/etc/unbound-whitelist-ads.txt looks like this:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;adservice.google.com[^.]
googleadservices.com[^.]&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;&amp;hellip; and I&amp;rsquo;m going to set up unbound to allow one host to have the whitelisted ads.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Grabbing &lt;strong&gt;B&lt;/strong&gt;ad IPs
&lt;div id="grabbing-bad-ips" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#grabbing-bad-ips" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;I used Pf-badhost as a source of places to grab bad IP addresses from. I ended up with this script, grab-bad-ips.sh:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;#! /bin/sh
/usr/local/bin/wget -O ./banlist_firehol_level1 https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset
/usr/local/bin/wget -O ./banlist_firehol_level2 https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset
/usr/local/bin/wget -O ./banlist_binarydefence https://www.binarydefense.com/banlist.txt
/usr/local/bin/wget -O ./banlist_emergingthreats https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt
cat ./banlist_firehol_level1 &amp;gt; pf-badguys.table
printf &amp;#34;\n&amp;#34;&amp;gt;&amp;gt; pf-badguys.table
cat ./banlist_firehol_level2 &amp;gt;&amp;gt; pf-badguys.table
printf &amp;#34;\n&amp;#34;&amp;gt;&amp;gt; pf-badguys.table
cat ./banlist_binarydefence &amp;gt;&amp;gt; pf-badguys.table
printf &amp;#34;\n&amp;#34;&amp;gt;&amp;gt; pf-badguys.table
cat ./banlist_emergingthreats &amp;gt;&amp;gt; pf-badguys.table
printf &amp;#34;\n&amp;#34;&amp;gt;&amp;gt; pf-badguys.table
grep &amp;#39;^[0-9]&amp;#39; pf-badguys.table | sort | uniq &amp;gt; pf-badguys.table.sort.uniq
mv pf-badguys.table.sort.uniq pf-badguys.table
echo &amp;#34;In a root shell, run:&amp;#34;
echo &amp;#34; cat pf-badguys.table &amp;gt; /etc/pf-badguys.table&amp;#34;
echo &amp;#34; pfctl -F Tables -f /etc/pf.conf&amp;#34;
echo &amp;#34;(or run root-update-ips.sh as root)&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;This script creates /etc/pf-badguys.table, which I&amp;rsquo;ll plug into my PF configuration.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Configuring unbound to use the bad hosts lists
&lt;div id="configuring-unbound-to-use-the-bad-hosts-lists" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configuring-unbound-to-use-the-bad-hosts-lists" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s not hard to have unbound import the bad hosts, which are already formatted to redirect to 127.0.0.1. Here&amp;rsquo;s a sample from /var/unbound/etc/unbound-adhosts.conf:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;local-zone: &amp;#34;1-1ads.com&amp;#34; redirect
local-data: &amp;#34;1-1ads.com A 127.0.0.1&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;unbound-adhosts-whitelist.conf looks the same, but only contains the whitelisted ad servers. After the last local-data:/local-data-ptr: pair for my network, I added the following:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt; # open a hole for ad servers
# 192.168.150.180 is unblocked desktop
access-control-view: 192.168.150.180/32 adview&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;After my access-control: directives, I added:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# Host addresses - spam to block
#
include: /var/unbound/etc/unbound-adhosts.conf
include: /var/unbound/etc/unbound-adhosts-whitelist.conf
include: /var/unbound/etc/unbound-adhosts-local.conf&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;At the end of the unbound.conf file I added:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;view:
name: &amp;#34;adview&amp;#34;
include: /var/unbound/etc/unbound-adhosts.conf
include: /var/unbound/etc/unbound-adhosts-local.conf&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Then a quick:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# unbound-checkconf
# rcctl restart unbound&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;&amp;hellip; and I was blocking ad servers. I could update this in cron, but I prefer to do it manually every week or so.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Configuring PF to block the bad IP addresses
&lt;div id="configuring-pf-to-block-the-bad-ip-addresses" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configuring-pf-to-block-the-bad-ip-addresses" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;I know there&amp;rsquo;s an argument for not blocking bad IP addresses. After all, there may be legitimate sites that are hosted on the same IP address. But&amp;hellip; so far I&amp;rsquo;ve only run into one. So I&amp;rsquo;m happy to continue blocking them. In my pf.conf, after the &lt;martians&gt; table, I added a &lt;badguys&gt; table:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;table &amp;lt;badguys&amp;gt; persist file &amp;#34;/etc/pf-badguys.table&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Then after the block for martians, I added an equivalent for badguys:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# Block addresses in the badguys table
# We use the &amp;#34;quick&amp;#34; parameter here to make this rule the last.
# Note that badguys might contain martians, but they get handled before
# this rule.
block in quick on $ext_if from &amp;lt;badguys&amp;gt; to any
block return out quick on $ext_if from any to &amp;lt;badguys&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Then &lt;code&gt;pfctl -F Tables -f /etc/pf.conf&lt;/code&gt; to reread the table.&lt;/p&gt;
&lt;p&gt;This post is part of a series on &lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/" &gt;setting up an OpenBSD 7.4 firewall device&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Setting up Wireguard on an OpenBSD 7.4 firewall device</title><link>https://andrewmemory.acornwall.net/blog/2023-10-22-setting-up-wireguard-on-an-openbsd-7-4-firewall-device/</link><pubDate>Sun, 22 Oct 2023 21:09:01 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2023-10-22-setting-up-wireguard-on-an-openbsd-7-4-firewall-device/</guid><description>&lt;p&gt;It took me a little while after I set up my firewall device to set up Wireguard as a VPN. It&amp;rsquo;s probably something I should have done right away — the benefits of being able to log in from home (and block ads) while on the road is really nice.&lt;/p&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="The Wireguard logo"
width="300"
height="300"
src="https://andrewmemory.acornwall.net/blog/2023-10-22-setting-up-wireguard-on-an-openbsd-7-4-firewall-device/images/wireguard_logo-1.png"
srcset="https://andrewmemory.acornwall.net/blog/2023-10-22-setting-up-wireguard-on-an-openbsd-7-4-firewall-device/images/wireguard_logo-1.png 800w, https://andrewmemory.acornwall.net/blog/2023-10-22-setting-up-wireguard-on-an-openbsd-7-4-firewall-device/images/wireguard_logo-1.png 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2023-10-22-setting-up-wireguard-on-an-openbsd-7-4-firewall-device/images/wireguard_logo-1.png"&gt;&lt;/figure&gt;
&lt;p&gt;Wireguard needs a publicly available IP or domain name. I used DuckDNS. I &lt;a href="https://andrewmemory.acornwall.net/blog/2023-05-20-setting-up-duckdns-on-openbsd/" &gt;posted about that&lt;/a&gt; a while back so I won&amp;rsquo;t do it again here, but you&amp;rsquo;ll need to do that first.&lt;/p&gt;
&lt;p&gt;Wireguard is in the kernel in 7.4. Prior releases required you to &lt;code&gt;pkg_add wireguard_tools&lt;/code&gt;, but these days you don&amp;rsquo;t need to.&lt;/p&gt;
&lt;p&gt;The way Wireguard works is that you generate public and private keys. Each device gets its own private key, and you share the public keys. Wireguard has tools to do that. On the OpenBSD firewall side:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# mkdir /etc/wireguard
# chmod 700 /etc/wireguard
# wg genkey &amp;gt; /etc/wireguard/private.key
# chmod 600 /etc/wireguard/private.key
# wg pubkey &amp;lt; private.key &amp;gt; public.key&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Next, you need to generate private and public keys on each of your clients. Then set up /etc/wireguard/wg0.conf to contain the firewall&amp;rsquo;s private key and also the public keys of the client. I decided to do this on a completely different network — 172.16. I assigned individual IP addresses for each device. It&amp;rsquo;s a little more management headache, but makes it easy to delete something if I lose a device. The /etc/wireguard/wg0.conf looks like this:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;[Interface]
PrivateKey = the private key from /etc/wireguard/private.key
ListenPort = 51820
[Peer]
# My first peer - a laptop
PublicKey = the public key from the laptop
AllowedIPs = 172.16.0.2
[Peer]
# My second peer - an Android device running Wireguard from F-Droid
PublicKey = the public key from the device
AllowedIPs = 172.16.0.3
# ... etc...&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Obviously, on each device you have to do the reverse: specify the private key generated on device, and put the firewall&amp;rsquo;s public key in as the peer. Next, you need an /etc/hostname.wg0 to bring the network up:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;inet 172.16.0.1 255.255.255.0 NONE up
!/usr/local/bin/wg setconf wg0 /etc/wireguard/wg0.conf&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Before you can go further, you need to unblock the Wireguard interface in /etc/pf.conf:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;(after the lan_if macro)
vpn_if=&amp;#34;wg0&amp;#34;
vpn_port=&amp;#34;51820&amp;#34;
(at the end after the NAT rules)
#---------------------------------#
# WireGuard
#---------------------------------#
pass in on $vpn_if
pass in inet proto udp from any to any port $vpn_port
pass out on egress inet from ($vpn_if:network) nat-to ($ext_if:0)&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;From there, you can restart pf and reload the new rules and &lt;code&gt;sh /etc/netstart wg0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This post is part of a series on &lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/" &gt;setting up an OpenBSD 7.4 firewall device&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Set up networking for an OpenBSD 7.4 firewall device</title><link>https://andrewmemory.acornwall.net/blog/2023-10-21-set-up-networking-for-an-openbsd-7-4-firewall-device/</link><pubDate>Sat, 21 Oct 2023 17:14:44 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2023-10-21-set-up-networking-for-an-openbsd-7-4-firewall-device/</guid><description>&lt;p&gt;I had a few wrinkles because I&amp;rsquo;d already set up networking, and I didn&amp;rsquo;t want to have to go through and redo all my static IPs. But I wanted to make my network more rational. I had devices on 192.168.150.*/24 and I wanted something bigger, so I decided to go with /20. That gave me a range of 92.168.144.1–192.168.159.254. (I admit I used an &lt;a href="https://www.calculator.net/ip-subnet-calculator.html" target="_blank" rel="noreferrer"&gt;IP subnet calculator&lt;/a&gt; for that.) My ISP gives me IPv4, so I&amp;rsquo;m using that and not worrying about IPv6.&lt;/p&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt=""
width="221"
height="172"
src="https://andrewmemory.acornwall.net/blog/2023-10-21-set-up-networking-for-an-openbsd-7-4-firewall-device/images/wafiz14.jpg"
srcset="https://andrewmemory.acornwall.net/blog/2023-10-21-set-up-networking-for-an-openbsd-7-4-firewall-device/images/wafiz14.jpg 800w, https://andrewmemory.acornwall.net/blog/2023-10-21-set-up-networking-for-an-openbsd-7-4-firewall-device/images/wafiz14.jpg 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2023-10-21-set-up-networking-for-an-openbsd-7-4-firewall-device/images/wafiz14.jpg"&gt;&lt;/figure&gt;
&lt;p&gt;First things first: let&amp;rsquo;s make the box forward packets:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# sysctl net.inet.ip.forwarding=1
# echo &amp;#39;net.inet.ip.forwarding=1&amp;#39; &amp;gt;&amp;gt; /etc/sysctl.conf&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Configure Network Adapters
&lt;div id="configure-network-adapters" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configure-network-adapters" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;I have two Intel NICs. I&amp;rsquo;m connecting igc0 to my cable modem. I&amp;rsquo;m connecting igc1 to a 16-port switch which is my internal network. I&amp;rsquo;ve got another network port that was autodetected, but I&amp;rsquo;m not using it yet, so I&amp;rsquo;ll remove it.&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# echo &amp;#34;inet autoconf&amp;#34; &amp;gt; /etc/hostname.igc0
# echo &amp;#34;inet 192.168.144.1 255.255.240.0 NONE&amp;#34; &amp;gt; /etc/hostname.igc1
# rm /etc/hostname.igc2
# sh /etc/netstart.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Setting up the PF packet filter
&lt;div id="setting-up-the-pf-packet-filter" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#setting-up-the-pf-packet-filter" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Now, set up PF. I got my instructions for this mostly from &lt;a href="https://home.nuug.no/~peter/pf/en/ftpproblem.html" target="_blank" rel="noreferrer"&gt;home.nuug.no/~peter/pf/en/ftpproblem.html&lt;/a&gt;. Here&amp;rsquo;s how I edited /etc/pf.conf:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;#---------------------------------#
# Macros
#---------------------------------#
ext_if=&amp;#34;igc0&amp;#34;
lan_if=&amp;#34;igc1&amp;#34;
ftpproxy=&amp;#34;127.0.0.1&amp;#34;
ftpproxyport=&amp;#34;8021&amp;#34;
#---------------------------------#
# Tables
#---------------------------------#
table &amp;lt;localonly&amp;gt; { \
# Addresses that can talk to the local network but not to the rest of the world
\
# Local printer
192.168.158.1/32 \
}
# This is a table of non-routable private addresses.
table &amp;lt;martians&amp;gt; { 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 \
172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 224.0.0.0/3 \
192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 \
203.0.113.0/24 }
#---------------------------------#
# Protect and block by default
#---------------------------------#
set skip on lo0
set block-policy drop
# Spoofing protection for all NICs.
block in from no-route
block in quick from urpf-failed
# Block non-routable private addresses.
# We use the &amp;#34;quick&amp;#34; parameter here to make this rule the last.
block in quick on $ext_if from &amp;lt;martians&amp;gt; to any
block return out quick on $ext_if from any to &amp;lt;martians&amp;gt;
# Default blocking all traffic in on all LAN NICs from any computer or device
# attached.
block return in on { $lan_if }
# Default blocking all traffic in on the external NIC from the Internet/ISP,
# we&amp;#39;ll log that too.
block drop in log on $ext_if
# Don&amp;#39;t allow ICMP from outside. Commented out this section.
# Yeah, I know people hate that.
#
#match in on $ext_if inet proto icmp icmp-type {echoreq } tag ICMP_IN
#block drop in on $ext_if proto icmp
#pass in proto icmp tagged ICMP_IN max-pkt-rate 100/10
# We need the router to have access to the Internet, so we&amp;#39;ll default allow
# packets to pass out from our router through the external NIC to the Internet.
pass out inet from $ext_if
#---------------------------------#
# LAN Setup
#---------------------------------#
# Allow any computer or device on the LAN to send data packets in through the NIC.
# This means any computer attached to this network interface can pass in data
# reaching anywhere, i.e. the Internet or any of the computers attached to the
# router.
pass in on $lan_if
# Always block DNS queries not addressed to our DNS server.
block return in quick on $lan_if proto { udp tcp } to ! $lan_if port { 53 853 }
# Block localonly from seeing the internet
block in quick on lan_if from &amp;lt;localonly&amp;gt;
# Allow data packets to pass from the router out through the NIC to the
# computers or devices attached to it on the lan NIC.
# Without this we can&amp;#39;t even ping computers attached to the lan NIC from
# the router itself.
pass out on $lan_if inet keep state
#---------------------------------#
# FTP
#---------------------------------#
# allow ftp clients to work
anchor &amp;#34;ftp-proxy/*&amp;#34;
pass in quick on $lan_if inet proto tcp to port ftp divert-to $ftpproxy port $ftpproxyport
# no ftp servers so don&amp;#39;t pass out
# pass out proto tcp from $ftpproxy to any port ftp
#---------------------------------#
# NAT
#---------------------------------#
pass out on $ext_if inet from $lan_if:network to any nat-to ($ext_if)&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Then you can test things out and restart pf. I think my network connection dropped at this point:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# pfctl -n -f /etc/pf.conf
# pfctl -F all
# pfctl -f /etc/pf.conf&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Configure dhcpd
&lt;div id="configure-dhcpd" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configure-dhcpd" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Now I need to configure serving IPs from this machine. (My old firewall is still on the network at this point and serving real IPs as well as doing DNS, but I need to get the new firewall enabled.) So time to edit /etc/dhcpd.conf:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# $OpenBSD: dhcpd.conf,v 1.1 1998/08/19 04:25:45 form Exp $
#
# DHCP server options.
# See dhcpd.conf(5) and dhcpd(8) for more information.
#
# This is for 192.168.144.* to 192.168.159.*
subnet 192.168.144.0 netmask 255.255.240.0 {
option domain-name &amp;#34;lan.example.net&amp;#34;;
option domain-name-servers 192.168.144.1;
option routers 192.168.144.1;
########################################
# Dynamic IP addresses
########################################
range 192.168.145.1 192.168.149.254;
########################################
# Fixed IP machines that humans don&amp;#39;t
# (normally) see - WAPs etc.
########################################
host wap {
hardware ethernet dc:9f:44:11:22:33;
fixed-address 192.168.144.13;
}
########################################
# Fixed IP machines that humans see.
########################################
# These are machines I normally ssh
# or telnet to, or run servers on.
#
host fileserver {
hardware ethernet 22:33:44:55:66:77;
fixed-address 192.168.150.171;
option host-name &amp;#34;fileserver&amp;#34;;
}
host weather {
hardware ethernet 88:99:00:AA:BB:CC;
fixed-address 192.168.150.177;
option host-name &amp;#34;weather&amp;#34;;
}
#... etc...
# I&amp;#39;m putting my laptop on a non-150 address for testing purposes.
# That way I can plug it into the server and get a DHCP address that
# should route on to the internet.
host laptop {
hardware ethernet 00:22:44:66:88:ff;
fixed-address 192.168.161.2;
}
} # subnet&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Next, start serving DHCP:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;rcctl enable dhcpd
rcctl start dhcpd&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;At this point I could plug my laptop into the new firewall and get an IP address.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Start Unbound for DNS
&lt;div id="start-unbound-for-dns" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#start-unbound-for-dns" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;On my old firewall I was using bind. That was heavyweight, so I decided to do something different here. Originally I thought I needed NSD and Unbound for DNS (I was looking at some instructions at &lt;a href="https://jamsek.dev/blog/2019/Jul/28/openbsd-dns-server-with-unbound-and-nsd/" target="_blank" rel="noreferrer"&gt;jamsek.dev/blog/2019/Jul/28/openbsd-dns-server-with-unbound-and-nsd/&lt;/a&gt;) but eventually realized I could get away with just unbound.&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# rcctl enable unbound
# rcctl start unbound&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Next, I changed my DNS server so the new firewall was getting it locally instead of going to the old firewall. In /etc/resolv.conf:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;nameserver 127.0.0.1
lookup file bind&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;After that, configure DNSSEC:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# unbound-anchor -a &amp;#34;/var/unbound/db/root.key&amp;#34;
# ftp -S do -o /var/unbound/db/root.hints https://www.internic.net/domain/named.root&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;With root.key downloaded, I could set up my /var/unbound/etc/unbound.conf:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# $OpenBSD: unbound.conf,v 1.21 2020/10/28 11:35:58 sthen Exp $
server:
interface: 192.168.144.1
interface: ::1
# override the default &amp;#34;any&amp;#34; address to send queries; if multiple
# addresses are available, they are used randomly to counter spoofing
access-control: 0.0.0.0/0 refuse
access-control: ::0/0 refuse
access-control: 127.0.0.0/8 allow
access-control: 192.168.144.0/20 allow
access-control: ::1 allow
hide-identity: yes
hide-version: yes
# Perform DNSSEC validation.
#
auto-trust-anchor-file: &amp;#34;/var/unbound/db/root.key&amp;#34;
root-hints: &amp;#34;/var/unbound/db/root.hints&amp;#34;
qname-minimisation: yes
val-log-level: 2
# Synthesize NXDOMAINs from DNSSEC NSEC chains.
# https://tools.ietf.org/html/rfc8198
#
aggressive-nsec: yes
# Serve zones authoritatively from Unbound to resolver clients.
# Not for external service.
#
local-zone: &amp;#34;lan.example.net.&amp;#34; static
#
# Host addresses - infrastructure
#
local-data: &amp;#34;firewall.lan.example.net. IN A 192.168.144.1&amp;#34;
local-data-ptr: &amp;#34;192.168.144.1 firewall.lan.example.net&amp;#34;
#
# Host addresses - named servers
#
local-data: &amp;#34;fileserver.lan.example.net. IN A 192.168.150.171&amp;#34;
local-data-ptr: &amp;#34;192.168.150.171 fileserver.lan.example.net&amp;#34;
local-data: &amp;#34;weather.lan.example.net. IN A 192.168.150.177&amp;#34;
local-data-ptr: &amp;#34;192.168.150.177 weather.lan.example.net&amp;#34;
# ... etc...
remote-control:
control-enable: yes
control-interface: /var/run/unbound.sock&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;With all that set up, I could make sure I didn&amp;rsquo;t have any syntax errors and restart unbound:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# unbound-checkconf
# rcctl restart unbound&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I think it was at this point that I made the switch from using the old firewall to the new firewall. I changed the new firewall to have the same MAC as the old one (my ISP wanted to see a specific MAC) and then put the new one in place:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;# echo &amp;#34;inet autoconf lladdr 11:22:33:44:55:66&amp;#34; &amp;gt; /etc/hostname.igc0
# sh /etc/netstart&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Having two devices with the same MAC is a Bad Thing, and I had a lot of weirdness on my network until I unplugged the old firewall.&lt;/p&gt;
&lt;p&gt;This post is part of a series on &lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/" &gt;setting up an OpenBSD 7.4 firewall device&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Setting up an OpenBSD 7.4 Firewall Device</title><link>https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/</link><pubDate>Sun, 15 Oct 2023 23:59:39 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/</guid><description>&lt;p&gt;My &lt;a href="https://www.pcengines.ch/alix.htm" target="_blank" rel="noreferrer"&gt;PC Engines ALIX&lt;/a&gt; running the (mumble) version of OpenBSD has been a great firewall. But now that it looks like PC Engines is &lt;a href="https://www.pcengines.ch/eol.htm" target="_blank" rel="noreferrer"&gt;wrapping up&lt;/a&gt;, it&amp;rsquo;s time to find something new. For a while I&amp;rsquo;ve suspected that the ALIX is a little underpowered. It&amp;rsquo;s harder to find 4G CF cards these days. Plus I want something with a HDMI port so I can put it on a KVM switch and don&amp;rsquo;t need to worry about serial port speeds.&lt;/p&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="The OpenBSD Puffy logo"
width="300"
height="300"
src="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/images/puffy-firewall-sticker.png"
srcset="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/images/puffy-firewall-sticker.png 800w, https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/images/puffy-firewall-sticker.png 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/images/puffy-firewall-sticker.png"&gt;&lt;/figure&gt;
&lt;p&gt;So&amp;hellip; here&amp;rsquo;s the order of operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-15-buying-new-hardware-for-an-openbsd-firewall/" &gt;Buy new hardware&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-15-installing-openbsd-7-3-for-a-firewall/" &gt;Get OpenBSD running on the new hardware&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-21-set-up-networking-for-an-openbsd-7-4-firewall-device/" &gt;Reorganize my network while it&amp;rsquo;s running&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-22-block-ad-sites-and-nasties-on-openbsd-7-4/" &gt;Block ad sites and nasties&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-22-setting-up-wireguard-on-an-openbsd-7-4-firewall-device/" &gt;Add a Wireguard VPN&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here goes!&lt;/p&gt;</description></item><item><title>Installing OpenBSD 7.4 for a Firewall</title><link>https://andrewmemory.acornwall.net/blog/2023-10-15-installing-openbsd-7-3-for-a-firewall/</link><pubDate>Sun, 15 Oct 2023 23:45:00 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2023-10-15-installing-openbsd-7-3-for-a-firewall/</guid><description>&lt;p&gt;Installing OpenBSD 7.4 was pretty simple. I followed the &lt;a href="https://www.openbsd.org/faq/faq4.html" target="_blank" rel="noreferrer"&gt;OpenBSD installation guide&lt;/a&gt; and used dd on a Linux box to write install74.img to a USB stick. Don&amp;rsquo;t use the .iso, it doesn&amp;rsquo;t boot. Then I booted off the USB stick. (You don&amp;rsquo;t have to disable UEFI.) I used a standard layout&lt;/p&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="A picture of Puffy, the OpenBSD puffer fish logo"
width="500"
height="500"
src="https://andrewmemory.acornwall.net/blog/2023-10-15-installing-openbsd-7-3-for-a-firewall/images/puffy-firewall-sticker-1.png"
srcset="https://andrewmemory.acornwall.net/blog/2023-10-15-installing-openbsd-7-3-for-a-firewall/images/puffy-firewall-sticker-1.png 800w, https://andrewmemory.acornwall.net/blog/2023-10-15-installing-openbsd-7-3-for-a-firewall/images/puffy-firewall-sticker-1.png 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2023-10-15-installing-openbsd-7-3-for-a-firewall/images/puffy-firewall-sticker-1.png"&gt;&lt;/figure&gt;
&lt;p&gt;At the time I wondered if I should install all the packages or not. I decided that maintenance would be simpler if I just went for everything, so I added all the packages including X. That turned out to be the right decision.&lt;/p&gt;
&lt;p&gt;I used a relatively standard partitioning scheme, although I think I bumped up a few of the sizes. I probably should have bumped up X11R6 more, right now it&amp;rsquo;s at 41%:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;/dev/sd0a 986M /
/dev/sd0l 295G /home
/dev/sd0d 291M /tmp
/dev/sd0f 5.8G /usr
/dev/sd0g 986M /usr/X11R6
/dev/sd0h 19.4G /usr/local
/dev/sd0k 5.8G /usr/obj
/dev/sd0j 2.9G /usr/src
/dev/sd0e 34.4G /var&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Set up doas
&lt;div id="set-up-doas" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#set-up-doas" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;After installing, I set up doas &amp;lsquo;cause I like seatbelts:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ su
# vi /etc/doas.conf
permit persist andrewmemory as root
permit persist keepenv root as root&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;h2 class="relative group"&gt;Install patches and packages
&lt;div id="install-patches-and-packages" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#install-patches-and-packages" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;After that I installed patches:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ doas syspatch
$ doas shutdown -r now&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Next I installed a few useful packages:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ doas pkg_add -i emacs mutt firefox wget &lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I picked the -no_x11 version for emacs, and the normal (not gpge, not sasl, not slang) version for mutt. I&amp;rsquo;m not going to be mailing to the world from this box, just looking at local emails. I also installed Firefox, which turned out to be another good idea. It&amp;rsquo;s a lot easier to search for doc on the firewall box itself than to ssh in.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Set up mfs for /tmp
&lt;div id="set-up-mfs-for-tmp" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#set-up-mfs-for-tmp" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;Finally, I&amp;rsquo;m paranoid about wearing out my SSD, so I set up /tmp to be mfs in /etc/fstab using the useful &lt;a href="https://dataswamp.org/~solene/2018-05-08-mfs-tmp.html" target="_blank" rel="noreferrer"&gt;instructions from Solene Rapenne&lt;/a&gt;:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ doas vi /etc/fstab
#f1ea06b71e2dca43.d /tmp ffs rw,nodev,nosuid 1 2
swap /tmp mfs rw,nodev,nosuid,-s=300m 0 0&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;&amp;hellip; and I had to boot to single-user mode to fix up permissions for /tmp:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ doas umount /tmp
$ doas chmod 1777 /tmp
$ doas mount /tmp&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Apparently &lt;a href="http://undeadly.org/cgi?action=article&amp;amp;sid=20160812011743" target="_blank" rel="noreferrer"&gt;tmpfs has been removed&lt;/a&gt; because it&amp;rsquo;s not supported, so mfs it is. I&amp;rsquo;ve got plenty of RAM for a /tmp file system, but I have delusions of putting most of /var in its own mfs file system, so I restricted /tmp to 300M.&lt;/p&gt;
&lt;p&gt;Once that was done, I could log into a few other machines on my network to establish fingerprints for them. I also tested X by running startx, and then firefox, and it worked.&lt;/p&gt;
&lt;h2 class="relative group"&gt;There were some noisy beeps
&lt;div id="there-were-some-noisy-beeps" class="anchor"&gt;&lt;/div&gt;
&lt;span
class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none"&gt;
&lt;a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#there-were-some-noisy-beeps" aria-label="Anchor"&gt;#&lt;/a&gt;
&lt;/span&gt;
&lt;/h2&gt;
&lt;p&gt;By default, OpenBSD rings the bell when you mistype certain things. That was annoying other people in the house, so I had to shut those up. That took two things. In ~/.login I added:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;/sbin/wsconsctl keyboard.bell.volume=0&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Then, I created ~/.xsession and added:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;/usr/X11R6/bin/xset b off&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;This post is part of a series on &lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/" &gt;setting up an OpenBSD 7.4 firewall device&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Buying new hardware for an OpenBSD firewall</title><link>https://andrewmemory.acornwall.net/blog/2023-10-15-buying-new-hardware-for-an-openbsd-firewall/</link><pubDate>Sun, 15 Oct 2023 23:30:53 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2023-10-15-buying-new-hardware-for-an-openbsd-firewall/</guid><description>&lt;p&gt;I knew going in that I wanted more than two ethernet ports for my OpenBSD firewall device. I had visions of multiple networks and/or a spare port that I could use when I screwed up my pf configuration. I also knew that I wanted HDMI so I could pop the firewall on my KVM switch - I&amp;rsquo;d used serial to the APU2 and that was not always wonderful. The Linux box would sometimes forget about the serial ports when they were plugged in for a while.&lt;/p&gt;
&lt;p&gt;In the end, I got a random Intel N5105 mini-PC with four Intel ethernet ports. The &lt;a href="https://www.amazon.com/gp/product/B0B53MKZBX/" target="_blank" rel="noreferrer"&gt;HUNSN Micro Firewall Appliance, Mini PC, VPN, Router PC, Intel N5105, HUNSN RJ03, AES-NI, 4 x Intel 2.5GbE I226-V LAN, Type-C, TF, M.2 WiFi 6 Slot, Barebone, NO RAM, NO Storage, NO System&lt;/a&gt; was around $250 US. Add a Western Digital NVMe &lt;a href="https://www.amazon.com/gp/product/B09HKG6SDF/" target="_blank" rel="noreferrer"&gt;500G drive&lt;/a&gt; and 16G of &lt;a href="https://www.amazon.com/gp/product/B08C4WV6FT/" target="_blank" rel="noreferrer"&gt;Cruical laptop RAM&lt;/a&gt; and I had something on which I could install a system. It&amp;rsquo;s low-powered enough that I don&amp;rsquo;t mind keeping it running 24/7, and high-powered enough that I&amp;rsquo;m not worried about it being a bottleneck.&lt;/p&gt;
&lt;p&gt;[&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="The HUNSN Micro Firewall Appliance from the front"
width="359"
height="211"
src="https://andrewmemory.acornwall.net/blog/2023-10-15-buying-new-hardware-for-an-openbsd-firewall/images/hunsn.png"
srcset="https://andrewmemory.acornwall.net/blog/2023-10-15-buying-new-hardware-for-an-openbsd-firewall/images/hunsn.png 800w, https://andrewmemory.acornwall.net/blog/2023-10-15-buying-new-hardware-for-an-openbsd-firewall/images/hunsn.png 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2023-10-15-buying-new-hardware-for-an-openbsd-firewall/images/hunsn.png"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;I learned afterwards that the I226-V might &lt;a href="https://www.reddit.com/r/openbsd/comments/12g637u/experience_with_problematic_intel_i225_25_gbps/" target="_blank" rel="noreferrer"&gt;potentially have a problem&lt;/a&gt; if you want to do 2.5G ethernet. So far, I haven&amp;rsquo;t experienced any network instability because of that.&lt;/p&gt;
&lt;p&gt;As a belt-and-suspenders kind of thing, I bought a &amp;ldquo;silent&amp;rdquo; USB fan that sits on top of the case, just because the server room can get a little warm.&lt;/p&gt;
&lt;p&gt;This post is part of a series on &lt;a href="https://andrewmemory.acornwall.net/blog/2023-10-15-setting-up-an-openbsd-7-4-firewall-device/" &gt;setting up an OpenBSD 7.4 firewall device&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>ssh unable to negotiate</title><link>https://andrewmemory.acornwall.net/blog/2022-08-28-ssh-unable-to-negotiate/</link><pubDate>Sun, 28 Aug 2022 22:24:49 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2022-08-28-ssh-unable-to-negotiate/</guid><description>&lt;p&gt;My upgrade to Ubuntu LTS 22.04.1 from 20.04 was pretty smooth, but I ran into one issue. When I tried to ssh to a few other boxes around the network, I saw:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;Unable to negotiate with 192.168.1.x port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Looks like I need to upgrade my ssh server on some machines. But I can&amp;rsquo;t do that if I can&amp;rsquo;t ssh to them! So I added a couple of lines to my /etc/ssh/ssh_config instead:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;HostKeyAlgorithms ssh-rsa,rsa-sha2-512,rsa-sha2-256
PubkeyAcceptedKeyTypes ssh-rsa,rsa-sha2-512,rsa-sha2-256&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;ll need to undo that once I fix the server, but until then I can log into it.&lt;/p&gt;</description></item><item><title>Setting up NFS on Ubuntu 20.04</title><link>https://andrewmemory.acornwall.net/blog/2021-05-30-setting-up-nfs-on-ubuntu-20-04/</link><pubDate>Sun, 30 May 2021 14:11:18 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2021-05-30-setting-up-nfs-on-ubuntu-20-04/</guid><description>&lt;p&gt;I have been experiencing random weirdness with CIFS on Ubuntu 20.04. Every now and then the current working directory will disappear. I can usually get things back to normal by &amp;ldquo;cd ..; cd &lt;em&gt;myDir&lt;/em&gt;&amp;rdquo; but that&amp;rsquo;s a pain - more of a pain when a build has failed because the current directory evaporated on the Gradle script.&lt;/p&gt;
&lt;p&gt;So I decided to try NFS. Setting up NFS is not too hard. I found a &lt;a href="https://linuxconfig.org/how-to-configure-nfs-on-linux" target="_blank" rel="noreferrer"&gt;very useful page on LinuxConfig.org&lt;/a&gt; that got me 90% of the way there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server setup&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In short, on the server:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ sudo apt install nfs-kernel-server
$ sudo systemctl enable --now nfs-server&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Then edit /etc/exports on the server to include the line:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;/myshareddisk 192.168.1.0/8(rw,sync,no_subtree_check)&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;It looks as if there&amp;rsquo;s no easy way for NFS to handle DHCP unless the file server can look up the IP. Boo. Going onward and knowing my network is now less protected:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ sudo exportfs -arv&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;At this point, the LinuxConfig.org instructions start pointing you to server configuration. However, there&amp;rsquo;s a vital step omitted - pretty much every sane app will fail with &amp;ldquo;no locks available.&amp;rdquo; Why? &lt;a href="https://askubuntu.com/questions/837962/nfs-error-no-locks-available-after-update-to-16-10" target="_blank" rel="noreferrer"&gt;StackExchange&lt;/a&gt; to the rescue. Enabling NFS doesn&amp;rsquo;t enable rpc-statd.&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ sudo systemctl enable rpc-statd
$ sudo systemctl start rpc-statd &lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;As the StackExchage article says, &amp;ldquo;Thanks, systemd!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client setup&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Next you can set up the client:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;$ sudo mount -t nfs4 my-server-fqdn:/myshareddisk /localmnt&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;Hey, it mounted, and it looks like a file system. Now anyone on my network can mount my NFS server! Exposing it to ransomware and all sorts of excellent things like that. Err&amp;hellip; good?&lt;/p&gt;
&lt;p&gt;Last step is to make it automount on the client by editing /etc/fstab:&lt;/p&gt;
&lt;figure class="highlight"&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-" data-lang=""&gt;my-server-fqdn:/myshareddisk /localmnt nfs4 defaults,user,exec 0 2&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;I&amp;rsquo;m really unsure about this, and may need to undo it or at least find a way to stop it advertising to everyone. But it&amp;rsquo;s that way for now.&lt;/p&gt;</description></item><item><title>Ubiquiti AP enters reset state but never leaves</title><link>https://andrewmemory.acornwall.net/blog/2018-07-20-ubiquiti-ap-enters-reset-state-but-never-leaves/</link><pubDate>Fri, 20 Jul 2018 13:11:31 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2018-07-20-ubiquiti-ap-enters-reset-state-but-never-leaves/</guid><description>&lt;p&gt;I did something dumb recently. I had a network failure at my ISP, so I had to kill my dhclient and restart it. After restarting, everything seemed cool - until I reset my Ubiquiti AP. At that point, the AP entered the &amp;ldquo;Restarting&amp;rdquo; state and never came out.&lt;/p&gt;
&lt;p&gt;After a few minutes of poking around on the Ubiquiti management screen, I saw this:&lt;/p&gt;
&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="auto"
alt="Screenshot from 2018-07-20 13:11:31-0700 13-09-11"
width="376"
height="139"
src="https://andrewmemory.acornwall.net/blog/2018-07-20-ubiquiti-ap-enters-reset-state-but-never-leaves/images/screenshot-from-2018-07-20-13-09-11.jpg"
srcset="https://andrewmemory.acornwall.net/blog/2018-07-20-ubiquiti-ap-enters-reset-state-but-never-leaves/images/screenshot-from-2018-07-20-13-09-11.jpg 800w, https://andrewmemory.acornwall.net/blog/2018-07-20-ubiquiti-ap-enters-reset-state-but-never-leaves/images/screenshot-from-2018-07-20-13-09-11.jpg 1280w"
sizes="(min-width: 768px) 50vw, 65vw"
data-zoom-src="https://andrewmemory.acornwall.net/blog/2018-07-20-ubiquiti-ap-enters-reset-state-but-never-leaves/images/screenshot-from-2018-07-20-13-09-11.jpg"&gt;&lt;/figure&gt;
&lt;p&gt;Apparently I killed off my DHCP server too. Oops.&lt;/p&gt;
&lt;p&gt;One restart of the DHCP server later, my AP was back online (it detected when the DHCP server started). Some day I really should put the AP on its own static IP. That&amp;rsquo;s what I get for being lazy&amp;hellip;&lt;/p&gt;</description></item><item><title>Making the Netgear WGR614 a bridge</title><link>https://andrewmemory.acornwall.net/blog/2014-08-16-making-the-netgear-wgr614-a-bridge/</link><pubDate>Sat, 16 Aug 2014 20:51:55 -0700</pubDate><author>andrewmemoryblog@gmail.com (Andrew's Memory Blog)</author><guid>https://andrewmemory.acornwall.net/blog/2014-08-16-making-the-netgear-wgr614-a-bridge/</guid><description>&lt;p&gt;For the longest time, I&amp;rsquo;ve had a Netgear WGR614 acting as a NAT for my wifi traffic. That meant I had a separate network for wifi traffic, rather than sharing traffic with my wired network.&lt;/p&gt;
&lt;p&gt;Eventually this lead to problems. Some phone apps want to search the network for printers or set top boxes, for instance - and because the wireless devices were on a different network, they&amp;rsquo;d never find the wired devices.&lt;/p&gt;
&lt;p&gt;After a long time thinking about this, I decided to see what it would take to turn my wifi router into a bridge. Turns out the Netgear WGR614 is very nicely suited to that. All it takes is one plug change and a few settings changes, and now my wireless and wired traffic is all on the same IP address range.&lt;/p&gt;
&lt;p&gt;I found a few useful posts for this:&lt;/p&gt;
&lt;p&gt;&lt;a href="http://www.fieldsnet.com/2009/10/how-to-use-a-netgear-wgr614-wireless-router-as-a-bridge/" target="_blank" rel="noreferrer"&gt;http://www.fieldsnet.com/2009/10/how-to-use-a-netgear-wgr614-wireless-router-as-a-bridge/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="http://puzzling.org/uncategorized/2006/08/netgear-wgr614/" target="_blank" rel="noreferrer"&gt;http://puzzling.org/uncategorized/2006/08/netgear-wgr614/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="http://kb.netgear.com/app/answers/detail/a" target="_blank" rel="noreferrer"&gt;http://kb.netgear.com/app/answers/detail/a&lt;/a&gt;_id/19852&lt;/p&gt;
&lt;p&gt;Note that this assumes you&amp;rsquo;ve got something else on the network that&amp;rsquo;s going to serve IP addresses for you. If you don&amp;rsquo;t know, you probably shouldn&amp;rsquo;t do this.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how to do it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Unplug the Netgear WGR614 from everything except one laptop. Make sure the laptop is plugged into a regular port, not the WAN port.&lt;/li&gt;
&lt;li&gt;Hard-reset the Netgear WGR614 (push the button that&amp;rsquo;s inset next to the WAN port for 10 seconds).&lt;/li&gt;
&lt;li&gt;After the router reboots, connect to http://192.168.0.1 from the laptop. After a reset, the account is &amp;ldquo;admin&amp;rdquo; and the password is &amp;ldquo;password&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ll be asked if you want to step through the configuration. Select &amp;ldquo;No, I know what I&amp;rsquo;m doing&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;First off, change that password. Choose the &amp;ldquo;Set Password&amp;rdquo; tab on the left and make it something better.&lt;/li&gt;
&lt;li&gt;Next, go into the Wireless Settings tab. Set the SSID, security to WPA2-PSK and passphrase for WPA2.&lt;/li&gt;
&lt;li&gt;If you know what channels other routers in your neighbourhood use, now is a good time to set the wifi channel as well.&lt;/li&gt;
&lt;li&gt;Go to the LAN Setup tab and unclick &amp;ldquo;Use Router as DHCP Server&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;Next, on the LAN Setup tab set the IP address for the router to something in your static address range.&lt;/li&gt;
&lt;li&gt;Now unplug the laptop and plug what was the WAN uplink cable into a regular port (non-WAN) on the router.&lt;/li&gt;
&lt;li&gt;Unplug and re-plug the router.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once you&amp;rsquo;ve done all that, your router will be acting as a bridge for traffic between the wifi and wired networks.&lt;/p&gt;
&lt;p&gt;One warning: I originally didn&amp;rsquo;t hard-reset the router. This left it with an IP address of my internal wired network on the WAN port. Once I&amp;rsquo;d done that, I couldn&amp;rsquo;t connect to it over the LAN interface, since it saw that as an address conflict. So just hard-reset it.&lt;/p&gt;
&lt;p&gt;Incidentally - this isn&amp;rsquo;t strictly a bridge, since the router has an IP address on the LAN. But it&amp;rsquo;s routing the packets from wifi to wired and back.&lt;/p&gt;</description></item></channel></rss>