<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>TechKnowLogick</title>
    <link>https://text.techknowlogick.com/</link>
    <description>Recent content on TechKnowLogick</description>
    <lastBuildDate>Tue, 10 Mar 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://text.techknowlogick.com/index.xml" rel="self" type="application/rss+xml"></atom:link>
    <item>
      <title>Vanity, thy name is Go</title>
      <link>https://text.techknowlogick.com/posts/vanity-thy-name-is-go/</link>
      <pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/vanity-thy-name-is-go/</guid>
      <description>Setting up Go vanity import URLs with Bunny.net</description>
      <content:encoded>&lt;p&gt;Go vanity imports let you put a custom domain in front of your import paths. Instead of &lt;code&gt;go get gitea.com/user/repo/pkg&lt;/code&gt; (or, in my case, &lt;code&gt;go get src.tklk.dev/tklk/x/pkg&lt;/code&gt;), the command becomes &lt;code&gt;go get tklk.dev/pkg&lt;/code&gt;. The shorter path is decoupled from where the code is hosted.&lt;/p&gt;&#xA;&lt;p&gt;After moving everything into a &lt;a href=&#34;https://text.techknowlogick.com/posts/monorepo/&#34;&gt;monorepo&lt;/a&gt;, this became trivial to set up. One repo means a single vanity domain covers all packages under it. On Bunny.net, the setup uses a custom 404 page stored as a static HTML file. Bunny serves it directly, so there is no server or edge compute to manage.&lt;/p&gt;&#xA;&lt;h2 id=&#34;go-vanity-import-lookup&#34;&gt;Go vanity import lookup&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#go-vanity-import-lookup&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;When you run &lt;code&gt;go get tklk.dev/somepkg&lt;/code&gt;, the Go toolchain makes an HTTP request to &lt;code&gt;https://tklk.dev/somepkg?go-get=1&lt;/code&gt; and parses the HTML response. It looks for a &lt;code&gt;&amp;lt;meta name=&amp;#34;go-import&amp;#34;&amp;gt;&lt;/code&gt; tag in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, which tells it where the actual source code lives.&lt;/p&gt;&#xA;&lt;p&gt;The meta tag has three space-separated values in its &lt;code&gt;content&lt;/code&gt; attribute: the import prefix, the VCS type, and the repository URL.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;meta&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;go-import&amp;#34;&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;content&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;tklk.dev git https://src.tklk.dev/tklk/x&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For a monorepo, all subpaths under the import prefix resolve to the same repo root. &lt;code&gt;tklk.dev/somepkg&lt;/code&gt; and &lt;code&gt;tklk.dev/otherpkg&lt;/code&gt; both point at &lt;code&gt;https://src.tklk.dev/tklk/x&lt;/code&gt;. The Go toolchain handles the rest, mapping the subpath to a directory within the repository.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-custom-404-page&#34;&gt;A custom 404 page&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#a-custom-404-page&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The Go toolchain mostly doesn&amp;#39;t care about the HTTP status code. It fetches the URL and parses the HTML looking for the meta tag. A 200 and a 404 work equally well, as long as the response body contains what it needs.&lt;/p&gt;&#xA;&lt;p&gt;A redirect to Gitea would&amp;#39;ve been simpler, but the Go toolchain reads the meta tag from the final response after following redirects. Gitea serves its own &lt;code&gt;go-import&lt;/code&gt; meta tag with the repo path as the import prefix, so a redirect would make the toolchain see the Gitea hostname instead of the vanity domain. The 404 page sidesteps this because the response comes from the vanity domain itself.&lt;/p&gt;&#xA;&lt;p&gt;Bunny.net lets you configure a custom error page for a pull zone. Set that 404 page to an HTML file containing the &lt;code&gt;go-import&lt;/code&gt; meta tag, and every path under &lt;code&gt;tklk.dev/anything&lt;/code&gt; that doesn&amp;#39;t match a real file returns the custom error page. The Go toolchain reads the meta tag and resolves the import.&lt;/p&gt;&#xA;&lt;p&gt;The full &lt;code&gt;404.html&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;head&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;meta&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;go-import&amp;#34;&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;content&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;tklk.dev git https://src.tklk.dev/tklk/x&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;head&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;Not found. See &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;a&lt;/span&gt; &lt;span class=&#34;na&#34;&gt;href&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;https://src.tklk.dev/tklk/x&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;source&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;.&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;#39;s the whole file, and the meta tag is the only line Go cares about.&lt;/p&gt;&#xA;&lt;p&gt;My pull zone also serves a few real files, like my &lt;a href=&#34;https://text.techknowlogick.com/posts/serverless-docker-registry-with-bunnycdn/&#34;&gt;Docker registry&lt;/a&gt; at &lt;code&gt;/v2/&lt;/code&gt;. None of them conflict with package names, so the custom 404 only fires for paths that don&amp;#39;t match anything real.&lt;/p&gt;&#xA;&lt;h2 id=&#34;setting-it-up&#34;&gt;Setting it up&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#setting-it-up&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you&amp;#39;re already using Bunny.net for static hosting, this is a small addition. I covered the basics of creating storage zones and pull zones in a &lt;a href=&#34;https://text.techknowlogick.com/posts/using-bunny-cdn-to-host-static-sites/&#34;&gt;previous post&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Create a storage zone and pull zone for the vanity domain (or reuse an existing one).&lt;/li&gt;&#xA;&lt;li&gt;Upload the &lt;code&gt;404.html&lt;/code&gt; to the storage zone. Bunny expects custom error pages at &lt;code&gt;bunnycdn_errors/404.html&lt;/code&gt;. You can use another path by changing the storage zone settings.&lt;/li&gt;&#xA;&lt;li&gt;In the pull zone settings, set the storage zone as the origin.&lt;/li&gt;&#xA;&lt;li&gt;Add a DNS CNAME record pointing &lt;code&gt;tklk.dev&lt;/code&gt; to the pull zone hostname.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;testing-it&#34;&gt;Testing it&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#testing-it&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A quick curl confirms it&amp;#39;s working:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -s &lt;span class=&#34;s1&#34;&gt;&amp;#39;https://tklk.dev/somepkg?go-get=1&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You should see the HTML with the meta tag come back, even though the response status is 404. From there, &lt;code&gt;go get tklk.dev/somepkg&lt;/code&gt; resolves to the Gitea repo and pulls the code down.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://text.techknowlogick.com/posts/monorepo/&#34;&gt;monorepo&lt;/a&gt; is what makes the whole setup this simple, and it costs nothing beyond what the CDN already costs. A domain fronting multiple independent repos would need a different meta tag for each import path, plus edge compute or a server to route requests.&lt;/p&gt;&#xA;&lt;p&gt;I have a separate project, &lt;a href=&#34;https://gitea.com/techknowlogick/go-vanity-url&#34; rel=&#34;nofollow&#34;&gt;go-vanity-url&lt;/a&gt;, that handles that case. It generates static HTML files from a TOML config, one per package, with the correct &lt;code&gt;go-import&lt;/code&gt; meta tag. You can host the output on any static file server or CDN. Here, one meta tag covers everything, so a static 404 page is enough.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Monorepo! Monorepo! Monorepo!</title>
      <link>https://text.techknowlogick.com/posts/monorepo/</link>
      <pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/monorepo/</guid>
      <description>Well, sir, there&#39;s nothing on earth like a genuine, bona fide, electrified, six-car monorepo!</description>
      <content:encoded>&lt;p&gt;What&amp;#39;s it called? Monorepo! That&amp;#39;s right, monorepo!&lt;/p&gt;&#xA;&lt;p&gt;I&amp;#39;ve been maintaining many small Go repositories for personal projects, libraries, and a bunch of experiments. The overhead that comes with all of that adds up. After recently switching to a monorepo, I&amp;#39;m a convert: so much toil immediately disappeared.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-inspiration&#34;&gt;The inspiration&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-inspiration&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://xeiaso.net/&#34; rel=&#34;nofollow&#34;&gt;Xe Iaso&lt;/a&gt; maintains a public monorepo, &lt;a href=&#34;https://github.com/Xe/x/&#34; rel=&#34;nofollow&#34;&gt;&lt;code&gt;x&lt;/code&gt;&lt;/a&gt;, that contains many Go projects, experiments, and libraries. Sometimes full projects, such as &lt;a href=&#34;https://anubis.techaro.lol/&#34; rel=&#34;nofollow&#34;&gt;Anubis&lt;/a&gt;, have even been spun out of it. The repo uses idiomatic Go project layouts with a &lt;code&gt;go.mod&lt;/code&gt; at the root, plus repo-wide policies for things like linting, CI, and other similar items.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-problem&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;When I have a new idea, I usually make a scratch folder for it, because setting up CI, linting, and dependency management for a one-off experiment feels like more work than it&amp;#39;s worth. If the experiment turns out to be interesting and I want to keep it around, I make a repo for it and copy the code over. I&amp;#39;d rather not sign up to maintain something that might be a dead end.&lt;/p&gt;&#xA;&lt;p&gt;The repos that did get created were inconsistent, and a &amp;#34;breaking&amp;#34; change in a shared library could mean updating every repo that imported it.&lt;/p&gt;&#xA;&lt;p&gt;Dependency management was the worst of it. I use &lt;a href=&#34;https://about.gitea.com/resources/tutorials/use-gitea-and-renovate-bot-to-automatically-monitor-software-packages&#34; rel=&#34;nofollow&#34;&gt;Renovate&lt;/a&gt; to keep things updated, and it does its job well. With several repos sharing dependencies, &amp;#34;doing its job&amp;#34; means a steady stream of PRs across all of them, even for repos I barely care about. Automatic merging handles most of the updates, but the emails and notifications still come in, and sometimes the tests fail. Then I have to decide whether to context switch and fix it or let it rot until I next need that repo and notice the breakage. It&amp;#39;s a minor annoyance, but that still adds up as the repo count grows.&lt;/p&gt;&#xA;&lt;h2 id=&#34;one-repo-to-rule-them-all&#34;&gt;One repo to rule them all&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#one-repo-to-rule-them-all&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;After looking at how Xe structured their &lt;code&gt;x&lt;/code&gt; repo, I moved everything into a single repository. Most of those annoyances just went away.&lt;/p&gt;&#xA;&lt;p&gt;The shared CI and build setup is available right out of the gate for any new project. A standard structure means I can copy an existing project to get started. The barrier to trying a new idea is much lower, and anything I keep ends up with the same coding style and structure.&lt;/p&gt;&#xA;&lt;p&gt;I can also have shared utilities across multiple programs, and just have to update them once without needing to bump deps for a bunch of repos. I can also have shared CI and linting rules, so when I want to change a rule, I change it once and it applies everywhere.&lt;/p&gt;&#xA;&lt;p&gt;Because the infrastructure is already there, more of my experiments end up in source control instead of dying in a scratch folder. That makes them easier to find if I want to revisit one later.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-freedom-of-building-for-one&#34;&gt;The freedom of building for one&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-freedom-of-building-for-one&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A personal monorepo with no expectation of being used by others gives me the freedom to make choices I wouldn&amp;#39;t make for a project that has to take into account the needs of others and how different people may approach using the tool.&lt;/p&gt;&#xA;&lt;p&gt;Those decisions might be wrong for an open source library or a shared codebase at work, where separate repos provide clear versioning and ownership boundaries. For my personal projects, that is too much overhead and it creates a barrier from actually building things.&lt;/p&gt;&#xA;&lt;h2 id=&#34;give-it-a-try&#34;&gt;Give it a try&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#give-it-a-try&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you maintain a pile of small repos, and spend more time on repo maintenance than on the code itself, you might want to look into monorepos. Xe&amp;#39;s &lt;a href=&#34;https://github.com/Xe/x/&#34; rel=&#34;nofollow&#34;&gt;monorepo&lt;/a&gt; is a good reference for how to structure one.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>A serverless Docker registry with Bunny.net</title>
      <link>https://text.techknowlogick.com/posts/serverless-docker-registry-with-bunnycdn/</link>
      <pubDate>Tue, 03 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/serverless-docker-registry-with-bunnycdn/</guid>
      <description>Using Bunny.net storage zones and edge rules to build a Docker container registry without running any servers</description>
      <content:encoded>&lt;p&gt;I &lt;a href=&#34;https://text.techknowlogick.com/posts/migrate-docker-images-to-self-hosted-registry/&#34;&gt;host all of my Docker images using my personal Gitea instance&lt;/a&gt;. Rather than expose my home network on the internet, I wanted to  figure out a way to distribute them without having to worry about managing yet other server. I already use Bunny.net for some things, and thought I could try to figure out how to make that work.&lt;/p&gt;&#xA;&lt;p&gt;Bunny.net has edge compute similar to AWS Lambda@Edge, but it also comes with a per-request cost. But why use those when edge-rules exist, so I wanted to see whether they were enough. The OCI distribution API turns out to be mostly static file lookups (at least for public images since you don&amp;#39;t need to consider authentication). A CDN that can rewrite URLs and set response headers can pretend to be a registry.&lt;/p&gt;&#xA;&lt;h2 id=&#34;registry-endpoints&#34;&gt;Registry endpoints&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#registry-endpoints&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The OCI distribution spec boils down to a few GET endpoints:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;GET /v2/&lt;/code&gt; returns 200 OK as an auth check (conventionally with a body of &lt;code&gt;{}&lt;/code&gt;, though the spec doesn&amp;#39;t mandate a specific body). If you return 200, clients will assume auth isn&amp;#39;t required.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;GET /v2/&amp;lt;name&amp;gt;/manifests/&amp;lt;reference&amp;gt;&lt;/code&gt; returns a manifest (by tag or digest)&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;GET /v2/&amp;lt;name&amp;gt;/blobs/&amp;lt;digest&amp;gt;&lt;/code&gt; returns layers and configs&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Manifests need a little extra handling. Edge compute could distinguish an index from a manifest and set headers accordingly. With plain edge rules, I instead store platform-specific manifests and multi-arch indexes in separate directories, so the CDN can set &lt;code&gt;Content-Type&lt;/code&gt; based on the path alone. If a client requests a manifest by digest, the CDN looks for it in the manifest directory first. If it isn&amp;#39;t there, it must be an index, so the CDN issues a redirect to the index path. The client then gets the correct &lt;code&gt;Content-Type&lt;/code&gt; without the CDN inspecting file contents.&lt;/p&gt;&#xA;&lt;p&gt;The responses also need specific headers: the correct &lt;code&gt;Content-Type&lt;/code&gt; and &lt;code&gt;Content-Length&lt;/code&gt; for each artifact, and &lt;code&gt;Docker-Content-Digest&lt;/code&gt; on manifests and blobs. I also set &lt;code&gt;Docker-Distribution-API-Version: registry/2.0&lt;/code&gt; on all &lt;code&gt;/v2/&lt;/code&gt; responses. This is a legacy Docker header that the OCI spec considers optional, but some older clients still expect it. If you serve the right files with the right headers at these paths, Docker doesn&amp;#39;t know the difference.&lt;/p&gt;&#xA;&lt;h2 id=&#34;storage-layout&#34;&gt;Storage layout&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#storage-layout&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The file structure in the Bunny storage zone mirrors the API paths, with some extras to help the edge rules dispatch correctly:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;/v2/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  _root                              # static {} for the base endpoint&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  _blobs/sha256:&amp;lt;hex&amp;gt;                # shared blob store (deduplicated)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &amp;lt;image&amp;gt;/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    manifests/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      sha256:&amp;lt;hex&amp;gt;                   # platform-specific manifests&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      tags/&amp;lt;tag&amp;gt;                     # image index JSON (multi-arch)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    indexes/sha256:&amp;lt;hex&amp;gt;             # image indexes (by digest)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Blobs live in a shared &lt;code&gt;_blobs/&lt;/code&gt; directory, deduplicated across images.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-edge-rules&#34;&gt;The edge rules&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-edge-rules&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Bunny.net&amp;#39;s edge rules are enough to make the CDN look like a Docker registry. They handle path rewrites, response headers, and the 404-to-redirect trick for manifest vs index dispatch.&lt;/p&gt;&#xA;&lt;h3 id=&#34;base-endpoint&#34;&gt;Base endpoint&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#base-endpoint&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Set the &lt;code&gt;Docker-Distribution-API-Version: registry/2.0&lt;/code&gt; header on all &lt;code&gt;/v2/*&lt;/code&gt; requests.&lt;/li&gt;&#xA;&lt;li&gt;Rewrite &lt;code&gt;/v2/&lt;/code&gt; (exact match) to serve the &lt;code&gt;_root&lt;/code&gt; file, which contains &lt;code&gt;{}&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Set &lt;code&gt;Content-Type: application/json&lt;/code&gt; on that base response.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&#34;tag-resolution&#34;&gt;Tag resolution&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#tag-resolution&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;When a client requests &lt;code&gt;/v2/&amp;lt;name&amp;gt;/manifests/&amp;lt;tag&amp;gt;&lt;/code&gt; (where the tag is a human-readable name like &lt;code&gt;latest&lt;/code&gt;, not a sha256 digest), rule 4 rewrites the path to &lt;code&gt;/v2/&amp;lt;name&amp;gt;/manifests/tags/&amp;lt;tag&amp;gt;&lt;/code&gt;. This uses Bunny&amp;#39;s &lt;code&gt;triggerMatchAll&lt;/code&gt; to match &lt;code&gt;/v2/*/manifests/*&lt;/code&gt; while excluding paths that already contain &lt;code&gt;sha256:&lt;/code&gt; or &lt;code&gt;tags/&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Rule 5 sets &lt;code&gt;Content-Type: application/vnd.oci.image.index.v1+json&lt;/code&gt; on responses from &lt;code&gt;/v2/*/manifests/tags/*&lt;/code&gt;. In my setup, tag lookups always return an image index (the multi-arch manifest), since I always build multi-arch with &lt;code&gt;buildx&lt;/code&gt;. If you only built single-platform images, a tag could point directly to a platform manifest instead.&lt;/p&gt;&#xA;&lt;h3 id=&#34;blob-requests&#34;&gt;Blob requests&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#blob-requests&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Rule 6 rewrites blob requests from &lt;code&gt;/v2/&amp;lt;name&amp;gt;/blobs/sha256:*&lt;/code&gt; to &lt;code&gt;/v2/_blobs/sha256:*&lt;/code&gt;, pointing at the shared store. Bunny&amp;#39;s path segment variables (&lt;code&gt;%{Path.3}&lt;/code&gt;) extract the &lt;code&gt;sha256:&amp;lt;hex&amp;gt;&lt;/code&gt; segment to build the rewritten path.&lt;/p&gt;&#xA;&lt;h3 id=&#34;manifest-vs-index-dispatch&#34;&gt;Manifest vs index dispatch&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#manifest-vs-index-dispatch&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;When a client requests a manifest by digest (&lt;code&gt;/v2/&amp;lt;name&amp;gt;/manifests/sha256:*&lt;/code&gt;), the CDN looks in &lt;code&gt;/manifests/&lt;/code&gt; first. If the digest belongs to a platform manifest (the config and layers for a single architecture, like linux/amd64), the file exists there and the CDN serves it directly. Rule 8 sets &lt;code&gt;Content-Type: application/vnd.oci.image.manifest.v1+json&lt;/code&gt; on these paths.&lt;/p&gt;&#xA;&lt;p&gt;If the digest refers to an image index (the document that lists which platform manifest to use for each architecture), it won&amp;#39;t exist in &lt;code&gt;/manifests/&lt;/code&gt;. It is stored only in &lt;code&gt;/indexes/&lt;/code&gt;, so the request 404s. Rule 7 catches that 404 and issues a 302 redirect to &lt;code&gt;/v2/&amp;lt;name&amp;gt;/indexes/sha256:*&lt;/code&gt;. This has to be a real redirect, not a URL override, so the client makes a fresh request. That way the response header rules evaluate against the &lt;code&gt;/indexes/&lt;/code&gt; path. Rule 9 then sets &lt;code&gt;Content-Type: application/vnd.oci.image.index.v1+json&lt;/code&gt; correctly.&lt;/p&gt;&#xA;&lt;h3 id=&#34;digest-headers&#34;&gt;Digest headers&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#digest-headers&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Rules 10 and 11 extract the sha256 digest from the URL path (using the &lt;code&gt;%{Path.3}&lt;/code&gt; path segment variable mentioned above) and set it as the &lt;code&gt;Docker-Content-Digest&lt;/code&gt; header. One covers manifest paths, the other covers index paths after redirect. Strictly speaking, the OCI spec also requires &lt;code&gt;Docker-Content-Digest&lt;/code&gt; on blob responses, but in practice the clients I&amp;#39;ve tested (&lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;crane&lt;/code&gt;, &lt;code&gt;skopeo&lt;/code&gt;) don&amp;#39;t depend on it for blobs, so I haven&amp;#39;t added a rule for that.&lt;/p&gt;&#xA;&lt;h2 id=&#34;publishing-images&#34;&gt;Publishing images&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#publishing-images&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Publishing goes through a script because the files have to land in the exact directory structure above. I build images with &lt;code&gt;docker buildx&lt;/code&gt; in CI, targeting multiple architectures and exporting in OCI format. The script transforms the OCI layout into the storage zone structure, then uploads everything using the Bunny.net storage zone API. After the upload, it purges the tag manifest URL so &lt;code&gt;docker pull&lt;/code&gt; immediately sees the new version.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;publish-oci-image &amp;lt;image-name&amp;gt; &amp;lt;tag&amp;gt; &amp;lt;oci-layout-dir-or-tar&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  extract OCI input into a staging directory if it&amp;#39;s a tar file&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  read index.json to find the top-level index digest&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  copy the index blob to:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    /v2/&amp;lt;name&amp;gt;/manifests/tags/&amp;lt;tag&amp;gt;   (tag lookup)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    /v2/&amp;lt;name&amp;gt;/indexes/&amp;lt;digest&amp;gt;       (digest lookup)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  for each platform manifest listed in the index:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    copy the manifest blob to /v2/&amp;lt;name&amp;gt;/manifests/&amp;lt;digest&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    for each blob (config + layers) referenced by that manifest:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      copy to /v2/_blobs/&amp;lt;digest&amp;gt; if not already there&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  upload the directory to the Bunny storage zone as it matches the structure from above&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  purge the CDN cache for /v2/&amp;lt;name&amp;gt;/manifests/&amp;lt;tag&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The staging directory mirrors the final storage layout exactly, so the upload is a straightforward sync. Even if two platform manifests reference the same layer, the script only copies it once. The cache purge at the end targets just the tag URL, because that&amp;#39;s the only path where staleness matters (blob digests are content-addressed, so they never change).&lt;/p&gt;&#xA;&lt;h2 id=&#34;garbage-collection&#34;&gt;Garbage collection&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#garbage-collection&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With no registry server managing state, storage can balloon quickly, so I have a separate GC command for cleanup. It walks each image&amp;#39;s tags, resolves every referenced blob, manifest, and index, then checks for unreferenced files in &lt;code&gt;_blobs/&lt;/code&gt;. If it finds any, it deletes them using the Bunny API. Walking the whole graph is a bit slow, but that is fine for my use case. I publish a handful of images, and each one only ever has the &lt;code&gt;latest&lt;/code&gt; tag.&lt;/p&gt;&#xA;&lt;h2 id=&#34;pulling-images&#34;&gt;Pulling images&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#pulling-images&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;docker pull tklk.dev/abc:latest&lt;/code&gt; works. I&amp;#39;ve verified it with &lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;crane&lt;/code&gt;, and &lt;code&gt;skopeo&lt;/code&gt;. Each one sees a real registry because the headers and paths are correct.&lt;/p&gt;&#xA;&lt;h2 id=&#34;limitations&#34;&gt;Limitations&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#limitations&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There&amp;#39;s no &lt;code&gt;docker push&lt;/code&gt;. Images go through the publish script, which means CI is the only path to publishing. Images are public only, so this approach only works for content you&amp;#39;re happy to expose without authentication. I also haven&amp;#39;t implemented tag listing (&lt;code&gt;/v2/&amp;lt;name&amp;gt;/tags/list&lt;/code&gt;) or the Docker-specific catalogue endpoint (&lt;code&gt;/v2/_catalog&lt;/code&gt;, which isn&amp;#39;t part of the OCI distribution spec). I know what images I have because I put them there.&lt;/p&gt;&#xA;&lt;p&gt;If you need private images, a push API, tag listing, or fine-grained access control, you probably want something like Gitea. I use this only for public personal images that already publish through CI. It runs on a single pull zone with no server behind it.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Tricking GitHub Copilot into reviewing PRs in Gitea</title>
      <link>https://text.techknowlogick.com/posts/tricking-github-copilot-into-reviewing-prs-in-gitea/</link>
      <pubDate>Mon, 02 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/tricking-github-copilot-into-reviewing-prs-in-gitea/</guid>
      <description>Building an AI code reviewer for Gitea using the Copilot SDK and MCP tools.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://xeiaso.net/blog/2026/reviewbot/&#34; rel=&#34;nofollow&#34;&gt;Xe Iaso wrote about building a review bot&lt;/a&gt; that uses a self-hosted LLM to review GitHub pull requests. When I learned that some &amp;#34;AI&amp;#34; CLIs have SDKs, I wondered whether I could build something similar while leaving the loop and execution environment to a billion-dollar corporation.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Disclaimer: I chose Copilot because it was free, and I liked the novelty of using a GitHub thing somewhere other than GitHub. These companies also don&amp;#39;t need more money for a silly experiment.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Contrary to the title, I didn&amp;#39;t actually &amp;#34;trick&amp;#34; Copilot. These AI coding CLIs have &lt;a href=&#34;https://github.com/github/copilot-sdk&#34; rel=&#34;nofollow&#34;&gt;SDKs&lt;/a&gt; that let you interact with them programmatically, kinda like a RESTful API. The API is as non-deterministic as the CLI, and the agent can still run its normal loop and call tools. Copilot was convenient here, but this pattern is common across these kinds of tools.&lt;/p&gt;&#xA;&lt;p&gt;I should also note that I have reservations about AI tooling in general (a topic for another post that&amp;#39;s been sitting in my drafts). This post is about a pattern I found interesting, not an endorsement of any particular product, and especially not an endorsement/condoning of the practices of the companies behind them.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-approach&#34;&gt;The approach&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-approach&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Xe&amp;#39;s setup triggers via a GitHub Actions workflow when someone comments &lt;code&gt;/reviewbot&lt;/code&gt; on a PR. It uses an OpenAI-compatible API backed by a self-hosted model running on a DGX Spark. Its agentic loop can execute Python to analyze the codebase and submit the review. The bot also clones the repo so the model has filesystem access through Python execution.&lt;/p&gt;&#xA;&lt;p&gt;Instead of an Actions workflow, mine starts from a webhook listener that watches for a specific pattern in PR comments and fires off a review when it matches. A Go process fetches the PR&amp;#39;s diff and existing comments, then spins up the Copilot CLI with a prompt built from that context and a set of custom tools. The agent explores the codebase and calls the tools as needed, and when it&amp;#39;s done, the review summary gets posted back to the PR.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-tools&#34;&gt;The tools&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-tools&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The tools translate the agent&amp;#39;s feedback into the specific API calls Gitea expects. The agent has no idea it&amp;#39;s talking to Gitea.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;post_inline_comment&lt;/code&gt; posts a comment on a specific file and line. Its severity is either &lt;code&gt;blocker&lt;/code&gt; (must-fix before merge) or &lt;code&gt;suggestion&lt;/code&gt; (recommended improvement). The PR author can triage quickly: address the blockers, consider the suggestions.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;note_low_confidence&lt;/code&gt; records an observation the agent isn&amp;#39;t confident about.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;submit_review&lt;/code&gt; posts the final summary as a top-level PR comment. The agent calls it once at the end of the review.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;note_low_confidence&lt;/code&gt; tool came from &lt;a href=&#34;https://angiejones.tech/how-i-taught-github-copilot-code-review-to-think-like-a-maintainer/&#34; rel=&#34;nofollow&#34;&gt;Angie Jones&amp;#39; post on teaching Copilot to think like a maintainer&lt;/a&gt;. She recommends a confidence threshold above 80%, so the reviewer only comments when it&amp;#39;s fairly sure something is wrong. Without that, AI reviewers dump every observation as an equally-weighted comment, and people learn to ignore the bot fast.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Note: While we may describe these machines as &amp;#34;thinking&amp;#34;, it is important to recognize our anthropomorphization of what is really statistical analysis being done by the model.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I agree with the threshold idea, but my confidence in these models isn&amp;#39;t high enough to trust their weighting. If I were building a product or running this somewhere that noisy bot comments would overload developers, I&amp;#39;d probably have the bot stay silent below the threshold. For an experiment, though, I wanted to see everything the model had to say and judge it myself.&lt;/p&gt;&#xA;&lt;p&gt;Low-confidence observations go into a collapsible section at the bottom of the main review. They stay out of the way while remaining available to anyone curious. This way, I get to be my true Hannah Montana self and have the best of both worlds.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-system-prompt&#34;&gt;The system prompt&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-system-prompt&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Angie&amp;#39;s post was full of great ideas, like telling the reviewer to skip linting, since CI already catches those errors and does a better job of it.&lt;/p&gt;&#xA;&lt;p&gt;The prompt also explains the confidence model (high confidence gets an inline comment, low confidence gets a &lt;code&gt;note_low_confidence&lt;/code&gt; call) and includes the expected output format for the final summary.&lt;/p&gt;&#xA;&lt;p&gt;PR context goes in as a structured template: title, author, branches, description, the diff, existing comments (filtered to exclude bot noise). If the diff is too large, it&amp;#39;s truncated with a note telling the agent to use &lt;code&gt;git diff&lt;/code&gt; on specific files. The agent has the full cloned repo, so it can always look at more than what&amp;#39;s in the prompt.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-feedback-loop&#34;&gt;The feedback loop&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-feedback-loop&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Once the agent is done, the review summary comment gets updated. Low-confidence notes are appended in a collapsible &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; section, and usage metadata (token counts, model used, API calls, cost if available) goes in another collapsible section at the bottom.&lt;/p&gt;&#xA;&lt;p&gt;I include the usage metadata so that anyone reading a bot-generated review can see what produced it and roughly what it cost. A line like &amp;#34;claude-sonnet-4.5, 12 API calls, 45k input tokens&amp;#34; makes the resource use concrete, and hopefully nudges future contributions to stay focused. If a contribution is too large, it can overflow the context window and the review gets less useful.&lt;/p&gt;&#xA;&lt;h2 id=&#34;beyond-copilot&#34;&gt;Beyond Copilot&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#beyond-copilot&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Very little of this is Copilot-specific because the SDK, model, and hosting can all change. The custom tools adapt the review loop to Gitea.&lt;/p&gt;&#xA;&lt;p&gt;Code is left as an exercise to the reader. Although, if you are a VC, I am happy to send you my routing number in exchange for $10 Billion for this unicorn.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>DIY Multi-Hop Boundary Sessions without HCP</title>
      <link>https://text.techknowlogick.com/posts/diy-boundary-ingress-egress-workers/</link>
      <pubDate>Wed, 16 Apr 2025 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/diy-boundary-ingress-egress-workers/</guid>
      <description>Creating an unofficial ingress/egress worker setup for HashiCorp Boundary without HCP or enterprise licensing</description>
      <content:encoded>&lt;p&gt;I treat my homelab as if it has the same security requirements as a production environment because for me it is a production environment. I normally use Tailscale for protected access and avoid exposing services to the internet. Some remote networks block Tailscale or WireGuard connections, so I also need a break-glass option.&lt;/p&gt;&#xA;&lt;p&gt;As with many of my other posts, you can see I use HashiCorp&amp;#39;s suite of tools. Boundary provides identity-based access to systems in my homelab without SSH tunnels or exposing each system to the internet. It is also a single binary that runs without administrative access, which helps when I cannot install VPN software on a machine.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-problem&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;My homelab budget does not stretch to the enterprise version of Boundary or HCP (HashiCorp Cloud Platform). The community version does not support the ingress and egress workers required for multi-hop sessions.&lt;/p&gt;&#xA;&lt;p&gt;I want multi-hop sessions so I can keep at least some VLAN isolation without letting the server in the DMZ connect directly to every server in my homelab.&lt;/p&gt;&#xA;&lt;p&gt;The topology I wanted:&lt;/p&gt;&#xA;&lt;div class=&#34;mermaid&#34; role=&#34;img&#34; aria-label=&#34;Diagram&#34; aria-describedby=&#34;mermaid-1-source&#34; data-mermaid-source-id=&#34;mermaid-1&#34;&gt;&#xA;&lt;pre class=&#34;mermaid-source&#34; id=&#34;mermaid-1-source&#34;&gt;graph TB&#xA;    accTitle: DIY Boundary multi-hop network topology&#xA;    accDescr: Diagram of three network zones. A client laptop in the Internet zone connects to a Boundary controller hosted with a DMZ worker and FRP server in the DMZ. An internal worker in the Internal VLAN registers outbound with the controller, holds a tunnel to the FRP server, and reaches internal resources, so the DMZ never connects directly into the internal VLAN.&#xA;    subgraph Internet&#xA;        client[Client/Laptop]&#xA;    end&#xA;    &#xA;    subgraph DMZ&#xA;        controller[Boundary Controller]&#xA;        dmz_worker[DMZ Worker]&#xA;        frps[FRP Server]&#xA;    end&#xA;    &#xA;    subgraph Internal_VLAN[Internal VLAN]&#xA;        internal_worker[Internal Worker]&#xA;        internal_resources[Internal Resources]&#xA;    end&#xA;    &#xA;    internal_worker -- 1. Register --&amp;gt; controller&#xA;    client -- 2. Connect --&amp;gt; controller&#xA;    client -- 3. Session --&amp;gt; dmz_worker&#xA;    internal_worker &amp;lt;--&amp;gt; frps&#xA;    dmz_worker -- 4. Connect --&amp;gt; frps&#xA;    internal_worker -- 5. Access --&amp;gt; internal_resources&lt;/pre&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;my-diy-solution&#34;&gt;My DIY solution&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#my-diy-solution&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;My workaround depends on Boundary&amp;#39;s connection flow:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;The client (my laptop) connects to the controller&lt;/li&gt;&#xA;&lt;li&gt;The controller provides an authorization token&lt;/li&gt;&#xA;&lt;li&gt;The client connects to the worker port using this token&lt;/li&gt;&#xA;&lt;li&gt;The worker establishes the session with the target&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;I run an FRP (Fast Reverse Proxy) server alongside the DMZ Boundary controller and worker. The internal workers connect outward to that server, so I do not need firewall rules that permit inbound connections to the internal workers. After registration, each internal worker gets a Boundary target pointing at a unique &lt;code&gt;frps&lt;/code&gt; port on the Boundary server&amp;#39;s loopback interface.&lt;/p&gt;&#xA;&lt;p&gt;The resulting session path:&lt;/p&gt;&#xA;&lt;div class=&#34;mermaid&#34; role=&#34;img&#34; aria-label=&#34;Diagram&#34; aria-describedby=&#34;mermaid-2-source&#34; data-mermaid-source-id=&#34;mermaid-2&#34;&gt;&#xA;&lt;pre class=&#34;mermaid-source&#34; id=&#34;mermaid-2-source&#34;&gt;sequenceDiagram&#xA;    accTitle: DIY Boundary session establishment sequence&#xA;    accDescr: Sequence of nine steps showing how a session is established. The client authenticates with the Boundary controller and receives a token, connects to the internal worker target through the DMZ worker, which forwards over the FRP tunnel the internal worker already holds. A second token then lets the client connect through that established tunnel to the internal resource target.&#xA;    participant Client&#xA;    participant Controller as Boundary Controller&#xA;    participant DMZ as DMZ Worker&#xA;    participant FRP as FRP Server&#xA;    participant Internal as Internal Worker&#xA;    participant Target as Internal Resource&#xA;    &#xA;    Client-&amp;gt;&amp;gt;Controller: 1. Authenticate &amp;amp; request session&#xA;    Controller-&amp;gt;&amp;gt;Client: 2. Return session token&#xA;    Client-&amp;gt;&amp;gt;DMZ: 3. Connect to internal worker target&#xA;    DMZ-&amp;gt;&amp;gt;FRP: 4. Forward to internal worker via FRP tunnel&#xA;    FRP-&amp;gt;&amp;gt;Internal: 5. Connection established to internal worker&#xA;    Client-&amp;gt;&amp;gt;Controller: 6. Request session for internal resource&#xA;    Controller-&amp;gt;&amp;gt;Client: 7. Return session token&#xA;    Client-&amp;gt;&amp;gt;Internal: 8. Connect to internal resource via established tunnel&#xA;    Internal-&amp;gt;&amp;gt;Target: 9. Connect to target resource&lt;/pre&gt;&#xA;&lt;/div&gt;&#xA;&lt;h3 id=&#34;setting-it-up&#34;&gt;Setting it up&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#setting-it-up&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Following the standard Boundary installation process, expose the DMZ controller API, usually port 9200, and the worker, usually port 9202, to the internet. The endpoint could be &lt;code&gt;boundary.tklk.dev&lt;/code&gt;, with Caddy providing TLS for API access. Internal workers also need to reach the cluster worker coordinator on port 9201, but that port does not need public exposure. They can reach it through the same path used for the FRP server.&lt;/p&gt;&#xA;&lt;h4 id=&#34;installing-frp-server&#34;&gt;Installing FRP server&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#installing-frp-server&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;Install FRP on the DMZ server:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Download the appropriate FRP release from &lt;a href=&#34;https://github.com/fatedier/frp/releases&#34; rel=&#34;nofollow&#34;&gt;GitHub&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;Extract the archive and locate the &lt;code&gt;frps&lt;/code&gt; binary&lt;/li&gt;&#xA;&lt;li&gt;Create a configuration file as shown below&lt;/li&gt;&#xA;&lt;li&gt;Set up a systemd service (optional) to ensure it runs automatically&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;This is the &lt;code&gt;frps&lt;/code&gt; configuration I use:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;bindPort&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;7000&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;auth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;method&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;token&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;auth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;token&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;your_secure_token&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# meaning the internal workers can only use this range for listening on&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;allowPorts&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;start&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;9500&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;end&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;9999&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;internal-worker-setup&#34;&gt;Internal worker setup&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#internal-worker-setup&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;After the FRP server is running, configure the internal workers:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Install and configure a standard Boundary worker&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# minimal worker config&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# you may wish to add more configuration options, such as using a KMS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;listener&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt; {&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;  # this is the listener that frpc will connect to over the loop back&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  address&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;127.0.0.1:9202&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  purpose&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;proxy&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;worker&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;internal-worker-1&amp;#34;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;  # this is the address that the boundary controller will announce to the client&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;  # so this is what the target session should listen to on your local laptop/client&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  public_addr&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;127.0.0.1:9500&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  description&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Worker in the internal VLAN&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  controllers&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;boundary.tklk.dev:9201&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Install FRP client (frpc)&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Configure frpc to establish a reverse tunnel back to the primary server on a loopback interface&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;This is the &lt;code&gt;frpc&lt;/code&gt; configuration I use:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;serverAddr&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;boundary.tklk.dev&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;serverPort&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;7000&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;auth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;method&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;token&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;auth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;token&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;your_secure_token&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;proxies&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;boundary_internal_worker&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;localIP&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;127.0.0.1&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# 9202 is the Boundary worker&amp;#39;s default port, but since you may have multiple internal workers, you might want to pick&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# a unique port for each worker (which you&amp;#39;ll also need to set in the worker config)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# It is strongly recommended that you don&amp;#39;t use port 9202 as the DMZ worker will already likely be using this port, causing the frps connection to fail&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;localPort&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;9202&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;remotePort&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;9500&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;registering-the-internal-worker-as-a-target&#34;&gt;Registering the internal worker as a target&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#registering-the-internal-worker-as-a-target&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Register each internal worker as a Boundary target pointing to the FRP server&amp;#39;s loopback interface and the port selected in the &lt;code&gt;frpc&lt;/code&gt; configuration. You can do this with Terraform, the Boundary CLI, or the Boundary UI.&lt;/p&gt;&#xA;&lt;p&gt;At minimum, the target needs &lt;code&gt;127.0.0.1&lt;/code&gt; as its host, the port from the &lt;code&gt;frpc&lt;/code&gt; configuration, and a default client port that matches the worker port.&lt;/p&gt;&#xA;&lt;p&gt;An egress worker filter can force the DMZ worker to connect to the &lt;code&gt;frps&lt;/code&gt; port. Because the target uses localhost, Boundary may otherwise assign the route to a different worker that cannot reach it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;using-the-setup&#34;&gt;Using the setup&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#using-the-setup&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Authenticate with Boundary, then use &lt;code&gt;boundary connect&lt;/code&gt; to establish a connection to the internal worker target.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Authenticate with Boundary (if not already authenticated)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;boundary authenticate&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# List available targets&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;boundary targets list -recursive&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Accessing an internal resource takes two connections:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Establish a connection to the internal worker:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;boundary connect -target-id=ttcp_1234567890  # Your internal worker target ID&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;If nothing else on your local machine is listening on the worker port as defined above, you&amp;#39;ll open a connection to the internal worker. In your session list, this connection may be marked as &amp;#34;pending&amp;#34; since no traffic is being sent yet.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Once connected, you can access targets in the internal VLAN:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;boundary connect -target-id=ttcp_0987654321  # Internal resource target ID&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;This avoids HCP or enterprise license fees, but it adds FRP management and a second connection step.&lt;/p&gt;&#xA;&lt;h2 id=&#34;limitations-and-considerations&#34;&gt;Limitations and considerations&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#limitations-and-considerations&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;You need to manage FRP alongside Boundary&lt;/li&gt;&#xA;&lt;li&gt;The connection process requires an extra step compared with the enterprise solution&lt;/li&gt;&#xA;&lt;li&gt;Nested connections can add a small amount of latency&lt;/li&gt;&#xA;&lt;li&gt;Protect the FRP token to prevent unauthorized access&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Single-hop sessions are likely enough for my needs, with multi-hop being a tad over the top. Building this still gave me a better understanding of Boundary&amp;#39;s connection flow and another way to debug its networking.&lt;/p&gt;&#xA;&lt;p&gt;Boundary can also be integrated with Vault to broker SSH credentials. &lt;a href=&#34;https://text.techknowlogick.com/posts/use-vault-as-ssh-ca/&#34;&gt;Vault can be used as an SSH certificate authority&lt;/a&gt;, and so perhaps in a future post I might connect the two systems.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>SSH into your Gitea Actions for debugging</title>
      <link>https://text.techknowlogick.com/posts/ssh-into-your-gitea-actions-for-debugging/</link>
      <pubDate>Thu, 10 Apr 2025 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/ssh-into-your-gitea-actions-for-debugging/</guid>
      <description>Debug your Gitea Actions workflows by getting direct SSH access to the runner environment</description>
      <content:encoded>&lt;p&gt;While Gitea Actions allows for local debugging of workflows, sometimes you need to debug directly on the runner. This is especially challenging since the workflows only exist for the duration of the run. Even if you have access to the server and can &lt;code&gt;docker exec&lt;/code&gt; into the container, it may terminate while you are debugging.&lt;/p&gt;&#xA;&lt;p&gt;Since Gitea Actions are compatible with GitHub Actions, you can use &lt;a href=&#34;https://github.com/mxschmitt/action-tmate&#34; rel=&#34;nofollow&#34;&gt;action-tmate&lt;/a&gt; to SSH directly into the runner environment and debug your workflow in real time.&lt;/p&gt;&#xA;&lt;h2 id=&#34;adding-action-tmate-to-your-workflow&#34;&gt;Adding action-tmate to your workflow&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#adding-action-tmate-to-your-workflow&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;First, edit your Gitea Actions workflow YAML to add the action-tmate step.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;CI&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;l&#34;&gt;push]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;jobs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;build&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;runs-on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ubuntu-latest&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;actions/checkout@v4&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# Add this step wherever you need debugging access&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Debug with SSH&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;mxschmitt/action-tmate@v3&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When this step runs, it will pause the workflow and output SSH connection details in the logs. The workflow will wait until you&amp;#39;ve connected, finished debugging, and exited the session. However, if you place this step after a failing one, the workflow may stop before reaching it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;connecting-to-your-runner&#34;&gt;Connecting to your runner&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#connecting-to-your-runner&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Once the action runs, check your workflow logs. You&amp;#39;ll see connection details like:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;WebURL: https://tmate.io/t/...&#xA;SSH: ssh abc123...@...tmate.io&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;You can connect using either SSH or the web interface. The web interface is especially useful if your network&amp;#39;s firewall blocks SSH connections.&lt;/p&gt;&#xA;&lt;p&gt;Once connected, you&amp;#39;ll have full shell access to the runner environment, allowing you to inspect files, run commands, and verify network connectivity.&lt;/p&gt;&#xA;&lt;p&gt;When you&amp;#39;re done debugging, exit the SSH session. The workflow will then continue with the next step after action-tmate.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Security: Anyone with the connection details can access the session, so don&amp;#39;t leave it idle longer than needed. The example connection details use tmate.io, which adds an external service to the debugging path.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;targeted-debugging-approaches&#34;&gt;Targeted debugging approaches&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#targeted-debugging-approaches&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;You probably don&amp;#39;t want to SSH into every workflow run. Here are some more practical approaches:&lt;/p&gt;&#xA;&lt;h3 id=&#34;only-on-failure&#34;&gt;Only on failure&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#only-on-failure&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Debug with SSH&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;${{ failure() }}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;mxschmitt/action-tmate@v3&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This only activates the SSH session if a step in the workflow failed.&lt;/p&gt;&#xA;&lt;h3 id=&#34;manual-trigger&#34;&gt;Manual trigger&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#manual-trigger&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;With the introduction of &lt;code&gt;workflow_dispatch&lt;/code&gt; in Gitea 1.24, you can manually trigger workflows with an option to enable SSH debugging:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;inputs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;debug_enabled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;boolean&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;description&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Enable SSH debugging&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;required&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Then in your job:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Debug with SSH&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;${{ github.event_name == &amp;#39;workflow_dispatch&amp;#39; &amp;amp;&amp;amp; inputs.debug_enabled }}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;mxschmitt/action-tmate@v3&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Direct access is useful when logs are not enough. For other session controls, see the additional configuration options in &lt;a href=&#34;https://github.com/mxschmitt/action-tmate&#34; rel=&#34;nofollow&#34;&gt;action-tmate&lt;/a&gt;.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Manage an entire fleet of macOS VMs with HashiCorp Nomad</title>
      <link>https://text.techknowlogick.com/posts/nomad-manage-macos-vms/</link>
      <pubDate>Wed, 09 Apr 2025 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/nomad-manage-macos-vms/</guid>
      <description>Building a Nomad driver plugin to integrate with Tart macOS VMs</description>
      <content:encoded>&lt;p&gt;To learn how Nomad driver plugins work, I built a &lt;a href=&#34;https://gitea.com/techknowlogick/nomad-driver-tart&#34; rel=&#34;nofollow&#34;&gt;driver plugin for Tart&lt;/a&gt;. It lets Nomad manage Tart macOS VMs in much the same way that it manages Docker containers.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Disclaimer: This driver is a proof of concept and is not ready for production use. For a production-ready option built by Tart&amp;#39;s developers, consider &lt;a href=&#34;https://tart.run/orchard/quick-start/&#34; rel=&#34;nofollow&#34;&gt;Orchard&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;tart&#34;&gt;Tart&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#tart&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://tart.run/&#34; rel=&#34;nofollow&#34;&gt;Tart&lt;/a&gt; is a VM manager for macOS VMs on Apple Silicon. It integrates with OCI-compatible container registries, so VM images can be stored and distributed like Docker container images.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-driver-experiment&#34;&gt;The driver experiment&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-driver-experiment&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;One of the ways I learn a new technology is by building something with it. Nomad already has drivers for workloads such as Docker, so I chose Tart while learning the plugin system. Tart manages VMs on one machine rather than orchestrating them across several, which made it a useful fit for a driver. Tart also has its own orchestration manager, &lt;a href=&#34;https://tart.run/orchard/quick-start/&#34; rel=&#34;nofollow&#34;&gt;Orchard&lt;/a&gt;, but Nomad can manage other workloads alongside the VMs. Potential uses include CI/CD workflows, ephemeral development environments, and testing infrastructure.&lt;/p&gt;&#xA;&lt;h2 id=&#34;sample-nomad-job&#34;&gt;Sample Nomad job&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#sample-nomad-job&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;This job starts a macOS VM:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;job&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tart-demo&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  datacenters&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;dc1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  type&lt;/span&gt;        &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;service&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;group&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;demo&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;vm&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      driver&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tart&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;config&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        image&lt;/span&gt;     &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ghcr.io/cirruslabs/macos-sequoia-base:latest&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        cpu&lt;/span&gt;       &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        memory&lt;/span&gt;    &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;8192&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        disk_size&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;50&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        command&lt;/span&gt;   &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;echo &amp;#39;Hello from Tart VM&amp;#39;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded>
    </item>
    <item>
      <title>Trouble with Azure Functions and managed identities</title>
      <link>https://text.techknowlogick.com/posts/automating-tls-certs-with-azure-functions-part1/</link>
      <pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/automating-tls-certs-with-azure-functions-part1/</guid>
      <description>Using Azure Functions managed identity endpoints for TLS certificate automation</description>
      <content:encoded>&lt;p&gt;While automating TLS certificates for Azure Application Gateways, I ran into an issue with Azure Functions and managed identities. The tool I was using could not fetch a managed identity token. After reviewing its code and the Azure documentation, I found that Azure Functions exposes environment variables for endpoint information that differs slightly from the standard endpoint.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: Please don&amp;#39;t take any of this in a negative light. The functionality that would normally be provided by an SDK couldn&amp;#39;t be used, and the implementation is uncommon enough that it&amp;#39;s understandable a tool like acme.sh wouldn&amp;#39;t cover this edge case. It is a great tool, and I&amp;#39;m very happy with it.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I firmly believe that all TLS certificates should be automated. The goal of this project is to request certificates from Let&amp;#39;s Encrypt or ZeroSSL and store them in Azure Key Vault for Azure Application Gateway to use. The first problem I hit was managed identity authentication.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-setup&#34;&gt;The setup&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-setup&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;My plan was to run &lt;a href=&#34;https://github.com/acmesh-official/acme.sh&#34; rel=&#34;nofollow&#34;&gt;acme.sh&lt;/a&gt; in an Azure Function and use Azure&amp;#39;s managed identity to authenticate with Azure Key Vault. The function could then store certificates directly in Key Vault for Application Gateway.&lt;/p&gt;&#xA;&lt;p&gt;Running the Azure Function on a schedule would renew certificates before expiration without manual work.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-problem&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Installing acme.sh in the Azure Function was quick, but DNS validation with a managed identity could not fetch a managed identity token.&lt;/p&gt;&#xA;&lt;p&gt;The error showed that acme.sh could not connect to the managed identity token service. In most Azure environments, services access that token service through a link-local address, typically &lt;code&gt;169.254.169.254&lt;/code&gt;. This endpoint is &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service&#34; rel=&#34;nofollow&#34;&gt;well documented&lt;/a&gt; and widely used, so acme.sh would normally use the correct endpoint.&lt;/p&gt;&#xA;&lt;p&gt;After connecting to the Azure Function over SSH, I confirmed that it could not reach the metadata endpoint. The &lt;a href=&#34;https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/identity/azure-identity/TROUBLESHOOTING.md#verify-the-app-service-managed-identity-endpoint-is-available&#34; rel=&#34;nofollow&#34;&gt;Python SDK documentation&lt;/a&gt; pointed me to the &lt;code&gt;IDENTITY_ENDPOINT&lt;/code&gt; environment variable for the correct endpoint.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-fix&#34;&gt;The fix&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-fix&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With the correct endpoint and the additional authentication header, I fetched the managed identity token with curl. I then searched acme.sh&amp;#39;s code to see how it fetched the token. It made the same hardcoded-endpoint assumption I had, so I put together a small change to fix it.&lt;/p&gt;&#xA;&lt;p&gt;In the spirit of open-source, and to thank the authors for their work on acme.sh, I was able to send that change as a &lt;a href=&#34;https://github.com/acmesh-official/acme.sh/pull/6283&#34; rel=&#34;nofollow&#34;&gt;PR&lt;/a&gt; to fix the issue so that no one else would have to go through the same debugging process.&lt;/p&gt;&#xA;&lt;h2 id=&#34;next-steps&#34;&gt;Next steps&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#next-steps&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I plan to cover the certificate automation for Azure Application Gateway in a later post.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>SSH Multi-Factor Authentication with SoloKey V2</title>
      <link>https://text.techknowlogick.com/posts/ssh-multi-factor-auth-with-solokey-v2/</link>
      <pubDate>Sun, 30 Mar 2025 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/ssh-multi-factor-auth-with-solokey-v2/</guid>
      <description>Improving SSH security a second factor at a time</description>
      <content:encoded>&lt;p&gt;In a bout of attempting to improve my operational security, I decided to review my current practices and see which areas I could improve. While I already use SSH keys for auth, having a file that is essentially a password to all my servers is a bit of a security risk if it ever is compromised. I already use my SoloKey for 2FA on various websites, so why not for SSH too? This way, even if someone gets my private key file, they can&amp;#39;t access my servers without physically having my SoloKey. Still a risk (see the XKCD comic about a $5 wrench), but less so.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: While MFA by using a physical key is a great improvement, you really should look into using SSH Certificates as they are time scoped, and depending on how you have them set up, have automatic rotation. Among other benefits, this is a great way to ensure that even if a key is unknowingly compromised, it can&amp;#39;t be used for long.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Also, this same process could be used for other physical keys like YubiKeys, but I have several SoloKeys V2 (cheaper and open-source) so that is what I used.&lt;/p&gt;&#xA;&lt;h2 id=&#34;openssh-support-on-macos&#34;&gt;OpenSSH support on macOS&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#openssh-support-on-macos&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Creating the key starts with &lt;code&gt;ssh-keygen -t ed25519-sk&lt;/code&gt; and touching the SoloKey when prompted. On the macOS devices I tested, the bundled SSH version did not support this key type, so I installed a newer version of OpenSSH:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew install openssh libfido2&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After installation, you can check that you have the right version:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh -V&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You should see OpenSSH_8.2 or later, which supports the FIDO2 keys. If you don&amp;#39;t, then you may need to update your &lt;code&gt;$PATH&lt;/code&gt; environment variable to point to the Homebrew version of SSH.&lt;/p&gt;&#xA;&lt;h2 id=&#34;creating-a-key-tied-to-your-solokey&#34;&gt;Creating a key tied to your SoloKey&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#creating-a-key-tied-to-your-solokey&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With the newer OpenSSH installed, create the key:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You&amp;#39;ll be asked for a passphrase (recommended), and be prompted to touch your SoloKey to confirm. Once done, you&amp;#39;ll have two new files:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;~/.ssh/id_ed25519_sk&lt;/code&gt; - your private key&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;~/.ssh/id_ed25519_sk.pub&lt;/code&gt; - your public key&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;making-it-convenient&#34;&gt;Making it convenient&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#making-it-convenient&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;For convenience, I added the key to my SSH agent:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh-add ~/.ssh/id_ed25519_sk&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you don&amp;#39;t use the SSH agent, you could instead add the key to your SSH configuration file&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vi ~/.ssh/config&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;# Use the security key for all connections&#xA;Host *&#xA;    IdentityFile ~/.ssh/id_ed25519_sk&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;setting-up-the-server-side&#34;&gt;Setting up the server side&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#setting-up-the-server-side&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Similarly to how you would add a regular SSH key, you can add the public key to your server:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cat ~/.ssh/id_ed25519_sk.pub &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; pbcopy&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh user@remote-server &lt;span class=&#34;s2&#34;&gt;&amp;#34;vi ~/.ssh/authorized_keys&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# Paste the public key and save&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Just like with your local machine, you&amp;#39;ll need to make sure the remote server also supports the new key type. You can check this by running:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh user@remote-server &lt;span class=&#34;s2&#34;&gt;&amp;#34;ssh -V&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;the-moment-of-truth&#34;&gt;The moment of truth&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-moment-of-truth&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now try connecting:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh user@remote-server&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If everything&amp;#39;s set up correctly, you&amp;#39;ll be prompted to touch your SoloKey to authenticate.&lt;/p&gt;&#xA;&lt;p&gt;The older OpenSSH version bundled with macOS was the only setup problem I hit. The same key can also authenticate to other services, including Gitea.&lt;/p&gt;&#xA;&lt;p&gt;If you are on a Mac, you could also look into &lt;a href=&#34;https://github.com/maxgoedjen/secretive&#34; rel=&#34;nofollow&#34;&gt;Secretive&lt;/a&gt;, which stores SSH keys in the Secure Enclave and integrates with Touch ID.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Gitea Actions: Forget about YAML, let&#39;s use Nix</title>
      <link>https://text.techknowlogick.com/posts/gitea-actions-but-using-nix/</link>
      <pubDate>Sun, 04 Aug 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/gitea-actions-but-using-nix/</guid>
      <description>YAML has brought me too many headaches, so I&#39;m switching to Nix!</description>
      <content:encoded>&lt;p&gt;I maintain many repositories using Gitea Actions, and I&amp;#39;ve run into pipeline failures over small issues. Whitespace differences in YAML files are the most common cause, leading to unexpected parsing or complete failures. Recently, I helped debug a copied workflow where some of the indentation used tabs and the rest used spaces. That was enough to make the pipeline fail.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Update: After writing this post, I discovered a similar approach using Dhall. If you&amp;#39;re interested in this concept, check out &lt;a href=&#34;https://github.com/regadas/github-actions-dhall&#34; rel=&#34;nofollow&#34;&gt;github.com/regadas/github-actions-dhall&lt;/a&gt;, which has already implemented much of what I discuss here with Dhall instead of Nix.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;the-problem-with-yaml&#34;&gt;The problem with YAML&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-problem-with-yaml&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;YAML is extremely sensitive to whitespace, which can lead to subtle issues that are hard to debug when tabs and spaces look alike.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-solution-nix&#34;&gt;The solution: Nix&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-solution-nix&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;In my case, as is the case with all my problems, the solution was to use Nix. I had been using Terranix to manage some infrastructure. Terranix translates Nix to JSON for Terraform, and I realized a similar approach could work for Gitea Actions. JSON and YAML both represent data as key-value pairs, so I can convert Nix to JSON and then to YAML.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s note: The Nix below is an untested rough draft of the approach. If you try it, I&amp;#39;d be interested in any feedback so I can update this post.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&#34;potential-nix-benefits&#34;&gt;Potential Nix benefits&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#potential-nix-benefits&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Fewer whitespace-related errors in handwritten workflows&lt;/li&gt;&#xA;&lt;li&gt;Nix features such as conditionals, loops, and functions&lt;/li&gt;&#xA;&lt;li&gt;Code reuse across workflows&lt;/li&gt;&#xA;&lt;li&gt;Workflows split into smaller components&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;implementing-nix-based-workflows&#34;&gt;Implementing Nix-based workflows&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#implementing-nix-based-workflows&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;step-1-create-a-basic-step-function&#34;&gt;Step 1: Create a basic step function&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#step-1-create-a-basic-step-function&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A step is the most common workflow component, so I&amp;#39;ll start with a function that creates one.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# steps.nix&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;let&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;mkStep&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uses&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;run&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;let&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;n&#34;&gt;step&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;uses&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uses&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uses&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;run&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;run&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;run&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&amp;#39;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nb&#34;&gt;builtins&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;filterAttrs&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;v&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;v&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;step&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;mkStep&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This &lt;code&gt;mkStep&lt;/code&gt; function creates a step object and removes null fields from the YAML output.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Note: &lt;code&gt;with&lt;/code&gt; is a reserved keyword in Nix, so I used &lt;code&gt;with&amp;#39;&lt;/code&gt; instead.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&#34;step-2-create-a-checkout-function&#34;&gt;Step 2: Create a checkout function&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#step-2-create-a-checkout-function&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Checkout is another common workflow step. This function builds it with &lt;code&gt;mkStep&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# steps.nix&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ... keep the previous code and add the new `mkCheckout` function&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;mkCheckout&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uses&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;actions/checkout@v4&amp;#34;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;mkStep&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;n&#34;&gt;uses&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;n&#34;&gt;with&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Export both functions&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;mkStep&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mkStep&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;mkCheckout&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mkCheckout&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The same pattern could cover other common actions. Generating functions from &lt;code&gt;action.yaml&lt;/code&gt; files is another possibility.&lt;/p&gt;&#xA;&lt;h3 id=&#34;step-3-create-a-workflow&#34;&gt;Step 3: Create a workflow&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#step-3-create-a-workflow&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The functions above can create a complete workflow.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# workflow.nix&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;let&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;steps&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;./steps.nix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;mySteps&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mkCheckout&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&amp;#39;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fetch-depth&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mkStep&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;run&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;echo &amp;#39;Hello, World!&amp;#39;&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# More steps could be added here&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Example Workflow&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;on&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;push&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;jobs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;n&#34;&gt;build&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;runs-on&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ubuntu-latest&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;steps&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mySteps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;generate-the-gitea-actions-yaml-workflow&#34;&gt;Generate the Gitea Actions YAML workflow&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#generate-the-gitea-actions-yaml-workflow&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Convert the Nix workflow to YAML with:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nix-instantiate --eval --strict --json ./workflow.nix &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; yq &lt;span class=&#34;nb&#34;&gt;eval&lt;/span&gt; -P - &amp;gt; example.yaml&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This requires &lt;code&gt;yq&lt;/code&gt; and writes the generated workflow to &lt;code&gt;example.yaml&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;possible-next-steps&#34;&gt;Possible next steps&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#possible-next-steps&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;This may be most useful where projects have similar workflows. The Gitea project has one workflow for nightly release artifacts and another for tagged release artifacts. Arguments and conditionals could move their shared definitions into one place instead of maintaining two similar files.&lt;/p&gt;&#xA;&lt;p&gt;Nix flakes are another possible way to define inputs for an Action step and pass them to the workflow.&lt;/p&gt;&#xA;&lt;p&gt;This approach replaces handwritten YAML with Nix, but it adds Nix and &lt;code&gt;yq&lt;/code&gt; to the workflow toolchain and still generates YAML as an intermediate file. Whether that tradeoff is worthwhile depends on how much workflow code can be shared.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Streamlining My GCP SSH Workflow: Using SSH Configuration Tricks</title>
      <link>https://text.techknowlogick.com/posts/google-cloud-ssh-helper/</link>
      <pubDate>Thu, 18 Jul 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/google-cloud-ssh-helper/</guid>
      <description>How I simplified SSH access to GCP instances, increased security, and decreased points of failure.</description>
      <content:encoded>&lt;p&gt;I often recreate virtual machines (VMs) in Google Cloud Platform (GCP), which means their connection information changes frequently. That leaves stale entries in &lt;code&gt;known_hosts&lt;/code&gt; and creates more DNS work as the number of servers grows. Using GCP&amp;#39;s Identity-Aware Proxy (IAP) also requires the &lt;code&gt;gcloud&lt;/code&gt; CLI. So instead of dealing with this complexity each time, I used my past exprience with SSH configuration customization to handle those moving parts.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: At the time I made heavy use of GCP, however now I am using other hyperscalers. This post is kept up for historical purposes.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;the-challenge&#34;&gt;The challenge&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-challenge&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There were several issues I needed to solve:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Frequent VM recreation leading to connection information changes&lt;/li&gt;&#xA;&lt;li&gt;Managing stale &lt;code&gt;known_hosts&lt;/code&gt; entries&lt;/li&gt;&#xA;&lt;li&gt;DNS management for numerous servers&lt;/li&gt;&#xA;&lt;li&gt;The necessity of using IAP for secure access&lt;/li&gt;&#xA;&lt;li&gt;The complexity of the &lt;code&gt;gcloud compute ssh&lt;/code&gt; command&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;requirements&#34;&gt;Requirements&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#requirements&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The setup needs:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;The &lt;code&gt;gcloud&lt;/code&gt; CLI installed locally&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;nc&lt;/code&gt; (netcat) installed on the remote servers to proxy the connection from IAP to the VM&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;gcloud compute ssh&lt;/code&gt; wraps &lt;code&gt;ssh&lt;/code&gt; to handle authentication and IP resolution. It becomes unwieldy with other SSH-based tools such as Ansible and &lt;code&gt;rsync&lt;/code&gt; because each tool needs the command passed through to it. I had previously used &lt;a href=&#34;https://text.techknowlogick.com/posts/use-vault-as-ssh-ca/&#34;&gt;Vault to make SSH run a command for each connection&lt;/a&gt;, so I tried the same approach here. A &lt;a href=&#34;https://unix.stackexchange.com/a/592199&#34; rel=&#34;nofollow&#34;&gt;Stack Overflow answer&lt;/a&gt; gave me a starting point that I adapted for this setup.&lt;/p&gt;&#xA;&lt;p&gt;I use GCP&amp;#39;s Identity-Aware Proxy to tunnel connections without exposing SSH to the public or assigning public IPs to each VM. The &lt;code&gt;gcloud&lt;/code&gt; CLI provides this through the &lt;code&gt;--tunnel-through-iap&lt;/code&gt; flag.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ssh-configuration&#34;&gt;SSH configuration&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#ssh-configuration&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I added this block to my SSH configuration:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Host *.gcloud&#xA;    ProxyCommand bash -c &amp;#39;IFS=. read -r server zone project _ &amp;lt;&amp;lt;&amp;lt; &amp;#34;${1}&amp;#34;; gcloud compute --project &amp;#34;$project&amp;#34; ssh --zone &amp;#34;$zone&amp;#34; --ssh-key-file ~/.ssh/gcp_key --tunnel-through-iap &amp;#34;${2}@$server&amp;#34; --command=&amp;#34;nc 0.0.0.0 22&amp;#34;&amp;#39; _ %h %r&#xA;    IdentityFile ~/.ssh/gcp_key&#xA;    # GCP handles the known hosts, and hostkey checking separately, so we can ignore them here&#xA;    StrictHostKeyChecking no&#xA;    UserKnownHostsFile=/dev/null&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This configuration takes a hostname I provide (e.g., &lt;code&gt;server1.us-west1-b.project1-id.gcloud&lt;/code&gt;) and converts it into variables that are then passed to the &lt;code&gt;gcloud&lt;/code&gt; CLI.&lt;/p&gt;&#xA;&lt;p&gt;I also wanted to forward ports from inside GCP&amp;#39;s network to my local machine. For a particular host, define a more specific &lt;code&gt;Host&lt;/code&gt; block and add its options there. SSH will also apply the settings from the wildcard block.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Host server1.us-west1-b.project1-id.gcloud&#xA;    LocalForward 3306 10.20.30.40:3306&#xA;&#xA;# Host *.gcloud...&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;troubleshooting&#34;&gt;Troubleshooting&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#troubleshooting&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If this setup does not connect, make sure you&amp;#39;ve added the IAP IP addresses to your GCP network ingress rules. You can find the list in the &lt;a href=&#34;https://cloud.google.com/iap/docs/using-tcp-forwarding#iap-tcp-forwarding-ip-ranges&#34; rel=&#34;nofollow&#34;&gt;GCP documentation&lt;/a&gt;. You may also need to sign in to the &lt;code&gt;gcloud&lt;/code&gt; CLI.&lt;/p&gt;&#xA;&lt;p&gt;This setup has saved me a lot of time. I could manage DNS entries when creating servers with infrastructure as code, but that would add another piece of configuration that can fail. This approach also keeps the VMs&amp;#39; SSH ports off the public internet.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Use Drone plugins in Gitea Actions</title>
      <link>https://text.techknowlogick.com/posts/use-drone-plugins-as-gitea-action-step/</link>
      <pubDate>Sat, 29 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/use-drone-plugins-as-gitea-action-step/</guid>
      <description>Running existing Drone plugin images as Gitea Actions steps</description>
      <content:encoded>&lt;p&gt;Drone.IO runs each pipeline step in a Docker container. Drone plugins are containers with a predefined task and settings supplied by the pipeline. Because they are published as Docker images, the same plugins can also run in Gitea Actions.&lt;/p&gt;&#xA;&lt;p&gt;One plugin I used was &lt;a href=&#34;https://github.com/appleboy/drone-scp&#34; rel=&#34;nofollow&#34;&gt;drone-scp&lt;/a&gt;, which copies files from the build pipeline to a remote server. My Drone step looked like this:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;scp files&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;appleboy/drone-scp&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;settings&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;example1.com&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ubuntu&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;password&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;from_secret&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ssh_password&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;target&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;/home/deploy/web&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;source&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;release/*.tar.gz&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The step calls the predefined binary in the drone-scp image, which copies files from the release directory to the target directory on the remote server. The full script does not need to live in the pipeline.&lt;/p&gt;&#xA;&lt;p&gt;Because drone-scp is published as a Docker image, Gitea Actions can run it too. The quick approach works for most plugins. The more involved approach wraps the plugin as a full Gitea Action.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-simple-approach-use-the-docker-image-directly&#34;&gt;The simple approach: use the Docker image directly&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-simple-approach-use-the-docker-image-directly&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The easiest way to use a Drone plugin in Gitea Actions is to reference the Docker image directly with &lt;code&gt;uses: docker://&lt;/code&gt; and pass the plugin&amp;#39;s expected &lt;code&gt;PLUGIN_&lt;/code&gt; environment variables via the step&amp;#39;s &lt;code&gt;env:&lt;/code&gt; block:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;copy files&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;l&#34;&gt;push]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;jobs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;build&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Build&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;runs-on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ubuntu-latest&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;actions/checkout@v4&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;docker://appleboy/drone-scp&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;env&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;PLUGIN_HOST&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;${{ secrets.HOST }}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;PLUGIN_USERNAME&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ubuntu&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;PLUGIN_PASSWORD&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;${{ secrets.SSH_PASSWORD }}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;PLUGIN_TARGET&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;/home/deploy/web&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;PLUGIN_SOURCE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;release/*.tar.gz&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This works because Drone plugins read their configuration from &lt;code&gt;PLUGIN_&lt;/code&gt;-prefixed environment variables (derived from the &lt;code&gt;settings:&lt;/code&gt; block in Drone), and step-level &lt;code&gt;env:&lt;/code&gt; variables in Gitea Actions are passed directly into Docker containers. No repackaging is needed.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-full-approach-wrap-the-plugin-as-a-gitea-action&#34;&gt;The full approach: wrap the plugin as a Gitea Action&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#the-full-approach-wrap-the-plugin-as-a-gitea-action&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you want to publish a reusable Action with defined inputs (so consumers use &lt;code&gt;with:&lt;/code&gt; instead of &lt;code&gt;env:&lt;/code&gt;), you can wrap the Drone plugin in a Dockerfile, entrypoint script, and &lt;code&gt;action.yml&lt;/code&gt;. The Dockerfile should look something like this:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;appleboy/drone-scp&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; entrypoint.sh /entrypoint.sh&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; chmod +x /entrypoint.sh&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENTRYPOINT&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;/entrypoint.sh&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The entrypoint passes the action&amp;#39;s input arguments to the drone-scp binary:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/sh&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;set&lt;/span&gt; -eu&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sh -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;/bin/drone-scp &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$*&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;&#xA;&lt;p&gt;Note: When using the &lt;code&gt;with:&lt;/code&gt; block in Gitea Actions (like GitHub Actions), inputs are passed to Docker containers as environment variables prefixed with &lt;code&gt;INPUT_&lt;/code&gt; (e.g. &lt;code&gt;INPUT_HOST&lt;/code&gt;). Drone plugins natively expect the &lt;code&gt;PLUGIN_&lt;/code&gt; prefix (e.g. &lt;code&gt;PLUGIN_HOST&lt;/code&gt;). Many popular Drone plugin authors (including Appleboy) have updated their binaries to accept both prefixes. If a plugin does not recognise &lt;code&gt;INPUT_&lt;/code&gt; variables, you may need to translate them in the entrypoint script by mapping each &lt;code&gt;INPUT_*&lt;/code&gt; variable to its corresponding &lt;code&gt;PLUGIN_*&lt;/code&gt; variable.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;The &lt;code&gt;action.yml&lt;/code&gt; file tells Gitea Actions which inputs to use.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;drone-scp Gitea Action&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# This tells the Gitea Action runner what type of Action this is&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# in this case, it&amp;#39;s &amp;#34;docker&amp;#34;, and the image to use is the Dockerfile&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;runs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;using&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;docker&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;image&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Dockerfile&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;inputs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;description&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;scp remote host&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ... define all the remaining inputs that exist for the Drone Plugin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can then reference this action in a Gitea Actions workflow the same way you used the Drone.IO plugin.&lt;/p&gt;&#xA;&lt;p&gt;For example:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;copy files&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;l&#34;&gt;push]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;jobs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;build&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Build&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;runs-on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ubuntu-latest&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;actions/checkout@v4&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;https://gitea.com/techknowlogick/scp-gitea-action@main&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;${{ secrets.HOST }}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nn&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Appleboy, the author of the SCP Drone plugin, has already done this. The resulting &lt;a href=&#34;https://github.com/appleboy/scp-action/&#34; rel=&#34;nofollow&#34;&gt;scp-action&lt;/a&gt; can be used directly.&lt;/p&gt;&#xA;&lt;p&gt;Plugins that accept &lt;code&gt;INPUT_&lt;/code&gt; variables can use this wrapper directly. Plugins that only accept &lt;code&gt;PLUGIN_&lt;/code&gt; variables need the mapping described above, but the plugin itself does not need to be rewritten. This keeps the existing implementation, including plugins written in languages the runner does not support natively.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Experimenting with Proxmox on Apple Silicon: Turning Your M1 MacBook Air into a Virtual Datacentre</title>
      <link>https://text.techknowlogick.com/posts/running-proxmox-on-your-apple-silicon-mac-with-utm/</link>
      <pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/running-proxmox-on-your-apple-silicon-mac-with-utm/</guid>
      <description>Playing around with Proxmox on unsupported Apple Silicon hardware</description>
      <content:encoded>&lt;p&gt;In an effort to create an easy-to-manage VM environment that I can use on the go, even with spotty internet, I decided to try running Proxmox on my Apple Silicon MacBook Air. Parallels and VMware Fusion were options, but I chose Proxmox to support open-source software. For production-level workloads, I would purchase a Proxmox license and use a dedicated server. Proxmox doesn&amp;#39;t distribute arm64/aarch64 packages, so this experiment depended on an unofficial port.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: These instructions are now out of date, as there are newer versions of Proxmox, and they may support Arm too. This post is kept up for historical reference.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;getting-started&#34;&gt;Getting started&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#getting-started&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Proxmox is based on Debian but does not come packaged for arm64. A Debian installation can be converted to Proxmox, so I tried the same route used for x86_64.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://mac.getutm.app/&#34; rel=&#34;nofollow&#34;&gt;UTM&lt;/a&gt; provides an interface for Apple&amp;#39;s Hypervisor and QEMU. I could have used QEMU directly, but I chose UTM and created a Debian VM from one of the premade images in its gallery. Its presets supplied the VM configuration.&lt;/p&gt;&#xA;&lt;h2 id=&#34;converting-debian-to-proxmox&#34;&gt;Converting Debian to Proxmox&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#converting-debian-to-proxmox&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With Debian running, I needed Proxmox packages for Arm. Since Proxmox doesn&amp;#39;t support Arm or distribute their packages for Arm I would have to build those myself. Luckily, some other &lt;a href=&#34;https://github.com/jiangcuo/Proxmox-Port&#34; rel=&#34;nofollow&#34;&gt;kind soul&lt;/a&gt; already did this and published them.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Warning: I&amp;#39;m installing packages from an unofficial source. My risk level is fine with this, but if you are considering doing this, I strongly recommend you evaluate the choice carefully. If you are not comfortable, you can build the packages from source.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Before installing the packages, I made sure the VM&amp;#39;s hostname resolved through &lt;code&gt;/etc/hosts&lt;/code&gt;. The VM already used &lt;code&gt;debian&lt;/code&gt;, so I added &lt;code&gt;192.168.64.11 debian&lt;/code&gt; to the end of the hosts file. This allowed both the VM and the Proxmox installer to resolve the hostname.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Update: These packages depended on OpenSSL 1.1, which is now removed from Debian, and so you&amp;#39;ll need to build from source and link the packages to OpenSSL 3+&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;These are the commands I used for the original package installation:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Add the 3rd party repository&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;deb https://global.mirrors.apqa.cn/proxmox/debian/pve bullseye port&amp;#34;&lt;/span&gt;&amp;gt;/etc/apt/sources.list.d/pveport.list&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Download/Trust the key that signs the 3rd party packages&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl https://global.mirrors.apqa.cn/proxmox/debian/pveport.gpg -o /etc/apt/trusted.gpg.d/pveport.gpg &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# pull the latest updates from the repositories, and update the system&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apt update &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt full-upgrade&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# install ifupdown2, which is required for Proxmox&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apt install ifupdown2&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# and finally, install Proxmox&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;apt install proxmox-ve postfix open-iscsi&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After running these commands, I rebooted the VM and opened the Proxmox admin interface at &lt;code&gt;https://YOUR_IP_HERE:8006&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;running-proxmox&#34;&gt;Running Proxmox&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#running-proxmox&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The resulting installation used the standard Proxmox interface. Any container or VM still needed an arm64/aarch64 image.&lt;/p&gt;&#xA;&lt;h2 id=&#34;alternatives-considered&#34;&gt;Alternatives considered&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#alternatives-considered&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Other options included Parallels, VMware Fusion, or Asahi Linux with VMs running directly on the host. A separate device would avoid running Proxmox inside another hypervisor, but I did not want to carry one when I was away from home. I preferred keeping the environment on my laptop without disrupting my day-to-day desktop.&lt;/p&gt;&#xA;&lt;h2 id=&#34;results&#34;&gt;Results&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#results&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;On the M1 MacBook Air, I was able to run several VMs and containers without a noticeable performance hit. The experience was better than I expected from an x86_64 system with similar specifications, but I was not running a serious workload.&lt;/p&gt;&#xA;&lt;p&gt;Terraform could&amp;#39;ve automated the environment setup, and the Proxmox API could script operations. I plan to keep experimenting with both.&lt;/p&gt;&#xA;&lt;h2 id=&#34;disclaimer&#34;&gt;Disclaimer&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#disclaimer&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: This setup is not recommended for production use. The arm64/aarch64 packages are unofficial and not provided by Proxmox. An M1 MacBook Air is not intended to be a server.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Bye-bye Docker Hub, time to self-host all of my Docker images</title>
      <link>https://text.techknowlogick.com/posts/migrate-docker-images-to-self-hosted-registry/</link>
      <pubDate>Wed, 26 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/migrate-docker-images-to-self-hosted-registry/</guid>
      <description>As with all my other services, such as my Git repositories, I&#39;ve been slowly moving them to my own self-hosted homelab. Today, it is my Docker images&#39; turn.</description>
      <content:encoded>&lt;p&gt;Gitea includes a built-in Docker registry, so I can host Docker and other OCI-compliant images without relying on Docker Hub or dealing with its rate limits.&lt;/p&gt;&#xA;&lt;p&gt;The MVP (&amp;#34;most valuable player&amp;#34;) of this whole thing is a tool called &lt;a href=&#34;https://github.com/containers/skopeo&#34; rel=&#34;nofollow&#34;&gt;skopeo&lt;/a&gt;. It is a command-line tool for working with container images and registries, and it does not require root access or a running Docker daemon.&lt;/p&gt;&#xA;&lt;p&gt;I used &lt;code&gt;skopeo&lt;/code&gt; and a small Bash script to migrate some of my Docker images from Docker Hub to Gitea. Gitea enables its package registry by default, so this only covers moving the images. The registry operations are not specific to Gitea, although the script uses the Docker Hub API to discover source repositories.&lt;/p&gt;&#xA;&lt;p&gt;Skopeo cannot fetch the full list of images in a namespace, so the script uses the Docker Hub API to get that list before passing each image to &lt;code&gt;skopeo&lt;/code&gt;. It uses &lt;code&gt;skopeo sync&lt;/code&gt; rather than &lt;code&gt;skopeo copy&lt;/code&gt; because &lt;code&gt;sync&lt;/code&gt; copies all tags without another API call to list them.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;## Variables to change&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# TODO: these shouldn&amp;#39;t be hardcoded, but rather passed in as arguments or via env vars&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#       but that&amp;#39;s a future problem&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Docker Hub credentials&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;HUB_NAMESPACE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;namespace&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# the user/org namespace on Docker Hub that you want to bring over&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;HUB_USERNAME&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;username&amp;gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;HUB_PASSWORD&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;password&amp;gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_DOMAIN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;domain&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# e.g. gitea.example.com&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_PACKAGE_NAMESPACE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;namespace&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# the user/org that you wish to store the images under&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_USERNAME&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;username&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# user that has access to the package registry namespace&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_TOKEN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;password&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# token needs the packages:write permission&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Function to check if a command exists&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;command_exists&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;command&lt;/span&gt; -v &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$1&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &amp;gt;/dev/null 2&amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Check if jq and skopeo are installed&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; ! command_exists jq &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; ! command_exists skopeo&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;either jq or skopeo is missing, please make sure they are both installed&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# login into Docker Hub and fetch an API token&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# its possible to do this without the token, but you&amp;#39;ll likely run into ratelimits, and it would also only provide public images&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;TOKEN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;curl -s -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; -X POST -d &lt;span class=&#34;s1&#34;&gt;&amp;#39;{&amp;#34;username&amp;#34;: &amp;#34;&amp;#39;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;HUB_USERNAME&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;#34;, &amp;#34;password&amp;#34;: &amp;#34;&amp;#39;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;HUB_PASSWORD&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;#34;}&amp;#39;&lt;/span&gt; https://hub.docker.com/v2/users/login/ &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; jq -r .token&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Get list 100 of images that the namespace has, if you have more than that then pagination will need to be added&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;REPOS&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;curl -s -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Authorization: JWT &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;TOKEN&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://hub.docker.com/v2/repositories/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;HUB_NAMESPACE&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/?page_size=100&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; jq -r &lt;span class=&#34;s1&#34;&gt;&amp;#39;.results[].name&amp;#39;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# TODO: The fetching of images above is Docker Hub API specific, and so if your source is non-Docker Hub this will need to be adjusted&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Loop through images and pass them to skopeo&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; repo in &lt;span class=&#34;nv&#34;&gt;$REPOS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Syncing &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;HUB_NAMESPACE&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;repo&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; to &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_DOMAIN&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_PACKAGE_NAMESPACE&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;repo&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    skopeo sync &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        --src docker --src-creds &lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;HUB_USERNAME&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;:&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;HUB_PASSWORD&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        --dest docker --dest-creds &lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_USERNAME&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;:&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_TOKEN&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;docker.io/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;HUB_NAMESPACE&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;repo&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_DOMAIN&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;GITEA_PACKAGE_NAMESPACE&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;done&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Finished moving images!&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Changing &lt;code&gt;HUB_NAMESPACE&lt;/code&gt; lets the same script copy another Docker Hub namespace, such as Bitnami.&lt;/p&gt;&#xA;&lt;p&gt;The script worked for my namespace, which had fewer than 100 images. It still needs argument or environment-variable handling, error handling, and Docker Hub API pagination for larger namespaces. If you modify it, I&amp;#39;d love to hear about it.&lt;/p&gt;&#xA;&lt;p&gt;If you want to read more about the &lt;code&gt;skopeo sync&lt;/code&gt; options, you can check out the &lt;a href=&#34;https://github.com/containers/skopeo/blob/main/docs/skopeo-sync.1.md&#34; rel=&#34;nofollow&#34;&gt;skopeo sync documentation&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Disclaimer: I am a maintainer of Gitea. Skopeo can use other registries, so the migration does not require Gitea as the destination.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Let WireGuard clients use your BGP-advertised IP blocks</title>
      <link>https://text.techknowlogick.com/posts/assigning-your-bgp-routes-to-wireguard-clients/</link>
      <pubDate>Sun, 16 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/assigning-your-bgp-routes-to-wireguard-clients/</guid>
      <description>Assign BGP-advertised IPv4 addresses to WireGuard clients.</description>
      <content:encoded>&lt;p&gt;This is an addendum to my previous post on &lt;a href=&#34;https://text.techknowlogick.com/posts/giving-yourself-ipv6-with-wireguard/&#34;&gt;giving yourself an IPv6 address using WireGuard&lt;/a&gt;. If you have an IPv4 range from before the &amp;#34;great IPv4 exhaustion&amp;#34;, you can assign addresses from that range to WireGuard clients.&lt;/p&gt;&#xA;&lt;p&gt;Several cloud providers, including OVH, Vultr, and Equinix Metal, allow customers to advertise their own IP ranges. OVH is the example here because its bandwidth costs are predictable and it does not require a separate BGP connection, which makes setup less work.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Note: If you followed part 1 and added a MASQUERADE rule for your network interface, you will need to remove it when assigning real public IPs from your own range. The MASQUERADE rule rewrites the source IP of outgoing packets to the server&amp;#39;s address, which defeats the purpose of giving clients their own public IP. Remove it with &lt;code&gt;iptables -t nat -D POSTROUTING -o &amp;lt;YOUR_NETWORK_INTERFACE&amp;gt; -j MASQUERADE&lt;/code&gt; (and the equivalent ip6tables rule if applicable).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;With the WireGuard connections from the previous post already set up, assign each client an address from your range. This example uses &lt;code&gt;8.8.8.0/24&lt;/code&gt;, the TEST-NET-1 block reserved for documentation.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# server adjustments to wg0.conf&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Peer]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# change each peer to have a specific &amp;#34;allowed IP&amp;#34; from your block of addresses&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;AllowedIPs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;8.8.8.1/32  # Replace this documentation address with one from your advertised range.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On the client, make the &lt;code&gt;Interface&lt;/code&gt; address match the address defined for that peer on the server:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# client adjustments to wg0.conf&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Interface]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Address&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;8.8.8.1/32 # must match the &amp;#34;AllowedIPs&amp;#34; configuration on the server for this peer&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The WireGuard configuration only assigns an address to a peer. Your server must also be able to receive and send traffic for the range. How that is configured depends on whether you maintain a BGP connection with the provider or use a setup such as OVH&amp;#39;s, where no separate BGP connection is required.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: If you don&amp;#39;t have your own IPv4 range, you can do this with IPv6, which is much easier to get. If you are an EU citizen, I can assist with the process. Send an email to &lt;code&gt;hello at &amp;#39;this domain without the text part&amp;#39;&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Giving yourself IPv6 with WireGuard</title>
      <link>https://text.techknowlogick.com/posts/giving-yourself-ipv6-with-wireguard/</link>
      <pubDate>Sat, 15 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/giving-yourself-ipv6-with-wireguard/</guid>
      <description>Tunnelling IPv6 traffic through a WireGuard server when the local ISP does not support IPv6</description>
      <content:encoded>&lt;p&gt;After many years of asking, my Internet Service Provider still doesn&amp;#39;t support IPv6. The same goes for many other ISPs. IPv4 exhaustion has pushed some ISPs toward &amp;#34;Carrier-Grade NAT&amp;#34;, where many customers share one address. Dedicated IPv4 addresses are also getting more expensive for small websites, and cloud providers have started charging for them. The obvious solution is for ISPs to support IPv6, but that takes work and many customers do not know there is a problem. Keeping the status quo is easier.&lt;/p&gt;&#xA;&lt;p&gt;I work around this by renting a server in a datacentre with an IPv6 address and tunnelling my traffic through it with WireGuard. This is moderately involved and costs money, but it also gives websites another source of IPv6 traffic. Perhaps that will help show that IPv6 is worth implementing.&lt;/p&gt;&#xA;&lt;p&gt;You can do this with any server that has an IPv6 address. I&amp;#39;ll use one from OVH because it does not charge for bandwidth and has a datacentre near me, which limits the latency added by the tunnel. DigitalOcean, Vultr, Linode, and other providers could also work, but check their bandwidth costs.&lt;/p&gt;&#xA;&lt;p&gt;I installed WireGuard on my laptop and server. On Debian or Ubuntu, run &lt;code&gt;apt install wireguard&lt;/code&gt; on both machines.&lt;/p&gt;&#xA;&lt;p&gt;Next, I created a public and private key on both machines with the WireGuard tools:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;mkdir ~/.wg&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; ~/.wg&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;wg genkey &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; tee privatekey &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; wg pubkey &amp;gt; publickey&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates a private key and public key in the &lt;code&gt;~/.wg&lt;/code&gt; directory. Next, configure the tunnel on the client and server.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# server config&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# /etc/wireguard/wg0.conf&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Interface]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Address&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;10.0.0.1/24  # Private IPv4 subnet for the VPN&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Address&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;fd42:42:42::1/64  # Private IPv6 subnet for the VPN&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;PrivateKey&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;lt;private_key_from_server&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;ListenPort&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;51820&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;[Peer] # your laptop&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;PublicKey&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;lt;public_key_from_laptop&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;AllowedIPs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;10.0.0.2/32, fd42:42:42::2/128&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ... you can add many other peers to this file, just make sure to increment their allowed IPs&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# client config&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# /etc/wireguard/wg0.conf&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Interface]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;PrivateKey&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;lt;private_key_from_laptop&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Address&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;10.0.0.2/24, fd42:42:42::2/64 # addresses that matches &amp;#34;AllowedIPs&amp;#34; from server config&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;DNS&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;1.1.1.1, 2606:4700:4700::1111 # your choice of DNS server, these are cloudflare&amp;#39;s but you could choose your own&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;[Peer]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;PublicKey&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;lt;public_key_from_server&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;AllowedIPs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;0.0.0.0/0, ::/0 # this says to send all traffic over the tunnel&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;Endpoint&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;lt;server_public_ip&amp;gt;:51820&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now, make sure the server can route IPv6 traffic. This requires iptables rules and system settings.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# as root run the following commands&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# the next three commands will let your system packet forward for IPv4 and IPv6&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;net.ipv4.ip_forward = 1&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; tee -a /etc/sysctl.conf&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;net.ipv6.conf.all.forwarding = 1&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; tee -a /etc/sysctl.conf&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sysctl -p &lt;span class=&#34;c1&#34;&gt;# this will immediately apply the configuration additions&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# now we need to let iptables to route the traffic appropriately&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# on my machine, my network interface is eno1, but it may be eth0 for you or something else, you can find this using: ip addr&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;iptables -A FORWARD -i wg0 -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;iptables -A FORWARD -o wg0 -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;iptables -t nat -A POSTROUTING -o &amp;lt;YOUR_NETWORK_INTERFACE&amp;gt; -j MASQUERADE&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip6tables -A FORWARD -i wg0 -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip6tables -A FORWARD -o wg0 -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip6tables -t nat -A POSTROUTING -o &amp;lt;YOUR_NETWORK_INTERFACE&amp;gt; -j MASQUERADE&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ensure that your server has the wireguard ports open&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;iptables -A INPUT -p udp --dport &lt;span class=&#34;m&#34;&gt;51820&lt;/span&gt; -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip6tables -A INPUT -p udp --dport &lt;span class=&#34;m&#34;&gt;51820&lt;/span&gt; -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# don&amp;#39;t allow outside traffic to connect back to your machine that you didn&amp;#39;t initiate&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# this is to prevent your machine becoming world accessible&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# if you plan on running a public service, such as a webserver on your laptop, you may need&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# to adjust these&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;iptables -A INPUT -j DROP&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip6tables -A INPUT -j DROP&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;&#xA;&lt;p&gt;Warning: The INPUT DROP rules above will drop all traffic that is not already established or explicitly allowed. If you are connected to this server over SSH, make sure you add a rule to allow SSH traffic &lt;em&gt;before&lt;/em&gt; adding the DROP rules (e.g. &lt;code&gt;iptables -A INPUT -p tcp --dport 22 -j ACCEPT&lt;/code&gt; and the equivalent ip6tables rule). Otherwise, you will lock yourself out.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Note: These iptables rules are not persistent across reboots. To make them survive a reboot, you can either use the &lt;code&gt;iptables-persistent&lt;/code&gt; package (&lt;code&gt;apt install iptables-persistent&lt;/code&gt; and then &lt;code&gt;netfilter-persistent save&lt;/code&gt;), or use WireGuard&amp;#39;s &lt;code&gt;PostUp&lt;/code&gt; and &lt;code&gt;PostDown&lt;/code&gt; directives in &lt;code&gt;wg0.conf&lt;/code&gt; to apply and remove the rules when the tunnel starts and stops.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Both machines are now ready. Run &lt;code&gt;wg-quick up wg0&lt;/code&gt; on the server and then on the laptop. Run &lt;code&gt;wg show&lt;/code&gt; on both machines to check the tunnel.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: &lt;a href=&#34;https://tailscale.com&#34; rel=&#34;nofollow&#34;&gt;Tailscale&lt;/a&gt; exit nodes are another way to route the traffic without managing the WireGuard configuration directly.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Instead of Docker-in-Docker, what about Docker-on-Fly?</title>
      <link>https://text.techknowlogick.com/posts/docker-in-fly/</link>
      <pubDate>Fri, 07 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/docker-in-fly/</guid>
      <description>Running the docker daemon on a &#34;serverless&#34; hosting platform</description>
      <content:encoded>&lt;p&gt;In my ever-ending quest to do things &lt;del&gt;that are probably not the best idea&lt;/del&gt; that aren&amp;#39;t a part of a usual use-case, I wanted to see if I could run a docker daemon on Fly.io.&lt;/p&gt;&#xA;&lt;p&gt;Fly.io uses Firecracker micro-VMs to run services. It &lt;a href=&#34;https://fly.io/blog/docker-without-docker/&#34; rel=&#34;nofollow&#34;&gt;&amp;#34;transmogrifies&amp;#34;&lt;/a&gt; Docker images for those micro-VMs. This avoids running the Docker daemon inside another Docker container.&lt;/p&gt;&#xA;&lt;p&gt;After creating a whole Docker image with the Docker daemon and getting it working on Fly, I found that Fly had already made one. Their version is much more succinct than mine, so this post uses it instead. I suspect mine will never see the light of day.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# first clone the repo&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;git clone https://github.com/fly-apps/docker-daemon.git docker-on-fly&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# then enter the directory&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; docker-on-fly&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# --build-only because we need to create a volume&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly launch --build-only&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# creating a volume with 50gb of storage&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly volumes create data --size &lt;span class=&#34;m&#34;&gt;50&lt;/span&gt; --region ams&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# run the service&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly deploy&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ssh into the machine (this example runs a container)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly ssh console -C &lt;span class=&#34;s2&#34;&gt;&amp;#34;docker run --rm hello-world&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can now do some interesting things on there such as running entire services with docker compose. Instead of translating a &lt;code&gt;docker-compose.yml&lt;/code&gt; file into &lt;code&gt;fly.toml&lt;/code&gt;, the service can use the existing Compose file.&lt;/p&gt;&#xA;&lt;p&gt;Using Compose avoids translating the service definition, but it replaces that work with a Docker daemon, a persistent volume, and a network path to maintain. A next step would be a WireGuard tunnel that lets the local Docker client connect to the daemon.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Adding SFTP abilities to Namespace.so&#39;s ephemeral instances</title>
      <link>https://text.techknowlogick.com/posts/sftp-files-to-namespace-cluster/</link>
      <pubDate>Thu, 06 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/sftp-files-to-namespace-cluster/</guid>
      <description>Namespace doesn&#39;t support SFTP to instances (right now), so how can you work around this?</description>
      <content:encoded>&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Update (February 2026):&lt;/strong&gt; Namespace has since added native file upload support to their CLI. You can now use &lt;a href=&#34;https://namespace.so/docs/reference/cli/instance-upload&#34; rel=&#34;nofollow&#34;&gt;&lt;code&gt;nsc instance upload&lt;/code&gt;&lt;/a&gt; to copy files directly to an instance, making the shell scripts in this post unnecessary. This post is kept for historical purposes.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Namespace.so provides ephemeral Linux and macOS instances, primarily for CI/CD workflows. Like Fly.io, it lets me avoid managing servers.&lt;/p&gt;&#xA;&lt;p&gt;I use it for some of my CI/CD workflows and wanted to try it for other tasks. One idea is an on-demand nixpkgs builder, similar to my Fly.io setup. With Fly.io I manage the instances myself, while Namespace terminates them after a set period. That reduces the monthly hosting spend.&lt;/p&gt;&#xA;&lt;p&gt;At the time I wrote this, Namespace did not support uploading files directly to instances. I wanted something I could use immediately, which turned into a small side project.&lt;/p&gt;&#xA;&lt;p&gt;I asked in the Namespace Discord and was pointed to &lt;a href=&#34;https://github.com/schollz/croc&#34; rel=&#34;nofollow&#34;&gt;croc&lt;/a&gt;, which others had used. I wanted an option that did not require another tool on the remote server as well as locally. There are plenty of other ways to move the files, but I like making little Bash tools for myself, and this was a good excuse to play around.&lt;/p&gt;&#xA;&lt;p&gt;Unlike Fly.io, Namespace does not provide direct SSH access, so I needed a way to send files through its existing command interface. The files might be configuration, binaries, or other inputs for whatever I was running.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;nsc&lt;/code&gt; client supports pseudo-shell sessions and arbitrary commands. My first pass copied a small file with &lt;code&gt;nsc ssh $machine_id &amp;#39;echo &amp;#34;hi&amp;#34; &amp;gt; /root/test.txt&amp;#39;&lt;/code&gt;, but that does not work well for binary files or large text files. Terminal length limits also meant I had to split files into chunks and reassemble them on the remote machine. Each chunk establishes a new connection, so the transfer is slower than a direct copy. Parallelizing those connections can reduce the transfer time, but that was outside the initial scope.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# help text&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;usage&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Usage: &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$0&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; -l &amp;lt;local_file&amp;gt; -r &amp;lt;remote_file&amp;gt; -m &amp;lt;machine_id&amp;gt; [-c &amp;lt;chunk_size&amp;gt;]&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;This Script uses the Namespace Client &amp;#39;nsc&amp;#39; to copy files to your instance&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Note: You&amp;#39;ll need to ensure you&amp;#39;ve logged in with &amp;#39;nsc login&amp;#39; first&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Options:&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;  -l &amp;lt;local_file&amp;gt;   Path to the local file to be transferred&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;  -r &amp;lt;remote_file&amp;gt;  Path to the remote file to be created&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;  -m &amp;lt;machine_id&amp;gt;   Machine ID for the nsc ssh command&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;  -c &amp;lt;chunk_size&amp;gt;   Size of the chunks for splitting the base64 encoded file (default: 1k)&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;  -h, --help        Show this help message and exit&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;chunk_size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;1k&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# default chunk size&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# parse args&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;while&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;getopts&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;:l:r:m:c:h&amp;#34;&lt;/span&gt; opt&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;opt&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt; in&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        l &lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nv&#34;&gt;local_file&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$OPTARG&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        r &lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nv&#34;&gt;remote_file&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$OPTARG&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        m &lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nv&#34;&gt;machine_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$OPTARG&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        c &lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nv&#34;&gt;chunk_size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$OPTARG&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        h &lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            usage&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;se&#34;&gt;\?&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            usage&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;esac&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;done&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# check for (full) help flag&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; arg in &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$@&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$arg&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;--help&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        usage&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;done&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# make sure all args are set&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; -z &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;local_file&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; -z &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;remote_file&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; -z &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;machine_id&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    usage&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#  base64 encode file and split into chunks&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cat &lt;span class=&#34;nv&#34;&gt;$local_file&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; base64 &amp;gt; /tmp/local_file.b64&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;split -l &lt;span class=&#34;nv&#34;&gt;$chunk_size&lt;/span&gt; /tmp/local_file.b64 /tmp/chunk_&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# init remote file&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nsc ssh &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;echo -n &amp;#39;&amp;#39; &amp;gt; /tmp/remote_file.b64&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# FIXME: transfer chunks individually, then reassemble them on server (this would allow for parallelization)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# loop over chunks and send each one&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; chunk in /tmp/chunk_*&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;do&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;chunk_content&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;cat &lt;span class=&#34;nv&#34;&gt;$chunk&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    nsc ssh &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;echo -n &amp;#39;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$chunk_content&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#39; &amp;gt;&amp;gt; /tmp/remote_file.b64&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;done&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# decode base64 file&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nsc ssh &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;base64 -d /tmp/remote_file.b64 &amp;gt; &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$remote_file&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# clean up local temp files&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rm /tmp/local_file.b64 /tmp/chunk_*&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;File transferred successfully.&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Run it like this:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# ensure you are logged into namespace.so&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nsc login&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# create a new ephemeral instance (4 cores, 8gb ram)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;machine_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;nsc create --machine_type 4x8 --bare --output json &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; jq -r .cluster_id&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# copy file up to server (assuming you have chmod +x the script already)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;./transfer_file.sh -l /home/tklk/Photos/nyan_cat.gif -r /root/nyan.gif -m &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Another option is to install Tailscale in the ephemeral instance and use &lt;code&gt;tailscale file cp&lt;/code&gt;. I left that for another day.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;After writing this post, I added parallelization with GNU &lt;code&gt;parallel&lt;/code&gt; to send the chunks to the remote machine. This reduced the transfer time. You need &lt;code&gt;parallel&lt;/code&gt; installed on the local machine because several common operating systems do not include it by default. Replace the transfer section of the script above with this version:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nsc ssh &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;mkdir -p /tmp/chunks &amp;amp;&amp;amp; echo -n &amp;#39;&amp;#39; &amp;gt; /tmp/remote_file.b64&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;send_chunk&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;chunk&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;chunk_name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;basename &lt;span class=&#34;nv&#34;&gt;$chunk&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;chunk_content&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;cat &lt;span class=&#34;nv&#34;&gt;$chunk&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    nsc ssh &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;echo -n &amp;#39;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$chunk_content&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#39; &amp;gt; /tmp/chunks/&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$chunk_name&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; -f send_chunk&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; machine_id&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# use gnu parallel to transfer files&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# FIXME: accept -j as an argument to be able to adjust the hardcoded number&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;#        left as an exercise for the reader&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;find /tmp/chunk_* &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; parallel -j &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt; send_chunk&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# reassemble chunks&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nsc ssh &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;cat /tmp/chunks/* &amp;gt; /tmp/remote_file.b64 &amp;amp;&amp;amp; rm -r /tmp/chunks&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# based64 decode and write to destination path&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nsc ssh &lt;span class=&#34;nv&#34;&gt;$machine_id&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;base64 -d /tmp/remote_file.b64 &amp;gt; &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$remote_file&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; &amp;amp;&amp;amp; rm /tmp/remote_file.b64&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded>
    </item>
    <item>
      <title>Build your own CDN - Part 3: Allowing bring-your-own domains with automatic TLS certificates</title>
      <link>https://text.techknowlogick.com/posts/byoc-pt3-customer-defined-domain-name/</link>
      <pubDate>Wed, 05 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/byoc-pt3-customer-defined-domain-name/</guid>
      <description>Validating customer domains before Caddy requests their TLS certificates</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://text.techknowlogick.com/posts/byoc-pt1-distrib-tls-to-pops/&#34;&gt;Part 1&lt;/a&gt; synchronized TLS certificates across the PoPs, and &lt;a href=&#34;https://text.techknowlogick.com/posts/byoc-pt2-use-nomad-to-distrib-caddy-config/&#34;&gt;Part 2&lt;/a&gt; distributed Caddy configuration with Nomad. Caddy still needs to request certificates only for names approved by Gitea Pages. Customer domains are not known in advance, so this requires a validation service. The same pattern applies to any SaaS that lets customers bring their own domain.&lt;/p&gt;&#xA;&lt;p&gt;Caddy&amp;#39;s on-demand TLS configuration accepts an &lt;code&gt;ask&lt;/code&gt; endpoint. Before requesting a certificate, Caddy sends a GET request with the domain name to that endpoint. A 2xx response approves the request, while any other status code denies it.&lt;/p&gt;&#xA;&lt;p&gt;The check protects the Let&amp;#39;s Encrypt quota from requests for domains outside the application&amp;#39;s control. It also prevents someone from pointing an unapproved domain at the infrastructure and receiving a valid TLS certificate.&lt;/p&gt;&#xA;&lt;p&gt;The Nomad template from Part 2 can add a customer&amp;#39;s domain to the Caddy configuration. Adding the name is not enough to establish that the customer controls it. The validation service must make that decision before approving Caddy&amp;#39;s request.&lt;/p&gt;&#xA;&lt;p&gt;The Caddy side of the configuration is small:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;{&#xA;    on_demand_tls {&#xA;        ask https://validation.service.tld/endpoint&#xA;    }&#xA;}&#xA;&#xA;example.com {&#xA;    tls {&#xA;        on_demand&#xA;    }&#xA;    respond &amp;#34;Hello World!&amp;#34;&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For this configuration, Caddy calls &lt;code&gt;https://validation.service.tld/endpoint&lt;/code&gt; with a query string such as &lt;code&gt;?domain=example.com&lt;/code&gt;. The initial request waits while Caddy checks the domain and obtains a certificate, so the validation endpoint should respond quickly.&lt;/p&gt;&#xA;&lt;p&gt;The service can be a small Go application that reads a file of approved domains or a database lookup inside a larger application. If approval depends on external checks, such as confirming that the domain points to the load balancer, those checks can run asynchronously and cache the result in the application&amp;#39;s database instead of running for every Caddy request.&lt;/p&gt;&#xA;&lt;p&gt;The validation handler depends on where the application stores customer domains and which checks it performs, so there is no single implementation to show here. It can be a separate HTTP service or part of an application that already handles customer and billing data. Caddy only depends on the HTTP response, not the implementation language. An endpoint that returns 200 for every name would remove the protection that the &lt;code&gt;ask&lt;/code&gt; check is meant to provide.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Build your own CDN - Part 2: Using Nomad to manage Caddy</title>
      <link>https://text.techknowlogick.com/posts/byoc-pt2-use-nomad-to-distrib-caddy-config/</link>
      <pubDate>Fri, 17 May 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/byoc-pt2-use-nomad-to-distrib-caddy-config/</guid>
      <description>Using Nomad to distribute generated Caddy configuration across CDN nodes</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://text.techknowlogick.com/posts/byoc-pt1-distrib-tls-to-pops/&#34;&gt;Part 1&lt;/a&gt; covered provisioning TLS certificates and keeping them synchronized across Caddy instances. The next problem I faced was creating Caddy configuration from the services running at each Point of Presence (PoP) and distributing it to the edge nodes. I used HashiCorp Nomad for that.&lt;/p&gt;&#xA;&lt;p&gt;Like Kubernetes, Nomad schedules containers and non-containerized applications across servers from a shared configuration.&lt;/p&gt;&#xA;&lt;p&gt;Nomad manages the servers across the CDN&amp;#39;s datacentres. The job below runs Caddy on each edge node. Later templates turn Nomad service metadata into Caddy reverse proxy configuration.&lt;/p&gt;&#xA;&lt;p&gt;The Caddy job specification starts with the edge nodes:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;job&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;caddy&amp;#34;&lt;/span&gt; {&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;  # with a CDN, you&amp;#39;ll want datacentres close to the people browsing your data&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;  # you can define as many PoPs here as you wish&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;  datacenters&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;dc1&amp;#34;, &amp;#34;dc2&amp;#34;, &amp;#34;dc3&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;constraint&lt;/span&gt; {&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # this constraint will ensure that the job will only be run on nodes that have&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # a certain hostname, in this case only those that are loadbalancers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;    attribute&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;${attr.unique.hostname}&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;    value&lt;/span&gt;     &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;edge-lb-.+&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;    operator&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;regexp&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;group&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;loadbalancer&amp;#34;&lt;/span&gt; {&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # by default count is 1, meaning only one container will be created&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # however you can use nomad&amp;#39;s autoscaler to dynamically change this&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # otherwise if you know the number of nodes in advance, and they will&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # remain fixed, you can hardcode as you wish&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # count = 1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;constraint&lt;/span&gt; {&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;      # ensure that a Caddy servers in the group are scheduled on separate physical hosts&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      operator&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;distinct_hosts&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;server&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      driver&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;docker&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;config&lt;/span&gt; {&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # we&amp;#39;ll use the official caddy image, but if you want to use the S3 cert sharing from&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # the previous post, you&amp;#39;ll need to build your own image with the caddy plugin included&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        image&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;caddy:2&amp;#34;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # to allow Caddy to bind directly to any port for the host, instead of using docker port forwarding&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # you can give caddy access to the host namespace&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        network_mode&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;host&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The job creates a Caddy instance on every node whose hostname matches &lt;code&gt;edge-lb-.+&lt;/code&gt;, which supports multiple load balancers in each datacentre. At this point Caddy still serves its default landing page. It does not proxy application requests or request TLS certificates.&lt;/p&gt;&#xA;&lt;p&gt;Serving applications and TLS certificates requires a Caddyfile template in the Nomad job.&lt;/p&gt;&#xA;&lt;p&gt;Nomad renders template stanzas into files mounted in the container:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;server&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;config&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # mount in the generated caddy configuration as a ready only volume&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;    # the path used, is the path to the generated files from the below template stanzas&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;mount&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        type&lt;/span&gt;     &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;bind&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        source&lt;/span&gt;   &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;..${NOMAD_ALLOC_DIR}/../server/caddy&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        target&lt;/span&gt;   &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/etc/caddy&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        readonly&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;template&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;EOH&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# caddy configuration goes here&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# this is hardcoded configuration that responds with the hostname of the node that is handling the response&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;80&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;respond&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Hello World from {{ env &amp;#34;node.unique.id&amp;#34; }}!&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;EOH&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # where to write out the configuration&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        destination&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;caddy/Caddyfile&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nomad templates can use variables and service data. The next template generates Caddy configuration from endpoints for services managed by Nomad:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;Caddyfile&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;template&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;{{&lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;service&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;nomad-example-app&amp;#34;&lt;/span&gt;}}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;{{&lt;span class=&#34;k&#34;&gt;index&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ServiceMeta&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;domain&amp;#34;&lt;/span&gt;}} {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;tls&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;on_demand&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;header&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;X&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;Balance&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;{{ env &amp;#34;node.unique.id&amp;#34; }}&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;reverse_proxy&lt;/span&gt; {{&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NodeAddress&lt;/span&gt;}}&lt;span class=&#34;err&#34;&gt;:&lt;/span&gt;{{&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;Port&lt;/span&gt;}}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;{{&lt;span class=&#34;k&#34;&gt;end&lt;/span&gt;}}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The template reads each &lt;code&gt;nomad-example-app&lt;/code&gt; service, uses its &lt;code&gt;domain&lt;/code&gt; metadata for the Caddy site address, and proxies traffic to that service&amp;#39;s node address and port. These connections use the network attached to the Docker bridge. If the published IPs are public, firewall rules need to prevent requests from bypassing Caddy.&lt;/p&gt;&#xA;&lt;p&gt;The application job also needs a port for the container. In this example, Nomad maps container port 80 to a random high port on the host. Its service metadata includes the domain that Nomad uses for the generated Caddy block.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;group&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;nomad-example-app&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;network&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      port &amp;#34;http&amp;#34;  { to&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;80&lt;/span&gt; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      mode&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;bridge&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;service&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;nomad-example-app&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      port&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;http&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;meta&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        domain&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;nomad-app.example.com&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;server&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;      driver&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;docker&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;k&#34;&gt;config&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        image&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;traefik/whoami:latest&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;        ports&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;http&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nomad updates the Caddy configuration as services appear or disappear.&lt;/p&gt;&#xA;&lt;p&gt;By default, each configuration change restarts the Caddy container. A full restart can be undesirable for a production load balancer. Setting &lt;code&gt;change_mode = &amp;#34;signal&amp;#34;&lt;/code&gt; and &lt;code&gt;change_signal = &amp;#34;SIGUSR1&amp;#34;&lt;/code&gt; on the template stanza sends a signal instead. Caddy handles &lt;code&gt;SIGUSR1&lt;/code&gt; with a graceful configuration reload, which avoids the downtime of a full container restart.&lt;/p&gt;&#xA;&lt;p&gt;Possible extensions include health checks, services created through GitOps or the Nomad API, and Consul Connect for mTLS between Caddy and the application.&lt;/p&gt;&#xA;&lt;p&gt;For Gitea Pages, I plan to run one container for each static site in every region and create a Nomad service for each site. Each Caddy instance would proxy to the site container in its own region, while the sites remain loosely coupled. When a new static-site container is published, Nomad would update the services globally.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://text.techknowlogick.com/posts/byoc-pt3-customer-defined-domain-name/&#34;&gt;Part 3&lt;/a&gt; covers Caddy&amp;#39;s on-demand certificates for customer-provided domains and the validation endpoint that controls those requests.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Using Fly.io as a remote builder for nixpkgs</title>
      <link>https://text.techknowlogick.com/posts/using-fly-as-a-remote-nix-builder/</link>
      <pubDate>Mon, 13 May 2024 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/using-fly-as-a-remote-nix-builder/</guid>
      <description>Offload building of your local nixpkgs to a remote builder on Fly.io</description>
      <content:encoded>&lt;p&gt;A service I enjoy using, and write about often, is fly.io, as it lets me have compute on demand, without having to worry about provisioning and managing an entire server.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: This was a fun experiment, but you are likely better off using a managed service such as &lt;a href=&#34;https://nixbuild.net/&#34; rel=&#34;nofollow&#34;&gt;nixbuild.net&lt;/a&gt; for anything even resembling a production use case.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;One of my latest use cases for it is offloading building various nixpkgs from my low-powered laptop to a remote Fly.io builder. I can give the builder more resources for the period when I need them.&lt;/p&gt;&#xA;&lt;p&gt;Start by creating the builder machine. The default has 256 MB of RAM, while this example requests 2 GB and four CPUs. Adjust those values for the package you are building. Fly machines can be created from container images, so this setup uses the same &lt;code&gt;nixos/nix&lt;/code&gt; image as Docker.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly launch --name tklk-nixbuilder --region ams -o personal --build-only --image nixos/nix --no-public-ips  --vm-cpus &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt; --vm-memory &lt;span class=&#34;m&#34;&gt;2048&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;--build-only&lt;/code&gt; flag keeps Fly from starting the machine before the remaining configuration is in place.&lt;/p&gt;&#xA;&lt;p&gt;The builder needs additional storage because building a package can pull in more of its dependency graph. Create a volume for the Nix store:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly volumes create nix_store -a tklk-nixbuilder -s &lt;span class=&#34;m&#34;&gt;50&lt;/span&gt; -r ams -y&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This example uses a 50 GB volume. Fly bills for the storage regardless of whether the machine is powered on, so choose the size based on the expected store. Set the mount path in &lt;code&gt;fly.toml&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mounts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;source&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;nix_store&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;destination&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;/data/nix&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Do not mount the empty volume at &lt;code&gt;/nix&lt;/code&gt; yet. It would hide the existing Nix store, leaving the machine without the binaries it needs. Mount it at &lt;code&gt;/data/nix&lt;/code&gt; first, copy the existing store, and then change the destination to &lt;code&gt;/nix&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Some internal Fly.io processes look for the &lt;code&gt;/bin/sleep&lt;/code&gt; binary. NixOS stores binaries under hashed &lt;code&gt;/nix/store&lt;/code&gt; paths instead of &lt;code&gt;/bin&lt;/code&gt;, so that path does not exist. Either create a symlink or set the command in the configuration. This example sets the command explicitly:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;experimental&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;cmd&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;nix-shell&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;-p&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;coreutils&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;--run&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;sleep inf&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Deploy the machine after the configuration is in place:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly deploy -a tklk-nixbuilder&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After the machine starts, SSH into it to finish the setup:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly ssh console -a tklk-nixbuilder&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;First, copy the contents of the nix store into the mounted volume.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cp -a /nix/. /data/nix/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Back on your host machine, change the &lt;code&gt;destination&lt;/code&gt; path of the mounted volume in &lt;code&gt;fly.toml&lt;/code&gt; to &lt;code&gt;/nix&lt;/code&gt;. Then restart the machine with &lt;code&gt;fly deploy -a tklk-nixbuilder&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Generate an SSH key for the remote connection with &lt;code&gt;fly ssh issue&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly ssh issue -o personal -u root /home/tklk/.ssh/fly_key&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Add the following entry to &lt;code&gt;~/.ssh/config&lt;/code&gt; to avoid repeating the connection options:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Host nixbuilder.fly&#xA;&#x9;StrictHostKeyChecking no&#xA;&#x9;UserKnownHostsFile=/dev/null&#xA;&#x9;Port 2200&#xA;&#x9;User root&#xA;&#x9;HostName localhost&#xA;&#x9;IdentityFile ~/.ssh/fly_key&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: This SSH entry disables host key checking for &lt;code&gt;nixbuilder.fly&lt;/code&gt;. That avoids prompts for the local proxy endpoint, but it also removes SSH host verification for this entry.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Start a local proxy from port 2200 to port 22 on the remote builder. This avoids configuring a WireGuard VPN connection to Fly.io, which is more involved than this port forward. The trailing &lt;code&gt;&amp;amp;&lt;/code&gt; runs the proxy in the background. If the terminal closes, the proxy stops and must be restarted before reconnecting.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;fly proxy 2200:22 &lt;span class=&#34;p&#34;&gt;&amp;amp;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With the proxy running, test the remote builder by adding &lt;code&gt;--store ssh-ng://nixbuilder.fly&lt;/code&gt; to a &lt;code&gt;nix build&lt;/code&gt; command.&lt;/p&gt;&#xA;&lt;p&gt;Example:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nix build nixpkgs#mercurial --store ssh-ng://nixbuilder.fly --show-trace&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The Fly builder uses x86-64 so if your local machine is aarch64, pass &lt;code&gt;--system x86_64-linux&lt;/code&gt; to &lt;code&gt;nix-build&lt;/code&gt; so the remote builder can build the package. You can copy the x86-64 output back, but it will not run natively on the Arm machine. For aarch64 builds, &lt;a href=&#34;https://nixbuild.net/&#34; rel=&#34;nofollow&#34;&gt;nixbuild.net&lt;/a&gt; supports aarch64 builders, though not macOS builders as of this writing.&lt;/p&gt;&#xA;&lt;p&gt;Further reading:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://nixos.wiki/wiki/Distributed_build&#34; rel=&#34;nofollow&#34;&gt;https://nixos.wiki/wiki/Distributed_build&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://gist.github.com/danbst/09c3f6cd235ae11ccd03215d4542f7e7&#34; rel=&#34;nofollow&#34;&gt;https://gist.github.com/danbst/09c3f6cd235ae11ccd03215d4542f7e7&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Build your own CDN - Part 1: Sync TLS certificates across your PoPs with Caddy</title>
      <link>https://text.techknowlogick.com/posts/byoc-pt1-distrib-tls-to-pops/</link>
      <pubDate>Tue, 31 Oct 2023 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/byoc-pt1-distrib-tls-to-pops/</guid>
      <description>Synchronizing TLS certificates across Points of Presence using Caddy and an S3 bucket</description>
      <content:encoded>&lt;p&gt;Gitea Pages will need many unique TLS certificates, and from past experience I know that a single Amazon CloudFront distribution limits how many certificates it can hold. I am building a Content Delivery Network (CDN) from scratch to be able to handle that constraint.&lt;/p&gt;&#xA;&lt;p&gt;The first task is keeping TLS certificates synchronized across the CDN&amp;#39;s Points of Presence (PoPs), because the servers spread across the globe close to be able to serve the end users in a locatin close to them. I chose Caddy because it has built-in Let&amp;#39;s Encrypt/ACME support and can obtain and renew TLS certificates automatically.&lt;/p&gt;&#xA;&lt;p&gt;Caddy lets me use a plugin I wrote, &lt;a href=&#34;https://github.com/techknowlogick/certmagic-s3&#34; rel=&#34;nofollow&#34;&gt;certmagic-s3&lt;/a&gt;, so each instance can share certificates through an S3 bucket. Any PoP can obtain or renew a certificate, and the other PoPs can retrieve the update from the bucket.&lt;/p&gt;&#xA;&lt;p&gt;This removes the central certificate distributor, but it makes the S3 bucket and custom Caddy binary part of the setup. Each PoP needs credentials for the bucket, and each Caddy upgrade needs another binary that includes the plugin. You can build Caddy with &lt;code&gt;xcaddy&lt;/code&gt;, or use Caddy&amp;#39;s &lt;a href=&#34;https://caddyserver.com/download&#34; rel=&#34;nofollow&#34;&gt;build service&lt;/a&gt; to download a compiled binary. In the build service, select certmagic-s3, choose a platform, and download the resulting binary.&lt;/p&gt;&#xA;&lt;p&gt;That binary can load the plugin from a Caddyfile and obtain TLS certificates from Let&amp;#39;s Encrypt:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;webmaster@example.com&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # The email associated with your Let&amp;#39;s Encrypt account&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;storage&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;s3&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Configuring S3 as the storage backend&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;host&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;minio.example.com&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Your S3-compatible storage host&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;bucket&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;certmagic-s3&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Bucket where certificates will be stored&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;access_key&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;ABC123&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Your S3 access key&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;secret_key&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;XYZ789&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Your S3 secret key&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;prefix&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;byoc&amp;#34;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Optional path prefix within the bucket&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gh&#34;&gt;site.example.com&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Domain to serve&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;tls&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;on_demand&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Obtain TLS certificates on first HTTP request instead of on start&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # Note: you should also configure an `ask` endpoint in the global `on_demand_tls`&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # block to prevent abuse of on-demand certificate issuance. See Part 3 of this&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;        # series for details on setting up a validation service.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;respond&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;hello world&amp;#34;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;  # Sample response&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After configuring and starting Caddy, the first request for the site will cause Caddy to obtain the certificate from Let&amp;#39;s Encrypt and store it in the S3 bucket. A second Caddy instance with the same storage configuration can retrieve the certificate from the bucket and serve the site.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://text.techknowlogick.com/posts/byoc-pt2-use-nomad-to-distrib-caddy-config/&#34;&gt;Part 2&lt;/a&gt; covers distributing this Caddy configuration across the PoPs with Nomad. Because this configuration uses &lt;code&gt;on_demand&lt;/code&gt;, the deployment also needs an &lt;code&gt;ask&lt;/code&gt; endpoint so unapproved domains cannot consume certificate issuance. &lt;a href=&#34;https://text.techknowlogick.com/posts/byoc-pt3-customer-defined-domain-name/&#34;&gt;Part 3&lt;/a&gt; covers that validation step.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Using Nix with Gitea Actions</title>
      <link>https://text.techknowlogick.com/posts/use-nix-with-gitea-actions/</link>
      <pubDate>Sun, 03 Sep 2023 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/use-nix-with-gitea-actions/</guid>
      <description>Using Nix to provide a reproducible build environment inside Gitea Actions</description>
      <content:encoded>&lt;p&gt;Carl Sagan once said, &amp;#34;If you wish to make an apple pie from scratch, you must first invent the universe.&amp;#34; A reproducible build does not need to go quite that far. Nix can provide the build environment inside Gitea Actions with a small addition to the workflow.&lt;/p&gt;&#xA;&lt;p&gt;I use Nix and maintain &lt;a href=&#34;https://github.com/NixOS/nixpkgs/commits?author=techknowlogick&#34; rel=&#34;nofollow&#34;&gt;several packages&lt;/a&gt;. I appreciate the reproducible build environments it provides and how quickly packages are updated.&lt;/p&gt;&#xA;&lt;p&gt;The workflow below installs Nix in a Gitea Actions job and runs &lt;code&gt;hello&lt;/code&gt; from &lt;code&gt;nix-shell&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;nix&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;push&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;jobs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;lint-backend&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;runs-on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ubuntu-latest&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;actions/checkout@v4&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Install dependencies for Nix setup action&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;          apt update -y&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;          apt install sudo -y          &lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;cachix/install-nix-action@v27&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;nix_path&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;nixpkgs=channel:nixos-unstable&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Test running command with Nix&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;nix-shell -p hello --run &amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The default Gitea Actions runner image does not include &lt;code&gt;sudo&lt;/code&gt;, which &lt;code&gt;cachix/install-nix-action&lt;/code&gt; requires, so the workflow installs it first. A custom runner that already includes &lt;code&gt;sudo&lt;/code&gt; can skip that step.&lt;/p&gt;&#xA;&lt;p&gt;I prefer to live on the bleeding edge with &lt;code&gt;nixpkgs=channel:nixos-unstable&lt;/code&gt;, but you can pin a more stable channel instead. The &lt;code&gt;cachix/install-nix-action&lt;/code&gt; Action does not configure a channel by default, so the workflow must specify one.&lt;/p&gt;&#xA;&lt;p&gt;Installing Nix adds setup time and requires &lt;code&gt;sudo&lt;/code&gt; on this runner image. In return, the job can use Nix package definitions instead of reproducing the environment with runner-specific installation commands.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: Using DetSys&amp;#39; Nix installer action can speed up the install of nix in your workflows, and also reduce the customization of the runner needed.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Watchtower... but for Kubernetes!?!</title>
      <link>https://text.techknowlogick.com/posts/watchtower-but-for-kubernetes/</link>
      <pubDate>Thu, 18 May 2023 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/watchtower-but-for-kubernetes/</guid>
      <description>Using Keel to update Kubernetes workloads when container images change</description>
      <content:encoded>&lt;p&gt;Watchtower runs on a schedule and checks for newer Docker container images. When it finds one, it pulls the image and recreates the container. Keel applies the same approach to Kubernetes workloads, including Helm releases, DaemonSets, StatefulSets, and Deployments. It also provides a UI for viewing the updates it manages.&lt;/p&gt;&#xA;&lt;h2 id=&#34;installing-keel&#34;&gt;Installing Keel&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#installing-keel&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Install Keel in the cluster with &lt;code&gt;kubectl apply&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;KEEL_NAMESPACE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;keel&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;KEEL_ADMIN_USER&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;keel&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;KEEL_ADMIN_PASS&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;keel&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl apply -f https://sunstone.dev/keel?namespace&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$KEEL_NAMESPACE&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\&amp;amp;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$KEEL_ADMIN_USER&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\&amp;amp;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;password&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$KEEL_ADMIN_PASS&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\&amp;amp;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;tag&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;latest&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;configuring-deployments-for-keel&#34;&gt;Configuring deployments for Keel&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#configuring-deployments-for-keel&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Add Keel labels to the Kubernetes Deployment specification. Keel uses SemVer (Semantic Versioning), and its policies can be &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;major&lt;/code&gt;, &lt;code&gt;minor&lt;/code&gt;, or &lt;code&gt;patch&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;apiVersion&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;apps/v1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;kind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Deployment&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;my-app&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;labels&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;keel.sh/policy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;major&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nn&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;major&lt;/code&gt; policy tells Keel to update the Deployment when a new major version of the container image is available.&lt;/p&gt;&#xA;&lt;h2 id=&#34;handling-private-images-and-rate-limits&#34;&gt;Handling private images and rate limits&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#handling-private-images-and-rate-limits&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you use private images or Docker Hub with its rate limit, Keel needs registry credentials. Keel can use the image pull secrets already configured in Kubernetes, so no additional secret is required.&lt;/p&gt;&#xA;&lt;h2 id=&#34;keel-ui&#34;&gt;Keel UI&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#keel-ui&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Keel&amp;#39;s UI shows the status of deployments and the updates it manages. Expose it through a Kubernetes ingress or use &lt;code&gt;kubectl port-forward&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl -n keel port-forward service/keel &lt;span class=&#34;m&#34;&gt;9300&lt;/span&gt; &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;update-tradeoffs&#34;&gt;Update tradeoffs&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#update-tradeoffs&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Automatic image updates save time and keep containers on newer images, which may include security fixes. They still require a rollback strategy and test pipeline.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Publishing a static site to Bunny.net using Gitea Actions</title>
      <link>https://text.techknowlogick.com/posts/using-bunny-cdn-to-host-static-sites/</link>
      <pubDate>Tue, 18 Apr 2023 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/using-bunny-cdn-to-host-static-sites/</guid>
      <description>Publish a Hugo-powered static site to Bunny.net using Gitea Actions.</description>
      <content:encoded>&lt;p&gt;Bunny.net (formerly BunnyCDN) can host a static site by serving files from a storage zone through a pull zone. The workflow below uses Gitea Actions to build a Hugo site, upload it over FTP, and purge the CDN cache after each deployment.&lt;/p&gt;&#xA;&lt;h2 id=&#34;creating-a-storage-zone&#34;&gt;Creating a storage zone&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#creating-a-storage-zone&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Create a storage zone in the Bunny.net dashboard and give it a name that you can associate with the site. Select the regions where Bunny.net should replicate the content. More regions can make the site faster in those locations. They also cost more.&lt;/p&gt;&#xA;&lt;p&gt;Open the storage zone&amp;#39;s FTP credentials page and keep the host, username, and password for the workflow.&lt;/p&gt;&#xA;&lt;h2 id=&#34;creating-a-pull-zone&#34;&gt;Creating a pull zone&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#creating-a-pull-zone&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Create a pull zone in the Bunny.net dashboard, give it a recognizable name, and set the storage zone as its &amp;#34;origin&amp;#34;. Select the regions from which the CDN should serve the site. For a custom domain, add it to the pull zone and create a CNAME record at your DNS provider that points to the Bunny.net pull zone.&lt;/p&gt;&#xA;&lt;h2 id=&#34;uploading-the-site&#34;&gt;Uploading the site&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#uploading-the-site&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;This &lt;a href=&#34;https://blog.gitea.com/hacking-on-gitea-actions/&#34; rel=&#34;nofollow&#34;&gt;Gitea Actions&lt;/a&gt; workflow builds the Hugo site, mirrors the generated &lt;code&gt;public/&lt;/code&gt; directory to the storage zone with its FTP credentials, and purges the pull zone cache. It can be adapted to another CI system.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# .gitea/workflows/hugo-build.yml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Build and Deploy to BunnyCDN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;push&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;branches&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;l&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;jobs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;bunnycdn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;bunnycdn-publish&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;runs-on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ubuntu-latest&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;actions/checkout@v4&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;submodules&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# Fetch Hugo themes (true OR recursive)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;fetch-depth&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c&#34;&gt;# Fetch all history for .GitInfo and .Lastmod&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Setup Hugo&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;uses&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;peaceiris/actions-hugo@v2&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;with&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;hugo-version&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;0.126.1&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;extended&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Build&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;hugo --minify&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Deploy to BunnyCDN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;          apt update -y&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;          apt install -y lftp&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;          lftp -e &amp;#34;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            set ftp:ssl-allow true;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            set ftp:ssl-protect-data true;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            set ssl:verify-certificate no;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            open ${{ secrets.BUNNYCDN_FTP_HOST }};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            user ${{ secrets.BUNNYCDN_FTP_USER }} ${{ secrets.BUNNYCDN_FTP_PASSWORD }};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            mirror -R --delete -v public/ .;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            bye;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;          &amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;purge bunny cache&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;          curl --request POST \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            --url https://api.bunny.net/pullzone/${{ secrets.BUNNYCDN_ZONE_ID }}/purgeCache \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            --header &amp;#39;AccessKey: ${{ secrets.BUNNYCDN_API_KEY }}&amp;#39; \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            --header &amp;#39;content-type: application/json&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Set the &lt;code&gt;BUNNYCDN_FTP_HOST&lt;/code&gt;, &lt;code&gt;BUNNYCDN_FTP_USER&lt;/code&gt;, and &lt;code&gt;BUNNYCDN_FTP_PASSWORD&lt;/code&gt; secrets from the storage zone&amp;#39;s FTP credentials page. Hugo builds the site into &lt;code&gt;public/&lt;/code&gt;, which &lt;code&gt;lftp&lt;/code&gt; recursively mirrors to the root of the storage zone. The &lt;code&gt;--delete&lt;/code&gt; option removes remote files that are no longer in &lt;code&gt;public/&lt;/code&gt;, and &lt;code&gt;-v&lt;/code&gt; prints verbose transfer output.&lt;/p&gt;&#xA;&lt;p&gt;Set &lt;code&gt;BUNNYCDN_ZONE_ID&lt;/code&gt; and &lt;code&gt;BUNNYCDN_API_KEY&lt;/code&gt; secrets in your Action&amp;#39;s settings found on the Bunny.net dashboard. The final workflow step uses them to purge the pull zone cache after the upload.&lt;/p&gt;&#xA;&lt;p&gt;The pull zone URL serves the uploaded site. If you configured a custom domain, it serves the same content. Caching, compression, and auto-HTTPS can be configured on the pull zone.&lt;/p&gt;&#xA;&lt;p&gt;Note: Bunny.net is rebuilding its dashboard, so these steps may not match the current dashboard. The underlying process should remain largely the same.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Secure SSH Access with SSH Certificates Managed by HashiCorp&#39;s Vault</title>
      <link>https://text.techknowlogick.com/posts/use-vault-as-ssh-ca/</link>
      <pubDate>Sun, 16 Apr 2023 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/use-vault-as-ssh-ca/</guid>
      <description>Use Vault&#39;s SSH secrets engine as a certificate authority for SSH access.</description>
      <content:encoded>&lt;p&gt;SSH user certificates can restrict which users may connect and which commands they may run. In the setup below, the server trusts a Certificate Authority (CA) public key and accepts user certificates signed by that CA. This does not replace host key verification on the client. Avoiding per-server host fingerprints requires a separate host certificate setup and client-side trust in the host CA. Vault can issue and revoke certificates, manage SSH keys, and provide audit logs. Vault can also manage host certificates to also prevent that TOFU issue.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Warning: This post describes a non-production Vault setup. It is not hardened and should only be used to learn about SSH CAs and Vault.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;install-vault&#34;&gt;Install Vault&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#install-vault&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;To quickly set up a development Vault server, use the official Docker image with the following command:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker run --cap-add&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;IPC_LOCK -e &lt;span class=&#34;s1&#34;&gt;&amp;#39;VAULT_DEV_ROOT_TOKEN_ID=dev-token&amp;#39;&lt;/span&gt; -e &lt;span class=&#34;s1&#34;&gt;&amp;#39;VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200&amp;#39;&lt;/span&gt; -p 8200:8200 hashicorp/vault server -dev&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;configure-the-ssh-certificate-authority&#34;&gt;Configure the SSH certificate authority&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#configure-the-ssh-certificate-authority&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With Vault installed and running, configure it to issue SSH certificates using the SSH secrets engine.&lt;/p&gt;&#xA;&lt;p&gt;First, set the Vault address and token for the dev server:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;VAULT_ADDR&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;http://127.0.0.1:8200&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;dev-token&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, run the following command to enable the SSH secrets engine&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vault secrets &lt;span class=&#34;nb&#34;&gt;enable&lt;/span&gt; ssh&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, you will need to configure the SSH CA and have it generate signing keys on your behalf to ensure that the private key stays within Vault itself.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vault write ssh/config/ca &lt;span class=&#34;nv&#34;&gt;generate_signing_key&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can get the generated SSH public key for use later, with:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vault &lt;span class=&#34;nb&#34;&gt;read&lt;/span&gt; -field&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;public_key ssh/config/ca &amp;gt; ssh_ca_key.pub&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After enabling the SSH CA in Vault, you&amp;#39;ll need to create a role called &lt;code&gt;ops-team&lt;/code&gt; to issue SSH certificates. This role allows any user with access to request an SSH certificate. The example below grants broad permissions, including any option for &lt;code&gt;allowed_users&lt;/code&gt; and port forwarding. Be sure to restrict these permissions based on your use case.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vault write ssh/roles/ops-team &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;key_type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;ca &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;ttl&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;2h &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;max_ttl&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;24h &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;allow_user_certificates&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;allowed_users&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;default_extensions&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;{&amp;#34;permit-pty&amp;#34;:&amp;#34;&amp;#34;,&amp;#34;permit-port-forwarding&amp;#34;:&amp;#34;&amp;#34;,&amp;#34;permit-agent-forwarding&amp;#34;:&amp;#34;&amp;#34;}&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Configure the remote server to accept the SSH certificates issued by Vault:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Copy the CA public key to the remote server:&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;scp ssh_ca_key.pub &amp;lt;username&amp;gt;@&amp;lt;target_server_ip&amp;gt;:/tmp/ssh_ca_key.pub&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&#34;2&#34;&gt;&#xA;&lt;li&gt;Add the public key to the OpenSSH configuration and restart the OpenSSH daemon:&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;TrustedUserCAKeys /etc/ssh/user_ca.pub&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sudo tee -a /etc/ssh/sshd_config&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo cp /tmp/ssh_ca_key.pub /etc/ssh/user_ca.pub&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sudo systemctl restart sshd&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;request-ssh-certificates&#34;&gt;Request SSH certificates&lt;a aria-label=&#34;Link to this heading&#34; class=&#34;heading-link&#34; href=&#34;#request-ssh-certificates&#34; tabindex=&#34;-1&#34;&gt;&lt;span class=&#34;heading-hash&#34; aria-hidden=&#34;true&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;To request an SSH certificate from Vault and use it to SSH into the remote server, follow these steps:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Use the &lt;code&gt;ops-team&lt;/code&gt; role to request the certificate and pass your local SSH key &lt;code&gt;id_rsa.pub&lt;/code&gt;. Also, specify the username to use when connecting to the remote server:&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vault write -field&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;signed_key ssh/sign/ops-team &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;public_key&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;@&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;/.ssh/id_rsa.pub &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;valid_principals&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;username&amp;gt;&amp;#34;&lt;/span&gt; &amp;gt; signed_id_rsa-cert.pub&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&#34;2&#34;&gt;&#xA;&lt;li&gt;Use the &lt;code&gt;signed_id_rsa-cert.pub&lt;/code&gt; file to SSH into the remote server:&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ssh -i signed_id_rsa-cert.pub -i &lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;/.ssh/id_rsa &amp;lt;username&amp;gt;@&amp;lt;target_server_ip&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Requesting a signed certificate manually each time can be tedious. To simplify this process, create a script called &lt;code&gt;vault-ssh.sh&lt;/code&gt; and make it executable with &lt;code&gt;chmod +x vault-ssh.sh&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Configuration&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;VAULT_ADDR&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;http://&amp;lt;vault_server_ip&amp;gt;:8200&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;VAULT_ROLE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;ops-team&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;USERNAME&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&amp;lt;username&amp;gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;PUBLIC_KEY_PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/.ssh/id_rsa.pub&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;CERT_PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$HOME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;/.ssh/id_rsa-cert.pub&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;CONFIG_FILE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;/path/to/vault-creds.conf&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Read the Vault token from the configuration file&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; -f &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$CONFIG_FILE&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nb&#34;&gt;source&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$CONFIG_FILE&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Error: Vault configuration file not found&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Check if the VAULT_TOKEN variable is set&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; -z &lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$VAULT_TOKEN&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Error: VAULT_TOKEN is not set in the configuration file&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; VAULT_TOKEN&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Generate a new SSH certificate&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;vault write -field&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;signed_key ssh/sign/&lt;span class=&#34;nv&#34;&gt;$VAULT_ROLE&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;public_key&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;@&lt;span class=&#34;nv&#34;&gt;$PUBLIC_KEY_PATH&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;valid_principals&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$USERNAME&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; &amp;gt; &lt;span class=&#34;nv&#34;&gt;$CERT_PATH&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This script requires a &lt;code&gt;vault-creds.conf&lt;/code&gt; file containing the Vault token:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&amp;lt;vault_token&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To integrate the certificate generation process with your SSH config, use the ProxyCommand configuration option, which allows you to run a custom command (like the script) as a &amp;#34;proxy&amp;#34; for the actual SSH connection.&lt;/p&gt;&#xA;&lt;p&gt;Add the following to your SSH config:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Host *&#xA;  IdentityFile ~/.ssh/id_rsa&#xA;  CertificateFile ~/.ssh/id_rsa-cert.pub&#xA;  ProxyCommand bash -c &amp;#39;/path/to/vault-ssh.sh &amp;amp;&amp;amp; nc %h %p&amp;#39;&#xA;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Keep in mind that this approach generates a new SSH certificate for every connection. Depending on the frequency of your connections and the TTL of your certificates, you might want to modify the &lt;code&gt;vault-ssh.sh&lt;/code&gt; script to check the current certificate&amp;#39;s validity and generate a new one only if necessary.&lt;/p&gt;&#xA;&lt;p&gt;Credits: This post draws on my experience using Vault and the official Vault Docker documentation for the development server configuration.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Playing around with Gitea Actions on Fly.io</title>
      <link>https://text.techknowlogick.com/posts/playing-around-with-gitea-actions-on-fly-io/</link>
      <pubDate>Mon, 03 Apr 2023 00:00:00 +0000</pubDate>
      <guid>https://text.techknowlogick.com/posts/playing-around-with-gitea-actions-on-fly-io/</guid>
      <description>A proof of concept for running the Gitea Actions runner on Fly.io</description>
      <content:encoded>&lt;p&gt;Fly.io is a &amp;#34;serverless&amp;#34; hosting platform usually used for web services, but it can also run long-running tasks. I wanted to try running the Gitea Actions runner there.&lt;/p&gt;&#xA;&lt;p&gt;Treat this as a proof of concept. There are better ways to do this, but it still was fun to try.&lt;/p&gt;&#xA;&lt;p&gt;To keep the experiment simple, I ran the runner in &amp;#34;host&amp;#34; mode. Jobs run directly on the host rather than in containers, which kept Docker-in-Docker debugging out of the initial setup.&lt;/p&gt;&#xA;&lt;p&gt;To get started, I created a new Fly.io app with the following configuration:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# fly.toml&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;app&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;actions-on-fly&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;primary_region&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;ams&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mounts&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;destination&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/data&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;source&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I mounted a persistent volume to &lt;code&gt;/data&lt;/code&gt; so the runner registration persists across restarts.&lt;/p&gt;&#xA;&lt;p&gt;Since there were no prebuilt Docker images as of publication, I created one and installed the runner in it. The Dockerfile is as follows:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Dockerfile&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;ghcr.io/catthehacker/ubuntu:act-latest&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# the FROM image is based on ubuntu and has appropriate tools installed to run Gitea Actions&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# install act_runner&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; curl https://dl.gitea.com/act_runner/nightly/act_runner-nightly-linux-amd64 &amp;gt; /usr/local/bin/act_runner &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    chmod +x /usr/local/bin/act_runner&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# add start script&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ADD&lt;/span&gt; start.sh /start.sh&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; chmod +x /start.sh &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; mkdir -p /data&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENTRYPOINT&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;/start.sh&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At startup, the container checks whether the runner is already registered and registers it if needed. The registration token comes from an environment variable, and then the runner starts.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#!/bin/bash&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# start.sh&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# $ACTIONS_REGISTER_TOKEN is the registration token for the runner that is given by the Gitea runner settings page.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# set /data as the working dir&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;cd&lt;/span&gt; /data&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# check if runner is already registered, and if not register it&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt; ! -f .runner &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;then&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;# register runner on gitea.com, and set label as fly-runner so it runs as &amp;#34;host&amp;#34; mode&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  act_runner register --no-interactive --instance &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://gitea.com&amp;#34;&lt;/span&gt; --labels &lt;span class=&#34;s2&#34;&gt;&amp;#34;fly-runner&amp;#34;&lt;/span&gt; --token &lt;span class=&#34;nv&#34;&gt;$ACTIONS_REGISTER_TOKEN&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fi&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# start runner&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;act_runner daemon&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It took only a handful of lines to get the runner working on Fly.io. The issue I ran into was memory: Fly.io terminates apps when they run out of memory. The workload I was testing needed more memory, so I increased the limit. Memory-heavy jobs may therefore need a larger machine than the default configuration.&lt;/p&gt;&#xA;&lt;p&gt;This remains a proof of concept rather than a recommendation. The blog you are reading right now is built using this runner.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s Note: This blog is no longer built using this approach, but still uses Gitea Actions to build and publish. The post is kept as historical reference.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;</content:encoded>
    </item>
  </channel>
</rss>