<?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>Thu, 06 Aug 2026 18:11:16 +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;), it&amp;#39;s just &lt;code&gt;go get tklk.dev/pkg&lt;/code&gt;. Shorter, and decoupled from wherever the code actually lives.&lt;/p&gt;&#xA;&lt;p&gt;After moving everything into a &lt;a href=&#34;/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. I got it working with Bunny.net using nothing but a custom 404 page. No servers, no edge compute, no edge rules. Just a static HTML file.&lt;/p&gt;&#xA;&lt;h2 id=&#34;how-go-vanity-imports-work&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#how-go-vanity-imports-work&#34;&gt;How Go vanity imports work&lt;span class=&#34;heading-hash&#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;the-trick-a-custom-404-page&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-trick-a-custom-404-page&#34;&gt;The trick: a custom 404 page&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;For most cases, the Go toolchain does not 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 entire thing. The &lt;code&gt;go-import&lt;/code&gt; tag tells Go where the repo is.&lt;/p&gt;&#xA;&lt;p&gt;For this specific pullzone of mine, it also serves a few real files, such as my &lt;a href=&#34;/posts/serverless-docker-registry-with-bunnycdn/&#34;&gt;Docker registry&lt;/a&gt; at &lt;code&gt;/v2/&lt;/code&gt;, and a few other things that don&amp;#39;t conflict with package names, so when the custom 404 fires for paths that don&amp;#39;t match anything else it&amp;#39;s still fine.&lt;/p&gt;&#xA;&lt;h2 id=&#34;setting-it-up&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#setting-it-up&#34;&gt;Setting it up&lt;span class=&#34;heading-hash&#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;/posts/using-bunny-cdn-to-host-static-sites/&#34;&gt;previous post&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;To set it up:&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;, or if you wanted it somewhere else, you can specify the path in 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;&lt;a class=&#34;heading-link&#34; href=&#34;#testing-it&#34;&gt;Testing it&lt;span class=&#34;heading-hash&#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;Clean import paths, zero infrastructure to run, and it costs nothing beyond what the CDN already costs. The &lt;a href=&#34;/posts/monorepo/&#34;&gt;monorepo&lt;/a&gt; is what makes the whole setup so simple. If the domain fronted multiple independent repos instead, each import path would need its own meta tag pointing at a different repository, and you&amp;#39;d need edge compute or a server to route them. 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 by generating a set of static HTML files from a TOML config, one per package, each 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. With a monorepo though, none of that is necessary. 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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-inspiration&#34;&gt;The inspiration&lt;span class=&#34;heading-hash&#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 is well-structured, using idiomatic Go project layouts, with a &lt;code&gt;go.mod&lt;/code&gt; at the root, and has repo-wide policies for things like linting, and other helpful items.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-problem&#34;&gt;The problem&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Whenever I have a new idea, I usually just make a temporary/scratch folder for it, since setting up CI, linting, dependency management, etc. 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, then I make a repo for it, and copy the code over. I do this, since I don&amp;#39;t want to have to sign up for maintenance on something that might just be a dead end.&lt;/p&gt;&#xA;&lt;p&gt;Whenever repos were created, they were inconsistent, and sometimes using a shared library meant signing up for potentially having to update multiple different repos that imported it when making a &amp;#34;breaking&amp;#34; change.&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. When you have several repos with overlapping dependencies, &amp;#34;doing its job&amp;#34; means a steady stream of PRs across all of them, especially for repos of low importance. Even with automatic merging on dependency updates, those emails still come in, notifications happen, and hopefully the tests pass. If not, and something needs to be fixed, then I have to decide if I&amp;#39;m going to spend the time to context switch and fix it, or if I&amp;#39;m just going to ignore it and let it rot until I need to use that repo again and notice the problem then. It&amp;#39;s a minor annoyance that adds up. Especially as more and more repos get added.&lt;/p&gt;&#xA;&lt;h2 id=&#34;one-repo-to-rule-them-all&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#one-repo-to-rule-them-all&#34;&gt;One repo to rule them all&lt;span class=&#34;heading-hash&#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 the annoyances I described above just went away.&lt;/p&gt;&#xA;&lt;p&gt;With a monorepo, I have CI set up right out of the gate for any new CLIs, servers, docker images, linting, etc. I don&amp;#39;t have to worry about that for new things, and by having a standard structure, I can just copy and paste from existing projects to get started. The barrier to entry for new experiments is much lower, so I&amp;#39;m more likely to try out new ideas, and have them use a consistent 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;Having the infrastructure already set up means I can start new things without worrying about it. More of my experiments end up in source control, giving me better visibility into them if I want to go back and revisit them later.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-freedom-of-building-for-one&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-freedom-of-building-for-one&#34;&gt;The freedom of building for one&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The monorepo approach doesn&amp;#39;t only work at the scale of one person; some of the largest companies in the world use monorepos for similar reasons. Those companies have the resources to build tooling to manage the complexity of a monorepo at scale, but the core motivation is the same: reduce overhead and friction in maintaining code.&lt;/p&gt;&#xA;&lt;p&gt;This isn&amp;#39;t unique to monorepos, but having a personal one with no expectation of supporting a wide audience gives me the freedom to make choices I wouldn&amp;#39;t make if I were building a library for others to use.&lt;/p&gt;&#xA;&lt;p&gt;This is the opposite of what you&amp;#39;d want for an open source library or a shared codebase at work. In those contexts, the overhead of separate repos with clear versioning and ownership boundaries exists for good reason. But for personal projects, that overhead is solving a problem that doesn&amp;#39;t exist.&lt;/p&gt;&#xA;&lt;h2 id=&#34;give-it-a-try&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#give-it-a-try&#34;&gt;Give it a try&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you are maintaining many small repos, possibly more than just Go, and you find yourself spending more time on repo maintenance than on the code itself, it might be worth trying. 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 things.&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;/posts/migrate-docker-images-to-self-hosted-registry/&#34;&gt;host all of my Docker images using my personal Gitea instance&lt;/a&gt;, but rather than exposing my home network on the internet, I figured I could upload the images to Bunny.net too, and use the CDN there to serve them.&lt;/p&gt;&#xA;&lt;p&gt;Bunny.net has edge compute (think AWS Lambda@Edge), but you end up paying a per-request cost, so I wanted to see if I could do it entirely with edge rules, which are cheaper. Luckily, 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;what-a-docker-registry-actually-serves&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#what-a-docker-registry-actually-serves&#34;&gt;What a Docker registry actually serves&lt;span class=&#34;heading-hash&#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;There&amp;#39;s a wrinkle with manifests. Since I opted not to use edge compute, where I could easily distinguish an index from a manifest, and set headers accordingly, I had to find another way. The solution was to store platform-specific manifests and multi-arch indexes separately, so the CDN can set &lt;code&gt;Content-Type&lt;/code&gt; based on path alone. If a client requests a manifest by digest, the CDN looks for it in the manifest directory first. If it&amp;#39;s not there, it must be an index, so the CDN issues a redirect to the index path. This way the client gets the correct &lt;code&gt;Content-Type&lt;/code&gt; without the CDN having to inspect 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;&lt;a class=&#34;heading-link&#34; href=&#34;#storage-layout&#34;&gt;Storage layout&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-edge-rules&#34;&gt;The edge rules&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Eleven edge rules let me make Bunny.net 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;&lt;a class=&#34;heading-link&#34; href=&#34;#base-endpoint&#34;&gt;Base endpoint&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Three rules handle the &lt;code&gt;/v2/&lt;/code&gt; base path:&lt;/p&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;&lt;a class=&#34;heading-link&#34; href=&#34;#tag-resolution&#34;&gt;Tag resolution&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#blob-requests&#34;&gt;Blob requests&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#manifest-vs-index-dispatch&#34;&gt;Manifest vs index dispatch&lt;span class=&#34;heading-hash&#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 only lives 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;&lt;a class=&#34;heading-link&#34; href=&#34;#digest-headers&#34;&gt;Digest headers&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#publishing-images&#34;&gt;Publishing images&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Unlike a regular Docker Registry, where you can use &lt;code&gt;docker push&lt;/code&gt;, I have to upload files in a specific way since I need manifests and indexes to upload into the 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. A publish script transforms the OCI layout into the storage zone structure, then uploads everything using the Bunny.net API for storagezones. After the upload, the script 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;&lt;a class=&#34;heading-link&#34; href=&#34;#garbage-collection&#34;&gt;Garbage collection&lt;span class=&#34;heading-hash&#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 to handle cleanup. It walks each image&amp;#39;s tags, resolves every referenced blob, manifest, and index, then checks for unreferenced files in _blobs. If it finds any, it deletes them using the Bunny API. This can be a bit slow since it has to walk the whole graph, but it&amp;#39;s good enough for my use case since I only publish a few images and for each of those images, I only use the &lt;code&gt;latest&lt;/code&gt; tag.&lt;/p&gt;&#xA;&lt;h2 id=&#34;pulling-images&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#pulling-images&#34;&gt;Pulling images&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#limitations&#34;&gt;Limitations&lt;span class=&#34;heading-hash&#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. This setup works well for personal and public images where the push workflow goes through CI anyway, and it runs on a single pull zone with no servers 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. It was an interesting idea, and when I found out about SDK&amp;#39;s for &amp;#34;AI&amp;#34; CLIs, I wondered if I could do something similar, but using a harness built/maintained by a billion-dollar corporation, instead of handling the loop and execution environment myself.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Disclaimer: I chose Copilot for this because: it was free to use, the novelty of having a GitHub thing do something for something other than their platform, and these companies don&amp;#39;t need more money, especially 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. Turns out 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 you would with a RESTful API, except in the same non-deterministic way the CLIs behave, plus they can still go through their normal loop and call tools. I happened to use Copilot, but it seems like this is a common pattern amongst these tools. 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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-approach&#34;&gt;The approach&lt;span class=&#34;heading-hash&#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, with an agentic loop that has two tools: one for executing Python (to analyze the codebase) and one for submitting the review. The bot also clones the repo so the model has filesystem access through Python execution.&lt;/p&gt;&#xA;&lt;p&gt;The way I approached this was different, and yet also similar. A webhook listener watches for a specific pattern in PR comments, and when it matches, it fires off a review. The review starts off by the golang process fetching information about the PR such as diff, other existing comments, etc., then spins up the copilot cli, with a crafted prompt based on the collected context, and a set of custom tools. The agent then 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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-tools&#34;&gt;The tools&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The agent gets three tools. These are the bridge between the PR and the agent itself. They translate between &amp;#34;AI wants to give feedback&amp;#34; and &amp;#34;Gitea needs specific API calls.&amp;#34; 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, with a severity level, 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. More on this one in a moment.&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 this exactly once, as its last action.&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;, where she discusses setting a confidence threshold for AI reviewers. Her key insight: set a confidence threshold (&amp;gt;80%) so the reviewer only comments when it&amp;#39;s fairly sure something is wrong. Without that threshold, 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 having a confidence threshold, but my confidence in these models are also not high enough to trust in their weighting. If I were building a product, or if this were a production environment where overloading the developers with noise would be a problem, then I probably would have the bot stay silent. But, for an experiment, I wanted to see all that these models had to offer, to make the evaluation myself.&lt;/p&gt;&#xA;&lt;p&gt;These low confidence observations are hidden in the main review, in a collapsible section at the bottom, so as not to distract from the main review, but they&amp;#39;re still there for anyone who wants to see them. 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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-system-prompt&#34;&gt;The system prompt&lt;span class=&#34;heading-hash&#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, especially around instructions on what to skip, such as linting, since presumably there are other CI workflows already catching those errors, and they&amp;#39;re better and more efficient at finding them.&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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-feedback-loop&#34;&gt;The feedback loop&lt;span class=&#34;heading-hash&#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 because when someone reads a bot-generated review, they should be able to see what produced it and roughly what it cost. Having it say &amp;#34;claude-sonnet-4.5, 12 API calls, 45k input tokens...&amp;#34; makes sure that the developer is aware of the resources that went into producing the review, and hopefully encourages their future contributions to be focused. If a contribution is too large, it may overflow the context window, causing reviews to be less effective.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-pattern&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-pattern&#34;&gt;The pattern&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;So little of this is Copilot-specific. The SDK, the model, the hosting. Those are all interchangeable. The tools are what make it work somewhere it was never built for.&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;A common theme in my posts, is to treat my homelab as if it has the same security requirements as a production environment. Since, in theory, it is a production environment, and I wouldn&amp;#39;t want anything to happen to it. For protected access, I normally use something like Tailscale, since I don&amp;#39;t need to expose anything to the internet, but sometimes when I am accessing it via a remote network, that network may be locked down and prevent Tailscale/wireguard connections, so I need a break-glass solution.&lt;/p&gt;&#xA;&lt;p&gt;It is no secret from other posts on this blog, that I use Hashicorp&amp;#39;s suite of tools, and the next one I&amp;#39;d like to introduce is Boundary. Boundary is a tool that allows for identity based access management, meaning that no matter the system I want to connect to in my homelab, I can log into Boundary, and connect to it, without having to worry about SSH tunneling, or exposing it to the internet. I also might not have access to a machine that I can install VPN software on, and in the case of Boundary, it is a single binary that doesn&amp;#39;t need administrative access to run, so it is a viable option for me.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-problem&#34;&gt;The Problem&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Sadly, I am budget constrained for my homelab, and the community version of Boundary does not support ingress/egress workers, which are required for multi-hop sessions. This is a feature that is only available in the enterprise version of Boundary, or HCP (HashiCorp Cloud Platform).&lt;/p&gt;&#xA;&lt;p&gt;I&amp;#39;d like to have these multi-hop sessions, so I can keep some VLAN isolation, and not have the server in the DMZ be able to connect back directly to every server in my homelab.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;#39;s a visual of what I wanted to achieve:&lt;/p&gt;&#xA;&lt;div class=&#34;mermaid&#34;&gt;&#xA;graph TB&#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&#xA;&#xA;&lt;/div&gt;&#xA;&lt;h2 id=&#34;my-diy-solution&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#my-diy-solution&#34;&gt;My DIY Solution&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The Boundary connection flow is key to my solution. Each session goes through the following steps:&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;With this process in mind, I developed a workaround. First, I had to create an FRP (Fast Reverse Proxy) server on the same server as my DMZ Boundary Controller and Worker. That way the internal workers could reach out to that server, and I wouldn&amp;#39;t need to open any firewall rules in the direction of the internal workers. Then, after the workers are registered with Boundary, I also had to create a target for the internal workers which point to a unique port from &lt;code&gt;frps&lt;/code&gt; on the Boundary server loopback that aligns with a specific worker.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;#39;s how the connection flow works with my solution:&lt;/p&gt;&#xA;&lt;div class=&#34;mermaid&#34;&gt;&#xA;sequenceDiagram&#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&#xA;&#xA;&lt;/div&gt;&#xA;&lt;h3 id=&#34;setting-it-up&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#setting-it-up&#34;&gt;Setting it up&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Using the standard Boundary installation process, you&amp;#39;d need to expose the DMZ Boundary Controller (for API access, and usually 9200), and worker (usually port 9202) to the internet. It could be something like &lt;code&gt;boundary.tklk.dev&lt;/code&gt;, and based on your preferences you could use Caddy and have it use real TLS certificates for when you are attempting to access the API. The internal workers will also need to connect to the cluster worker coordinator on port 9201, but that doesn&amp;#39;t need to be exposed to the internet, and can be done via the same way that the internal workers connect to the FRP server.&lt;/p&gt;&#xA;&lt;h4 id=&#34;installing-frp-server&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#installing-frp-server&#34;&gt;Installing FRP Server&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;First, you&amp;#39;ll need to install FRP on your 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;A sample &lt;code&gt;frps&lt;/code&gt; configuration, based on what I am actually using:&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;&lt;a class=&#34;heading-link&#34; href=&#34;#internal-worker-setup&#34;&gt;Internal Worker Setup&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Now that you have the brains of the operation setup, you&amp;#39;ll need to move onto the internal workers, and follow these steps:&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;Here&amp;#39;s a sample frpc configuration, based on what I am actually using:&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;&lt;a class=&#34;heading-link&#34; href=&#34;#registering-the-internal-worker-as-a-target&#34;&gt;Registering the Internal Worker as a Target&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The last part of the setup is that you&amp;#39;ll now need to register the internal worker as a target in Boundary. This is done by creating a target that points to the FRP server&amp;#39;s loopback interface, and the port that you set in the &lt;code&gt;frpc&lt;/code&gt; configuration. This can be done via terraform, the boundary cli, or the Boundary UI.&lt;/p&gt;&#xA;&lt;p&gt;A target with 127.0.0.1 as the target host, and the port that you set in the &lt;code&gt;frpc&lt;/code&gt; configuration, along with the default client port being the same as the worker port is the bare minimum you&amp;#39;ll need to set.&lt;/p&gt;&#xA;&lt;p&gt;You may also want to set an egress worker filter, to ensure that your DMZ worker is the one that connects to the frps port, as the target uses localhost, the routing might be assigned to a different worker that is not yet accessible.&lt;/p&gt;&#xA;&lt;h2 id=&#34;using-the-setup&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#using-the-setup&#34;&gt;Using the Setup&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now comes the fun part, actually using the setup. Go through the standard authentication process to get a session token, and then use the &lt;code&gt;boundary connect&lt;/code&gt; command 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;The workflow to access internal resources involves two steps:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;First 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;While this requires an extra step compared to the enterprise solution, it provides the same functionality at no additional cost.&lt;/p&gt;&#xA;&lt;h2 id=&#34;limitations-and-considerations&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#limitations-and-considerations&#34;&gt;Limitations and Considerations&lt;span class=&#34;heading-hash&#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 two-step connection process is less streamlined than 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;Security considerations: Be careful with your FRP token to prevent unauthorized access&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;While I probably don&amp;#39;t need the multi-hop sessions in my homelab, and likely single hop sessions would be sufficient, I wanted to fully explore the capabilities of Boundary to learn more about it in depth. I came out of this experience with a deeper understanding of the connection flow, and techniques to extend and debug various networking issues of it.&lt;/p&gt;&#xA;&lt;p&gt;This could be extended to use vault and broker SSH credentials with boundary, but you could leverage my previous post on using vault with SSH to do that instead. For now, I am happy with the setup I have.&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;&lt;a class=&#34;heading-link&#34; href=&#34;#adding-action-tmate-to-your-workflow&#34;&gt;Adding action-tmate to your workflow&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#connecting-to-your-runner&#34;&gt;Connecting to your runner&lt;span class=&#34;heading-hash&#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 and then the workflow will continue with the next step after the action-tmate step.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Security: Remember that SSH access is public (if someone gets the link), so don’t leave it idle for too long.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;targeted-debugging-approaches&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#targeted-debugging-approaches&#34;&gt;Targeted debugging approaches&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#only-on-failure&#34;&gt;Only on failure:&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#manual-trigger&#34;&gt;Manual trigger:&lt;span class=&#34;heading-hash&#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, and have 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;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There are a lot of ways to debug failing workflows, but nothing beats direct access to the exact environment where the workflow is running. &lt;a href=&#34;https://github.com/mxschmitt/action-tmate&#34; rel=&#34;nofollow&#34;&gt;action-tmate&lt;/a&gt; provides many other advanced configuration options as well that you can investigate.&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;As an experiment to familiarize myself with Nomad driver plugins, I created 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; that allows you to manage macOS VMs using Nomad. It allows you to manage macOS VMs with Nomad in a similar way to how you would manage Docker containers.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Disclaimer: This is a WIP and not yet ready for production use. It is a proof of concept and should be treated as such. Please do not use this in production environments. If you need production ready solutions, consider using &lt;a href=&#34;https://tart.run/orchard/quick-start/&#34; rel=&#34;nofollow&#34;&gt;Orchard&lt;/a&gt; which is built by the same developers behind Tart.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;what-is-tart&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#what-is-tart&#34;&gt;What is Tart?&lt;span class=&#34;heading-hash&#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 that is built to let you manage macOS VMs on Apple Silicon. One of Tart&amp;#39;s key features is its integration with OCI-compatible container registries, allowing you to store and distribute VM images just like Docker containers.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-create-the-nomad-driver-plugin&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#why-create-the-nomad-driver-plugin&#34;&gt;Why create the Nomad driver plugin?&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;One of the ways I like to learn new things is to build something. Since I wanted to learn more about Nomad and its driver plugin system, I decided to create a driver plugin. I chose Tart, since many others already exist such as Docker, etc.. Since Tart only manages specific VMs on a specific machine, rather than orchestrates across several, I thought it would be a good candidate for a driver plugin. Tart already has an orchestration manager, &lt;a href=&#34;https://tart.run/orchard/quick-start/&#34; rel=&#34;nofollow&#34;&gt;Orchard&lt;/a&gt;, but it is bespoke for Tart itself, and you may want to use Nomad for other workloads as well.&lt;/p&gt;&#xA;&lt;h2 id=&#34;sample-nomad-job&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#sample-nomad-job&#34;&gt;Sample Nomad Job&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here&amp;#39;s a simple example of how you can spin up 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;&lt;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There are many reasons why you may want to orchestrate macOS VMs, such as CI/CD workflows, ephemeral development environments, or testing infrastructure, and this plugin allows you to do just that.&lt;/p&gt;&#xA;</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>Interesting discoveries about Azure Functions and managed identities for TLS certificate automation</description>
      <content:encoded>&lt;p&gt;As a part of a project to automate TLS certificates for Azure Application Gateways, I ran into an unexpected issue with Azure Functions and managed identities. Specifically, the tool I was using was attempting to fetch a managed identity token, but wasn&amp;#39;t able to. I had to review the code of the tool, and many pages of documentation to discover that Azure Functions provide environment variables that contain the correct endpoint information which differ ever so 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, and the project I&amp;#39;m working on is to automatically request certificates from Let&amp;#39;s Encrypt/ZeroSSL and store them in Azure Key Vault so that Azure Application Gateway can use them. This is the first in what will likely be a series of posts documenting this journey.&lt;/p&gt;&#xA;&lt;h2 id=&#34;overview&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#overview&#34;&gt;Overview&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;My idea 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, using Azure&amp;#39;s managed identity to authenticate with Azure Key Vault. This would allow the certificates to be stored directly in Key Vault for App Gateway to use.&lt;/p&gt;&#xA;&lt;p&gt;Since the Azure Function would run on a schedule, this would ensure certificates are always renewed well before expiration. Fresh certificates, without any manual intervention, and no need to worry about expiration dates.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-problem&#34;&gt;The Problem&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Installing acme.sh in the azure function was quick enough, but when trying to run it with dns validation using a managed identity, I ran into the issue of being unable to fetch a token.&lt;/p&gt;&#xA;&lt;p&gt;The error was that it was unable to connect to the managed identity token service. In most Azure environments, services access the managed identity token service via 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. In most cases acme.sh script would&amp;#39;ve been using the correct endpoint.&lt;/p&gt;&#xA;&lt;p&gt;After SSH&amp;#39;ing into the Azure Function itself, I was able to confirm that it wasn&amp;#39;t able to connect to the metadata endpoint. It was only after reading through 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 docs&lt;/a&gt; that I discovered that I should be using the &lt;code&gt;IDENTITY_ENDPOINT&lt;/code&gt; environment variable to get the correct endpoint.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-solution&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-solution&#34;&gt;The Solution&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now that I discovered the correct endpoint (along with the additional header to be used for authentication), I was able to fetch the managed identity token successfully with curl. Then I had to search through acme.sh&amp;#39;s code to discover how it was attempting to fetch the token. I discovered that it made the same assumption I had about the hardcoded endpoint, and so I was able to quickly throw together a code change to fix the issue.&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;whats-next&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#whats-next&#34;&gt;What&amp;#39;s Next?&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;In the series of blog posts that I plan to write about the system design for auto-renewal of TLS certs using ACME for Azure App Gateways, I&amp;#39;ll dive deeper into the actual implementation of the certificate automation process, and how I deeply integrated it into the Azure ecosystem.&lt;/p&gt;&#xA;&lt;p&gt;Stay tuned if you&amp;#39;re interested in learning more about how to automate TLS certificates for Azure Application Gateways.&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;diving-right-into-it&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#diving-right-into-it&#34;&gt;Diving right into it&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;According to the docs, all that is required should only be running &lt;code&gt;ssh-keygen -t ed25519-sk&lt;/code&gt; and then touching the SoloKey when prompted. However, when testing this, I was on macOS devices and ran into an issue where the SSH version that ships with macOS doesn&amp;#39;t support these types of keys. So, I had to install a newer version of OpenSSH 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;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;&lt;a class=&#34;heading-link&#34; href=&#34;#creating-a-key-tied-to-your-solokey&#34;&gt;Creating a key tied to your SoloKey&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now that I have the right version of SSH installed, I can finally 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;&lt;a class=&#34;heading-link&#34; href=&#34;#making-it-convenient&#34;&gt;Making it convenient&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#setting-up-the-server-side&#34;&gt;Setting up the server side&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-moment-of-truth&#34;&gt;The moment of truth&lt;span class=&#34;heading-hash&#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;h2 id=&#34;wrapping-up&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#wrapping-up&#34;&gt;Wrapping up&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Setting this up took a bit of tinkering (especially figuring out about macOS and its older SSH version), but the extra security is worth it. You can also use this key for other services, like your Gitea instance.&lt;/p&gt;&#xA;&lt;p&gt;If you are on a mac, you could also look into the tool &lt;a href=&#34;https://github.com/maxgoedjen/secretive&#34; rel=&#34;nofollow&#34;&gt;Secretive&lt;/a&gt;, which allows you to store your SSH keys in the Secure Enclave of your Mac and integrate with the native 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 a lot of pipeline failures due to the smallest of issues. Most commonly it is whitespace differences in YAML files that cause unexpected parsing or complete failures. Recently, I was helping someone debug a workflow where a copy and paste from a different workflow brought in spaces for some lines instead of tabs leading to the pipeline to fail.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Update: After writing this post, I discovered that a similar approach is already being done with 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, but using Dhall instead of Nix.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;the-problem-with-yaml&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-problem-with-yaml&#34;&gt;The Problem with YAML&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;YAML is extremely sensitive to whitespace, and that can lead to subtle issues that are hard to debug. Especially when you can&amp;#39;t see the differences visually between tabs and spaces.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-solution-nix&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-solution-nix&#34;&gt;The Solution: Nix&lt;span class=&#34;heading-hash&#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. Recently, I was using Terranix to manage some infrastructure (which is a tool that translates Nix to JSON for Terraform), I realized a similar approach could work for Gitea Actions. Both JSON and YAML represent data as key-value pairs, making them easily convertible between each other, meaning I can use the same approach to convert Nix to JSON to YAML.&lt;/p&gt;&#xA;&lt;h3 id=&#34;benefits-of-using-nix-for-workflows&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#benefits-of-using-nix-for-workflows&#34;&gt;Benefits of Using Nix for Workflows&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Elimination of whitespace-related errors&lt;/li&gt;&#xA;&lt;li&gt;Nix features, like conditionals, loops, and functions&lt;/li&gt;&#xA;&lt;li&gt;Improved code reuse and maintainability&lt;/li&gt;&#xA;&lt;li&gt;Ability to split workflows into smaller, manageable components&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;implementing-nix-based-workflows&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#implementing-nix-based-workflows&#34;&gt;Implementing Nix-based Workflows&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;step-1-creating-a-basic-step-function&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#step-1-creating-a-basic-step-function&#34;&gt;Step 1: Creating a Basic Step Function&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The most common component of a workflow is a step. So we could start by defining a function that creates a 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;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 any null fields, resulting in cleaner YAML output.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Note: In Nix, &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-creating-a-checkout-function&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#step-2-creating-a-checkout-function&#34;&gt;Step 2: Creating a Checkout Function&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Building on the &lt;code&gt;mkStep&lt;/code&gt; function, the most common step in every workflow is the checkout step. So we can create a function to create a checkout step which uses the &lt;code&gt;mkStep&lt;/code&gt; function.&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;This approach can be extended to other common actions, and potentially even generating functions automatically from &lt;code&gt;action.yaml&lt;/code&gt; files.&lt;/p&gt;&#xA;&lt;h2 id=&#34;step-3-creating-a-workflow&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#step-3-creating-a-workflow&#34;&gt;Step 3: Creating a Workflow&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Using the functions above, we 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;generating-the-gitea-actions-yaml-workflow&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#generating-the-gitea-actions-yaml-workflow&#34;&gt;Generating the Gitea Actions YAML Workflow&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;To convert our Nix workflow to YAML, we can use 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;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 command generates the YAML file (assuming you have the &lt;code&gt;yq&lt;/code&gt; command installed).&lt;/p&gt;&#xA;&lt;h2 id=&#34;future-possibilities&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#future-possibilities&#34;&gt;Future Possibilities&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The benefits can be immediatly seen, especially for larger projects. For example, in the Gitea project, there are two workflows, one to build release artifacts for a nightly release, and one to  build release artifacts for a tagged release. Both of these workflows are very similar, and so arguments and conditionals could be used to reduce output by half.&lt;/p&gt;&#xA;&lt;p&gt;Nix Flakes could potentially be used to define the inputs for an Action&amp;#39;s step, and then be used by the workflow. This would allow for a more declarative and modular approach to workflows.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;By using Nix, I can now write workflows in a more declarative and modular way, and all of my pain points with YAML have been resolved.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor&amp;#39;s note: The Nix above hasn&amp;#39;t been tested, and is just a rough draft of what I&amp;#39;m thinking. If you do attempt this as an approach I&amp;#39;d love to hear, and update this post with any feedback you might have. I am also a maintainer of the Gitea project.&lt;/p&gt;&#xA;&lt;/blockquote&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;In my use of Google Cloud Platform (GCP), I often recreate virtual machines (VMs), requiring frequent updates to connection information. This process becomes cumbersome, especially when dealing with stale entries in the &lt;code&gt;known_hosts&lt;/code&gt; file and managing DNS entries for a growing number of servers. Additionally, using the Identity-Aware Proxy (IAP), which requires the &lt;code&gt;gcloud&lt;/code&gt; CLI for secure access, adds another layer of complexity. What I ended up doing was to leverage my previous experience with SSH configuration files to streamline my workflow.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-challenge&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-challenge&#34;&gt;The Challenge&lt;span class=&#34;heading-hash&#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;the-solution&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-solution&#34;&gt;The Solution&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;To implement my solution, I needed two key tools:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&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 VM&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The &lt;code&gt;gcloud compute ssh&lt;/code&gt; is a wrapper around &lt;code&gt;ssh&lt;/code&gt; that helps with authentication, and IP resolution. This command also got a bit unwieldy when attempting to combine it with other ssh based tools such as ansible, or rsync, as if I wanted to rsync between my local machine, and a remote server, I had to pass the command into rsync, and the same for other ssh-based tools. As I previously worked with &lt;a href=&#34;/posts/use-vault-as-ssh-ca/&#34;&gt;Vault to adjust the ssh configuration to execute a command per connection&lt;/a&gt; I figured I could use the same trick here. I found a helpful stating point on &lt;a href=&#34;https://unix.stackexchange.com/a/592199&#34; rel=&#34;nofollow&#34;&gt;StackOverflow&lt;/a&gt; and was able to adapt/extend it for my needs.&lt;/p&gt;&#xA;&lt;p&gt;To maintain security without opening SSH ports to the public or assigning public IPs to each VM, I utilized GCP&amp;#39;s Identity-Aware Proxy for tunneling. The &lt;code&gt;gcloud&lt;/code&gt; cli makes this easy by providing the &lt;code&gt;--tunnel-through-iap&lt;/code&gt; flag.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-configuration&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#the-configuration&#34;&gt;The Configuration&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The final addition to my SSH configuration was the following:&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 some ports from inside GCP&amp;#39;s network to my local machine. So to add customizations for specific host, you can define a more specific host (without the wildcard), and add the options. SSH will then also use the settings defined in 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;&lt;a class=&#34;heading-link&#34; href=&#34;#troubleshooting&#34;&gt;Troubleshooting&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you are following along and encounter issues with this setup, ensure that you&amp;#39;ve added the IAP IP addresses to your GCP network ingress rules. You can find the list of IP addresses 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;. Also, you may need to sign into the &lt;code&gt;gcloud&lt;/code&gt; cli, if you haven&amp;#39;t already.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;This setup has saved me a lot of time. I could manage DNS entries when creating the servers with infra-as-code, but this approach eliminates extra configuration which presents another point of failure, and lets me add extra security by not opening up any ports to the world.&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>Convert your existing Drone workflow to work with Gitea Actions.</description>
      <content:encoded>&lt;p&gt;Drone.IO is a CI/CD platform that uses Docker Containers for each pipeline step. This makes it easy to use existing Docker containers as steps in your pipeline. You can go futher and create special containers that are designed to be used as steps in your pipeline that accept arguments from the pipeline to run pre-defined tasks.&lt;/p&gt;&#xA;&lt;p&gt;These plugins can be re-used in Gitea Actions to be able to run the same tasks in Gitea Actions as you would in Drone.&lt;/p&gt;&#xA;&lt;p&gt;To give an example, a Drone 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;, and what it does is to allow me to copy files from the build pipeline to a remote server. I had a step in my Drone pipeline that 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 workflow step will call a pre-defined binary in the drone-scp Docker image that will copy the files from the release directory to the target directory on the remote server without the need to have that script defined in full in the pipeline.&lt;/p&gt;&#xA;&lt;p&gt;As this is published as a Docker image, it can be used in Gitea Actions as well. There are two approaches: a quick approach that works for most plugins, and a more involved approach that 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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-simple-approach-use-the-docker-image-directly&#34;&gt;The simple approach: use the Docker image directly&lt;span class=&#34;heading-hash&#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;&lt;a class=&#34;heading-link&#34; href=&#34;#the-full-approach-wrap-the-plugin-as-a-gitea-action&#34;&gt;The full approach: wrap the plugin as a Gitea Action&lt;span class=&#34;heading-hash&#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 will be a short script that passes all the input arguments for the action to the drone-scp binary. The entrypoint.sh script 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;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;Finally, you&amp;#39;ll need an &lt;code&gt;action.yml&lt;/code&gt; file that lets Gitea Actions know what inputs should be used.&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;Now, you can reference this action in any Gitea Actions workflow the same way that you would have used the Drone.IO plugin.&lt;/p&gt;&#xA;&lt;p&gt;An example of this in action:&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;The author of the SCP Drone plugin, Appleboy, has done exactly this already, and you can use that action from his repo found at &lt;a href=&#34;https://github.com/appleboy/scp-action/&#34; rel=&#34;nofollow&#34;&gt;https://github.com/appleboy/scp-action/&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;This is a simple example, but you can convert any Drone plugin to a Gitea Action by following the same steps. This allows you to re-use existing plugins in Gitea Actions without having to re-write the entire plugin, which is especially useful as you can use the programming language of your choice, rather than relying on the ones supported by the runner 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. I could use Parallels or VMware Fusion, but I instead chose Proxmox to support open-source software. Should the need arise to run production-level workloads, I can always purchase a Proxmox license and deploy it on a dedicated server. The challenging part was that Proxmox doesn&amp;#39;t distribute itself for arm64/aarch64, so I needed to find a way how to do that.&lt;/p&gt;&#xA;&lt;h2 id=&#34;getting-started&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#getting-started&#34;&gt;Getting Started&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Since Proxmox is an OS in and of itself (based on Debian Linux), and doesn&amp;#39;t come packaged for arm64, I needed a way to get a VM for it up and running. Since it is based on Debian Linux, a Debian install could be converted to Proxmox, and there are many guides on how to do this for x86_64, so I decided to attempt this route.&lt;/p&gt;&#xA;&lt;p&gt;UTM has a great interface for working with Apple&amp;#39;s Hypervisor, and QEMU. Using QEMU directly was definitely an option, but UTM&amp;#39;s user-friendly interface made the setup process much smoother and more intuitive for me. After installing &lt;a href=&#34;https://mac.getutm.app/&#34; rel=&#34;nofollow&#34;&gt;UTM&lt;/a&gt;, I created a Debian VM using an image provided directly by UTM from their gallery of several pre-made VM images. This was quick and easy, as the presets were all configured so everything worked out of the box.&lt;/p&gt;&#xA;&lt;h2 id=&#34;converting-debian-to-proxmox&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#converting-debian-to-proxmox&#34;&gt;Converting Debian to Proxmox&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now that I had a Debian VM, I needed to convert it to a Proxmox VM. 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;To use those pre-made packages I&amp;#39;ll need to configure a few things with the VM first. First, you&amp;#39;ll need to make sure your VMs hostname can be resolved using &lt;code&gt;/etc/hosts&lt;/code&gt;. Since the VM already has &lt;code&gt;debian&lt;/code&gt; as the hostname, I figured I&amp;#39;d keep that, and so I added &lt;code&gt;192.168.64.11 debian&lt;/code&gt; to the end of the hosts file. This is so that the VM can resolve its own hostname, and so that the Proxmox installer can resolve the hostname as well.&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;Now that the VM has the prerequisite configuration, we can get started with installing Proxmox.&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 through all these steps, I rebooted the VM, and then I was able to browse 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;&lt;a class=&#34;heading-link&#34; href=&#34;#running-proxmox&#34;&gt;Running Proxmox&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now, you are running a standard install of Proxmox and can do anything with it that you would on any other install. The only difference is that you need to ensure any container or VM you run uses an arm64/aarch64-based image.&lt;/p&gt;&#xA;&lt;h2 id=&#34;alternatives-considered&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#alternatives-considered&#34;&gt;Alternatives considered&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A non-exhaustive list of options considered, include Parallels and VMWare Fusion as mentioned above, but also just running Asahi Linux and running VMs directly on the host without having to use a hypervisor for Proxmox itself. Using a separate device was also an option, but this would require lugging around that device when I&amp;#39;m out and about. Keeping it as something on my laptop, without disrupting my day-to-day desktop environment, was most preferable for me.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Running Proxmox on the M1 MacBook Air was surprisingly smooth, even with the base model specifications. The system handled the virtualization tasks well, with minimal noticeable lag. The user experience was better than what I would expect on an x86_64 system with similar specs. I also wasn&amp;#39;t running any serious workload, but was able to run several VMs and containers without much of a performance hit.&lt;/p&gt;&#xA;&lt;p&gt;Future enhancements could include using Terraform to automate environment setup or exploring integrations with the Proxmox API to script operations. For now, I&amp;#39;m happy with the setup and will continue to experiment with it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;disclaimer&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#disclaimer&#34;&gt;Disclaimer&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editor’s Note: This setup is not recommended for production use. The aarch64/arm64 packages I’m using are not officially supported or provided by Proxmox, and running production workloads on an M1 MacBook Air is not advisable, as it&amp;#39;s not meant 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 repos, I&#39;ve been slowly moving them to my own self-hosted homelab. Today, it is my Docker images turn.</description>
      <content:encoded>&lt;p&gt;One of the great things about Gitea, is that it comes with a built-in Docker registry. This means that you don&amp;#39;t need to be reliant on Docker Hub (or its rate-limits) to host your docker images, or any other OCI compliant image for that matter.&lt;/p&gt;&#xA;&lt;p&gt;In this post, I&amp;#39;ll go over how I migrated some of my docker images from the Docker Hub to my own self-hosted registry in Gitea. I won&amp;#39;t go over the setup of the package registry in Gitea itself, as it&amp;#39;s enabled by default, and I&amp;#39;d rather focus on the migration of images itself. The approach I took also didn&amp;#39;t use any registry specific tools, and so it can be generalized to most other registries.&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 that allows for many different operations on container images, and image registries. It also doesn&amp;#39;t need to be run as root, or have an active docker daemon running.&lt;/p&gt;&#xA;&lt;p&gt;While skopeo is a great tool, it can&amp;#39;t fetch the entire list of images in a certain namespace, so I had to use some bash scripting to fetch the list of images, loop over them, and pass each of them to skopeo. Some caveats to be aware of, this script uses &lt;code&gt;skopeo sync&lt;/code&gt; instead of &lt;code&gt;skopeo copy&lt;/code&gt;, and that means that sync will copy all tags at once. The reason for choosing sync, rather than copy, is that it can handle all of the tags at once, instead of having to do another API to fetch the list of tags.&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;You can also use this script to copy images from other namespaces, so if you want to have a copy of another namespace, say for example Bitnami, you could use this script to do that as well.&lt;/p&gt;&#xA;&lt;p&gt;This script has helped me move over a lot of images, and it&amp;#39;s been working great for me. It&amp;#39;s missing a few things, like passing in the options as arguments, error handling, or pagination for the Docker Hub API, but I didn&amp;#39;t have more than 100 images in my namespace, so I didn&amp;#39;t need to worry about that. If you do use this script, and 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. You can use this with other registries as well, so you don&amp;#39;t need to use Gitea to use this script.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Let Wireguard clients use your IP Blocks advertised by BGP</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>Utilize those unused IPv4 addresses with a valid use-case to ensure they don&#39;t get reclaimed by ARIN or RIPE.</description>
      <content:encoded>&lt;p&gt;As an addendum to my previous post on &lt;a href=&#34;/posts/giving-yourself-ipv6-with-wireguard/&#34;&gt;giving yourself an IPv6 address using wireguard&lt;/a&gt;, I wanted to show how, if you are lucky enough to have a IPv4 range before the &amp;#34;great IPv4 exhaustion&amp;#34;, you can assign those addresses to clients using wireguard.&lt;/p&gt;&#xA;&lt;p&gt;Several consumer, prosumer, and enterprise cloud hosts, allow you to advertise your IP range for your cloud servers. Some of these providers are OVH, Vultr, and Equinix Metal, but like last time, I suggest you look into OVH due to their predictable costs around bandwidth, as well you don&amp;#39;t need to configure a BGP connection with them which makes setup much 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;Assuming you setup your wireguard connections as described in the previous post, there are a few adjustments that you&amp;#39;ll need to make to give your clients an address from your range. For the sake of simplicity, I&amp;#39;ll assume your IPv4 range is &lt;code&gt;8.8.8.0/24&lt;/code&gt;, even though this specific range is used by Google&amp;#39;s public DNS service.&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  # Let&amp;#39;s this specific client use this address publicly, note: if you are using IPv6 you&amp;#39;d give them one here too&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;and for your client, you&amp;#39;d have their &lt;code&gt;Interface&lt;/code&gt; configuration match the address defined 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;You&amp;#39;ll need to ensure that your server can indeed receive/send traffic for these ranges. Depending on if you&amp;#39;ve setup a BGP connection with your provider, or are using OVH where no BGP connection is required, the way to ensure this different based on your situation. A quick way to do this on OVH would be to create virtual interfaces of eth0/eno1 for each address.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editors Note: If you don&amp;#39;t have your own IPv4 range, you can do this with IPv6 which is much easier to get, and if you are an EU citizen I can assist you with this 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>Many ISPs don&#39;t support IPv6, and so this is how I work around this and give myself an IPv6 no matter where I am</description>
      <content:encoded>&lt;p&gt;After many years of asking, my Internet Service Provider (ISP) still doesn&amp;#39;t support IPv6; the same goes for many other ISPs. This is a problem for many reasons, the first being the exhaustion of IPv4 addresses, leading some ISPs to implement &amp;#34;Carrier-Grade NAT&amp;#34; (CGN), where many customers share the same address—the second being that it is getting more and more expensive for small websites to have their own dedicated IPv4 addresses, including cloud providers instituting a cost to have an address. The obvious solution would be for all ISPs to start supporting IPv6, but sadly, this requires effort on the part of ISPs, and the problem isn&amp;#39;t known to many of their customers, so the easy path for them is to keep status-quo.&lt;/p&gt;&#xA;&lt;p&gt;What I have done to work around this is to rent a server in a datacentre that has an IPv6 address, and to tunnel my traffic through it using Wireguard. This is a moderately involved solution, and does come with a cost, but by showing websites that IPv6 traffic is increasing, perhaps it&amp;#39;ll show that IPv6 is important to implement.&lt;/p&gt;&#xA;&lt;p&gt;You can do this with any server that has an IPv6 address, but I&amp;#39;ll be using one from OVH, because they don&amp;#39;t have bandwidth costs, and they have a datacentre located near me to reduce any additional latency that may be introduced from tunnelling my traffic. Other providers, such as DigitalOcean, Vultr, Linode, or others could also work, but beware around bandwidth costs.&lt;/p&gt;&#xA;&lt;p&gt;The way I started was to install Wireguard on both my laptop, and server. Assuming you are using Debian, or Ubuntu, you can run &lt;code&gt;apt install wireguard&lt;/code&gt; on both your laptop and server to install all the software you need.&lt;/p&gt;&#xA;&lt;p&gt;Next, I had to create a public and private key for both my laptop, and server. I did this by using the wireguard tools we just installed.&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 will create a private key, and a public key in the &lt;code&gt;~/.wg&lt;/code&gt; directory. Now, you&amp;#39;ll need to 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, you need to make sure your server can route your IPv6 traffic for you. This is done with some iptables configurations, and other 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;Now, both your laptop and server are ready to go. To start Wireguard on your server, run &lt;code&gt;wg-quick up wg0&lt;/code&gt; on your server. To start Wireguard on your laptop, run &lt;code&gt;wg-quick up wg0&lt;/code&gt; on your laptop. You can check that it&amp;#39;s working by running &lt;code&gt;wg show&lt;/code&gt; on both your laptop and server.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Editors Note: Instead of using wireguard for this, you could use something like &lt;a href=&#34;https://tailscale.com&#34; rel=&#34;nofollow&#34;&gt;Tailscale&lt;/a&gt; and setup/configure an exit-node, but this is left as an exercise for the user.&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;Even though you can think of Fly.io as a &amp;#34;container&amp;#34;-service hosting platform, it&amp;#39;s actually using Firecracker micro-VMs to run your service. They &lt;a href=&#34;https://fly.io/blog/docker-without-docker/&#34; rel=&#34;nofollow&#34;&gt;transmogrify&lt;/a&gt; Docker images into the micro-VMs they run. This means, it&amp;#39;s much easier to run a docker daemon, as you don&amp;#39;t need to worry about the implications of running the docker daemon inside of a docker container.&lt;/p&gt;&#xA;&lt;p&gt;After creating a whole Docker image with the docker daemon inside of it, and getting it working on Fly, I found that Fly already made one, and it&amp;#39;s much more succinct than mine. So this post will reference their implementation instead, as 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. So instead of trying to map an existing service into a fly.toml file, you could just use an existing docker-compose.yml.&lt;/p&gt;&#xA;&lt;p&gt;Next steps could include establishing a wireguard tunnel to your service and having the docker client on your local machine connect to the daemon over the tunnel.&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 is a service that provides ephemeral instances (Linux, and macOS) for you to use, primarily used for CI/CD workflows. Similar to Fly.io, it&amp;#39;s another way to not have to manage servers.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;#39;m currently using it to run some of my CI/CD workflows, but I want to play around with it in other ways. One, would to potentially make it a run-on-demand nixpkgs builder, similar to how I have with Fly.io, but with Fly.io I need to manage the instances myself, whereas Namespace will terminate instances after a set period of time. This reduces the monthly hosting spend.&lt;/p&gt;&#xA;&lt;p&gt;Before I start, I don&amp;#39;t begrudge Namespace at all for the (current, as of date of post) inability to SFTP files directly to instances, they are working hard on a lot of things, and this was a fun little thing I wanted to do. Namespace is constantly adding/building new functionality, and I suspect this will eventually be something they add, but I wanted something to play around with now. I happily pay Namespace for their service, and am very excited to see what they add next.&lt;/p&gt;&#xA;&lt;p&gt;I asked in the Namespace discord, if anyone had any way to do this already, and I was pointed to &lt;a href=&#34;https://github.com/schollz/croc&#34; rel=&#34;nofollow&#34;&gt;croc&lt;/a&gt; as something others have used before, but I wanted something that I didn&amp;#39;t need to run on the remote server in addition to locally. I&amp;#39;m sure there are a thousand other ways to do this, but I like to make fun little tools for myself in bash, and so this was a great excuse to play around.&lt;/p&gt;&#xA;&lt;p&gt;Unlike with Fly.io, Namespace doesn&amp;#39;t support direct SSH access, so I needed to come up with a way to &amp;#34;SFTP&amp;#34; the files up to the server. These files could be configuration, binary tools, and other files needed to run whatever is needed to run what I am working on at that time.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;nsc&lt;/code&gt; client allows for pseudo-shell sessions, and also running arbitrary commands, so I thought I could use that to my advantage. I originally did a naive way of copying files 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 wouldn&amp;#39;t work well with binary files, or large text files. Due to terminal length limitations, I also had to chunk the files up, and then reassemble them on the remote machine. This means that the transfer will take longer than usual, as each chunk will need to re-establish a connection with the server. A way to speed this transfer up would be to parallelize the transfer, but that&amp;#39;s not in scope for right now.&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;Then you&amp;#39;d run it like:&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;A way I could&amp;#39;ve avoided this using this hacky bash script would be to perhaps install tailscale in the ephemeral instance, and use &lt;code&gt;tailscale file cp&lt;/code&gt; to copy the files up, but that&amp;#39;s something for another day.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;I had some extra time after writing this post, and went back to add parallelization to the script. I used gnu&amp;#39;s &lt;code&gt;parallel&lt;/code&gt; to send the chunks to the remote machine. This sped up the transfer time significantly. You&amp;#39;ll need to ensure you have &lt;code&gt;parallel&lt;/code&gt; installed on your local machine to use these adjustments as it&amp;#39;s not installed by default on several common OSs. Here&amp;#39;s an updated section that you can replace parts of the script above:&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>Part 3 of the DIY CDN Series where we allow users to use their own domains, and automatically provision TLS certificates for them</description>
      <content:encoded>&lt;p&gt;Now that we are able to synch TLS certs and the Caddy configuration across various points-of-presence, we now need to ensure that Caddy will only request certificates for approved names. As we are doing this for Gitea Pages, we won&amp;#39;t know what the domain names are in advance and will have to create a validation service to handle this. This approach can also be abstracted to any SaaS that provides the ability for customers to bring their own domain.&lt;/p&gt;&#xA;&lt;p&gt;Caddy has a built-in way to request if a certain domain name is approved or not. The way this process works, is that in the Caddy configuration you&amp;#39;ll define an endpoint that will receive a request for a domain name, and run against your custom validation rules which could be a static list, a database call, or something else. If the name is approved, then the endpoint will return a 2xx status code, otherwise it will deny the request with a non-2xx status code.&lt;/p&gt;&#xA;&lt;p&gt;The benefits of ensuring that domains are processed against this validation service include not requesting certs for domains outside of your control which could use up your Let&amp;#39;s Encrypt quota, and it also prevents malicious actors from pointing their names at your infrastructure and receiving valid TLS certs.&lt;/p&gt;&#xA;&lt;p&gt;Following the previous tutorial where the Caddy configuration is dynamically generated, the customers domain will be added to the config file, but we don&amp;#39;t want users to be able to add any domain of their choice, we want to ensure that they have control over the domain, and that it also doesn&amp;#39;t point to a nefarious location.&lt;/p&gt;&#xA;&lt;p&gt;Using the following configuration as an explanation, we can see how this would work:&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;In this example, we are telling Caddy to ask the validation service at &lt;code&gt;https://validation.service.tld/endpoint&lt;/code&gt; if the domain name is approved for requesting TLS certificates. If the domain name is approved, then Caddy will request a certificate for the domain name, and serve the content as defined in the configuration, otherwise no certificates will be requested, protecting your Let&amp;#39;s Encrypt rate-limit.&lt;/p&gt;&#xA;&lt;p&gt;The validation service should be as fast as possible, to limit the time it takes for Caddy to request the certificate and respond to the initial request to that domain. The way Caddy will interact with that validation URL would be to make a GET request with a query string of &lt;code&gt;?domain=example.com&lt;/code&gt; where &lt;code&gt;example.com&lt;/code&gt; is the domain name that the user is requesting a certificate for. If the validation service returns a 2xx status code, then Caddy will request the certificate, otherwise any other status code will prevent Caddy from making a request for a certificate.&lt;/p&gt;&#xA;&lt;p&gt;The validation service could be as simple as a Go application that reads a file of approved domain names, or as complex as a database lookup. The important part is that it responds quickly and accurately to the domain name requests. Ideally, if you are running external checks on the domain, such as ensuring that it points to your loadbalancer or that it doesn&amp;#39;t attempt to use a nefarious domain, you should perform those checks asynchronously and cache the result in a database, rather than making those checks every time a request to the validation service is made.&lt;/p&gt;&#xA;&lt;p&gt;I will leave the completion of the validation service up to the reader, as there are many possible ways to implement this, and even many possible use-cases. There is no requirement for which language is used, or how the validation server be setup. You could use a standard http server that responds 200 to every request, to an entire monolithic application that integrates with your billing system, and everything in between.&lt;/p&gt;&#xA;&lt;p&gt;In this series, we looked at various different topics, and how I used them as building blocks for a DIY CDN, however these topics can each be used independently of each other, and in different ways. I hope you&amp;#39;ve enjoyed this series, and I look forward to writing more in the future. If I find that something else in building this CDN interesting, I&amp;#39;ll be sure to add yet another post in this series.&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>Part 2 of the DIY Series where we use Nomad to distribute Caddy configurations across nodes</description>
      <content:encoded>&lt;p&gt;In the previous post we discussed how to provision, and keep TLS certs in synch across multiple Caddy instances. In this post we will look at how to dynamically create and distribute Caddy configuration to multiple nodes using Hashicorp&amp;#39;s Nomad.&lt;/p&gt;&#xA;&lt;p&gt;Similar to other orchestrations tools such as Kubernetes, Nomad allows you to schedule and run containers, and non-containerized applications, across many servers with a standardized configuration.&lt;/p&gt;&#xA;&lt;p&gt;Using nomad, we can manage multiple servers, in many datacentres, to create those Points of Presence (PoPs) for our CDN we discussed in our previous post. We will create configuration that will run Caddy on each edge node, and generate configuration dynamically to act as a reverse proxy based on the applications that nomad is also running.&lt;/p&gt;&#xA;&lt;p&gt;First, we need to create the nomad job specification that defines the Caddy service we want to run. This will look something like:&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;  # as you with a CDN, you&amp;#39;ll want to have datacentres close to where your data is being browsed&#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 leverage 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;This job specification will create a Caddy instance on each node that has a hostname that matches &lt;code&gt;edge-lb-.+&lt;/code&gt;. This will allow you to have multiple loadbalancers in each datacentre, and have Caddy running on each of them. This example will serve the default Caddy landing page, and not forward any requests to your applications or request TLS certificates.&lt;/p&gt;&#xA;&lt;p&gt;To go beyond serving the default Caddy landing page, and start serving TLS certificates, we&amp;#39;ll need to extend the nomad job, and provide a template for the Caddy configuration file.&lt;/p&gt;&#xA;&lt;p&gt;The way that template files are loaded into a nomad job, is by mounting them into the container as a file. We can do this 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;&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;As you can see, the templates can take variables, and generate configuration based on them. We can extend this behaviour to have nomad generate configuration based on endpoints of services it is 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;&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;This will take the list of services from Nomad with the name &amp;#34;nomad-example-app&amp;#34;, loop over them using the &lt;code&gt;domain&lt;/code&gt; meta information, and have Caddy proxy traffic to the address, and port of each installation. Right now, the connection will happen over whichever network the docker bridge is attached to, and so please be aware that if your IPs are publically accessible, that bypass of the Caddy server may be possible based on network firewall rules.&lt;/p&gt;&#xA;&lt;p&gt;Minor updates to your application service will be needed. In your application Nomad specification, you&amp;#39;ll need to add a port that will be exposed for the container to listen on. In our case we are exposing port 80 of the container, and nomad will publish that to a random high port on the host. In your service meta information, you&amp;#39;ll need to define a domain so that nomad will be able to create a configuration block in the generated Caddy 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;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;Now as new services come online, or old ones go away, nomad will update the caddy configuration as needed.&lt;/p&gt;&#xA;&lt;p&gt;Nomad will restart the Caddy container as the configuration changes, but as this is possibly undesirable for a production load balancer, in the future you may want to look at using Caddy&amp;#39;s automatic reloading capabilities instead. This can be done by 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, which will send a signal to the Caddy process when the configuration changes. Caddy handles SIGUSR1 by performing a graceful config reload, avoiding the downtime of a full container restart.&lt;/p&gt;&#xA;&lt;p&gt;There are ways to expand on this further, by adding healthchecks, creating services dynamically using gitops or the nomad API, and integrating with a service mesh like Consul connect for mTLS on connection between Caddy, and the application. This covers the basics of using Nomad to dynamically generate and update Caddy configuration based on services defined by Nomad.&lt;/p&gt;&#xA;&lt;p&gt;In the way I plan to use the above for Gitea.pages, is to have a container exist for each static site, and create nomad services for each site. The static site will have an instance created in each region, and the Caddy server will direct traffic to the container in its region. This allows for regional CDN like capabilities, while keeping the sites loosely coupled and using Nomad for orchestration. When new static sites are built, we can let nomad know a new container is published, and then nomad will handle updating the services globally.&lt;/p&gt;&#xA;&lt;p&gt;In the next post we will look into how to use Caddy to safely request certificates for custom customer-provided domains on-demand, and how to validate 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;Editors 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 to offload building of nixpkgs from my low powered laptop, to a remote builder on fly where I can provision as many resources as I need, for as short of a period of time as needed.&lt;/p&gt;&#xA;&lt;p&gt;To start off, we&amp;#39;ll need to create the builder machine. You&amp;#39;ll likely need more than the default 256mb of ram that is provisioned, but maybe not the 2gb that I request. You can adjust the number of cpus and memory as needed. Since fly&amp;#39;s machines, are essentially containers (they do some neat things, and you can read about it in one of their many blog posts, but that is outside of scope of this post), you can also use the same container image that you would use with docker, in this case I am using nixos/nix.&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;We are not asking fly to start the machine right away, as we&amp;#39;ll need to add some more configuration before powering it on.&lt;/p&gt;&#xA;&lt;p&gt;First, we&amp;#39;ll need to give the machine more storage space, as storing/building a package may build more of the package&amp;#39;s dependency graph than just the package itself, and there will need to be some place to store it.&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;I&amp;#39;m setting 50gb, but since you are billed for space consumed regardless if the machine is powered on or not, you may wish to make this volume smaller based on your needs. You&amp;#39;ll also need to set the path that the volume is mounted with in your fly.toml 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;&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;Interesting note about this configuration, even though nix stores most things in &lt;code&gt;/nix&lt;/code&gt;, we are mounting to &lt;code&gt;/data/nix&lt;/code&gt; initially, as otherwise if we mount it directly to &lt;code&gt;/nix&lt;/code&gt;, it will be empty, and the machine won&amp;#39;t have access to any binaries. So we will mount to a different directory, copy everything over to the new volume, and then set the path to the correct location.&lt;/p&gt;&#xA;&lt;p&gt;Some internal Fly.io processes look for the &lt;code&gt;/bin/sleep&lt;/code&gt; binary, however in NixOS, due to it being entirely reproduceable, it stores binaries with a specific path name with a cryptographic hash of the package’s build dependency graph. This means that the &lt;code&gt;/bin/sleep&lt;/code&gt; binary is not available, so we&amp;#39;ll either need to create a symlink to it, or we can set a path to the binary in the 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;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;Your machine is now ready to be powered on. You can do 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;fly deploy -a tklk-nixbuilder&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Your terminal will show you progress of the machine starting up, and in a short period of time it will be all ready, and you can now SSH into it to finish some final setup steps.&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, you can change the &lt;code&gt;destination&lt;/code&gt; path of the mounted volume in your &lt;code&gt;fly.toml&lt;/code&gt; configuration file to &lt;code&gt;/nix&lt;/code&gt; and restart the machine with &lt;code&gt;fly deploy -a tklk-nixbuilder&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Your remote machine is now all ready to handle building of your remote nixpkgs.&lt;/p&gt;&#xA;&lt;p&gt;You can now use the &lt;code&gt;fly ssh issue&lt;/code&gt; command to generate an SSH key that can be used to SSH into your machine.&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;For sake of convenience you can add some minor configuration to your &lt;code&gt;~/.ssh/config&lt;/code&gt; file to reduce CLI options when letting your nix-builder connect to the remote machine.&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;p&gt;Finally, you&amp;#39;ll need to setup a proxy to forward port 2200 on your local machine to port 22 on your remote builder. This is to avoid having to configure a wireguard VPN connection to fly.io, which is a bit more involved than just adding a port forward. The &lt;code&gt;&amp;amp;&lt;/code&gt; in the command below, forces the proxy into the background so you don&amp;#39;t need to have a terminal dedicated to just the proxy command. Note: if you close the terminal then this command will quit and you&amp;#39;ll need to start it again if you want to connect back.&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;That&amp;#39;s it! Your remote builder, and local machine are all setup for building.&lt;/p&gt;&#xA;&lt;p&gt;You can test your remote builder by adding &lt;code&gt;--store ssh-ng://nixbuilder.fly&lt;/code&gt; to any &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;Some caveats are that the remote builder will be an x86-64 architecture, and if your local machine is aarch64 (aka Arm), you&amp;#39;ll need to pass &lt;code&gt;--system x86_64-linux&lt;/code&gt; to &lt;code&gt;nix-build&lt;/code&gt; to ensure that the remote builder will build the package. Sadly this means that the package will be built for x86-64, and not aarch64, so you&amp;#39;ll won&amp;#39;t be able to copy it to your machine. As the editors note says, you are better off using something like &lt;a href=&#34;https://nixbuild.net/&#34; rel=&#34;nofollow&#34;&gt;nixbuild.net&lt;/a&gt;, as they support aarch64 builders (though not macOS ones as of the time of writing).&lt;/p&gt;&#xA;&lt;p&gt;Continued 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: Synch TLS certificates across all 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>Learn how to synchronize TLS certificates across multiple Points of Presence (PoPs) using Caddy and an S3 bucket.</description>
      <content:encoded>&lt;p&gt;As a part of building a Content Delivery Network (CDN) for Gitea Pages, I&amp;#39;m documenting my process to share my experience with others. The reason I&amp;#39;m building one from scratch, rather than using a pre-built solution like Amazon CloudFront, is the potential need for many unique TLS certificates. From past experience, I&amp;#39;ve learned that there are limits to how many certificates you can have on a single CloudFront distribution. This is the first post in a series that will document the process of building a CDN from scratch.&lt;/p&gt;&#xA;&lt;p&gt;The initial task I&amp;#39;ll tackle is synchronizing TLS certificates across all the Points of Presence (PoPs) in the CDN. If you&amp;#39;re unfamiliar with what PoPs are, they are geographically distributed servers located near where end users are to improve content delivery performance. I&amp;#39;ve chosen to use Caddy for this purpose. Caddy is a web server that has built-in support for Let&amp;#39;s Encrypt and can automatically obtain and renew TLS certificates.&lt;/p&gt;&#xA;&lt;p&gt;While there are alternative approaches using other web servers like nginx, where a central server obtains the TLS certificates and then distributes them to the other servers, Caddy offers a more decentralized approach. I can use a plugin I wrote called &lt;a href=&#34;https://github.com/techknowlogick/certmagic-s3&#34; rel=&#34;nofollow&#34;&gt;certmagic-s3&lt;/a&gt;, that lets each instance of Caddy share the TLS certificates via an S3 bucket. This approach has the advantage that any of the PoPs can obtain and renew the TLS certificates, and the other PoPs will automatically receive the updated certificates.&lt;/p&gt;&#xA;&lt;p&gt;The most challenging part of this approach is to ensure that the plugin is properly installed in Caddy. You could use Caddy&amp;#39;s &lt;code&gt;xcaddy&lt;/code&gt; build tool, but Caddy also offers a &lt;a href=&#34;https://caddyserver.com/download&#34; rel=&#34;nofollow&#34;&gt;build service&lt;/a&gt; where you can download binaries that have already been compiled. Using that build service, select certmagic-s3 as a plugin to include, download it for your platform of choice, and you&amp;#39;ll have a Caddy binary with the plugin already installed.&lt;/p&gt;&#xA;&lt;p&gt;Now, using that binary, you can create a Caddyfile to configure Caddy to use the plugin and obtain TLS certificates from Let&amp;#39;s Encrypt. Here&amp;#39;s an example Caddyfile:&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;With the required information filled out from the configuration above, you can start Caddy and it will obtain the TLS certificates from Let&amp;#39;s Encrypt and store them in the S3 bucket. If you then start another instance of Caddy with the same configuration, it will automatically obtain the TLS certificates from the S3 bucket and serve the site.&lt;/p&gt;&#xA;&lt;p&gt;In upcoming posts I will describe how I setup nomad to distribute Caddy config to all the PoPs, and how I am using Caddy to serve custom dynamic domains for Gitea Pages. Stay tuned for more details on building out the CDN.&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>Learn how to create a reproducible build environment for your project by integrating Nix with 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; In the world of software, creating a reproducible build environment is the universe you need to invent. This post will walk you through using Nix in tandem with Gitea Actions to make that universe a reality for your projects.&lt;/p&gt;&#xA;&lt;p&gt;I am an enthusiastic user of Nix and am a maintainer of &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 reproducibility of the binaries it offers across different systems and its rapid update cycle.&lt;/p&gt;&#xA;&lt;p&gt;Gitea Actions is a CI/CD solution that can run your build and deployment tasks. Using Nix within Gitea Actions is as straightforward as adding a few lines to your workflow file. Here&amp;#39;s how:&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;This workflow will install Nix and then execute the &lt;code&gt;hello&lt;/code&gt; command. Note that we need to install &lt;code&gt;sudo&lt;/code&gt;, as it is a prerequisite for the &lt;code&gt;cachix/install-nix-action&lt;/code&gt; and is not present in the default Gitea Actions runner image. If you&amp;#39;re using a custom runner that already has sudo installed, feel free to skip that step.&lt;/p&gt;&#xA;&lt;p&gt;Regarding the Nix package channel, I prefer to live on the bleeding edge with &lt;code&gt;nixpkgs=channel:nixos-unstable&lt;/code&gt;. However, you&amp;#39;re free to pin to a more stable channel if you wish. The &lt;code&gt;cachix/install-nix-action&lt;/code&gt; Action does not have a channel configured by default, so you must specify one.&lt;/p&gt;&#xA;&lt;p&gt;If you haven&amp;#39;t explored Nix yet, I highly recommend you do so. It&amp;#39;s a powerful tool for creating consistent and reproducible build environments.&lt;/p&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>Use Keel to automate Kubernetes image updates the way Watchtower does for Docker.</description>
      <content:encoded>&lt;p&gt;Watchtower is an excellent tool for keeping your containers up to date. It&amp;#39;s a process that runs on a schedule and checks for new versions of your containers, and if it finds one, it pulls the new image and recreates the container with the latest image. It&amp;#39;s built for Docker, and it works great for Docker. But what about Kubernetes?&lt;/p&gt;&#xA;&lt;p&gt;Keel, a Kubernetes operator, also achieves what Watchtower can do but can automate Helm, DaemonSet, StatefulSet &amp;amp; Deployment updates. It also has a friendly UI to see the status of the updates it is managing.&lt;/p&gt;&#xA;&lt;h2 id=&#34;installing-keel&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#installing-keel&#34;&gt;Installing Keel&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The first step to utilizing Keel is installing it in your Kubernetes cluster. You can use &lt;code&gt;kubectl apply&lt;/code&gt; for this purpose:&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-your-deployments-for-keel&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#configuring-your-deployments-for-keel&#34;&gt;Configuring your Deployments for Keel&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Once you have installed Keel, you&amp;#39;ll need to configure your deployments to use it. This is as simple as adding a few labels to your Kubernetes deployment specifications. 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;. 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;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 above configuration means Keel will update the deployment whenever there&amp;#39;s a new major version of the container image.&lt;/p&gt;&#xA;&lt;h2 id=&#34;handling-private-images-and-rate-limits&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#handling-private-images-and-rate-limits&#34;&gt;Handling private images and rate limits&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you&amp;#39;re using private images or Docker Hub with its strict rate limit, you&amp;#39;ll need to configure Keel to authenticate with your registry. Keel also supports secrets for pulling images. Keel will use existing secrets that Kubernetes uses to pull the image so no additional configuration required.&lt;/p&gt;&#xA;&lt;h2 id=&#34;keel-ui&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#keel-ui&#34;&gt;Keel UI&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;One of the unique features of Keel is its UI which allows you to see at a glance the status of your deployments and any updates it&amp;#39;s managing. You can access it via a Kubernetes ingress or use kubectl port-forward:&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;wrapping-up&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#wrapping-up&#34;&gt;Wrapping Up&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Keel is a powerful tool that brings the simplicity and automation of Watchtower to the Kubernetes ecosystem. Whether you have simple Deployments, use Helm, or have more complex DaemonSets or StatefulSets, Keel has you covered.&lt;/p&gt;&#xA;&lt;p&gt;Remember, automating your image updates saves you time and ensures that you&amp;#39;re running the latest and potentially more secure version of your containers. As always, it&amp;#39;s essential to have robust rollback strategies and test pipelines in place, especially when using automatic updates.&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) is a low-cost, high-performance CDN provider that can be used to host static sites. This post will walk through the steps to hosting a static site using it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;creating-a-storage-zone&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#creating-a-storage-zone&#34;&gt;Creating a Storage Zone&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The first step is to create a storage zone. The storage zone is where the static site will be stored. To do this, log into the Bunny.net dashboard, click the Storage Zones tab, and follow the steps to create a new storage zone. The storage zone can be named anything, but it&amp;#39;s best to name something that will allow you to identify it later and associate it with your site quickly. You can select the regions you would like your content replicated to, and the more regions you have, the faster your site will be in those regions. Be careful; the more regions you select, the more it will cost you.&lt;/p&gt;&#xA;&lt;p&gt;Now that you have a storage zone, please navigate to the FTP credentials page, and have them ready for later.&lt;/p&gt;&#xA;&lt;h2 id=&#34;creating-a-pull-zone&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#creating-a-pull-zone&#34;&gt;Creating a Pull Zone&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The next step is to create a pull zone. The pull zone is what will be used to serve the static site. To do this, log into the Bunny.net dashboard, click the Pull Zones tab, and follow the steps to create a new pull zone. As with the storage zone, you should name it something memorable. You can also select the regions from which the CDN serves your data. The pull zone must be configured to use the storage zone you created earlier as the &amp;#34;origin&amp;#34;. You can also enable a custom domain for your site. If you do, you will need to add a CNAME record to your DNS provider that points to the Bunny.net pull zone and add it to the pull zone in the Bunny.net dashboard.&lt;/p&gt;&#xA;&lt;h2 id=&#34;uploading-the-site&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#uploading-the-site&#34;&gt;Uploading the Site&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now that you have a storage zone and a pull zone, you can upload your site to the storage zone. You can do this using the FTP credentials you created earlier. Here is an example of how to do this using &lt;a href=&#34;https://blog.gitea.com/hacking-on-gitea-actions/&#34; rel=&#34;nofollow&#34;&gt;Gitea Actions&lt;/a&gt;, but you can use any CI system you like.&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;You&amp;#39;ll need to set the &lt;code&gt;BUNNYCDN_FTP_HOST&lt;/code&gt; and &lt;code&gt;BUNNYCDN_FTP_USER&lt;/code&gt; secrets, they are the FTP host and user from the storage zone&amp;#39;s FTP credentials page. The &lt;code&gt;BUNNYCDN_FTP_PASSWORD&lt;/code&gt; is the password from the same page. The &lt;code&gt;public/&lt;/code&gt; directory is the directory that Hugo builds the site into. The &lt;code&gt;/&lt;/code&gt; is the root of the storage zone. The &lt;code&gt;-R&lt;/code&gt; flag tells lftp to mirror the directory recursively, and the &lt;code&gt;-v&lt;/code&gt; flag tells lftp to be verbose. Finally, you&amp;#39;ll also need to set the &lt;code&gt;BUNNYCDN_ZONE_ID&lt;/code&gt; and the &lt;code&gt;BUNNYCDN_API_KEY&lt;/code&gt; secrets with details you can get from the bunny.net dashboard, so that the last step in the workflow can clear the cache, and serve up the new blog posts.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;That&amp;#39;s it! You should now have a static site hosted on Bunny.net. You can now use the pull zone&amp;#39;s URL to access your site. If you enabled a custom domain, you can use that instead. You can also further configure the pull zone to enable caching, compression, and other features such as auto-https.&lt;/p&gt;&#xA;&lt;p&gt;Notes: Bunny.net is rebuilding its dashboard, so the steps in this post may not match the current dashboard, although the 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 certificates are an effective way to secure SSH server access. They can restrict users and the commands they can run, making them especially valuable for managing access to multiple servers. By using SSH certificates, server fingerprint validation becomes unnecessary since the certificates are signed by a Certificate Authority (CA) with the CA&amp;#39;s public key installed on the server. Vault is an excellent tool for managing SSH certificates, offering functionalities like issuing and revoking certificates, managing SSH keys, and providing audit logs.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Warning: This post describes a non-production setup of Vault. As such, it is not hardened with appropriate security measures, and it is not recommended to use this setup in production. You should use this for learning purposes on SSH CAs and Vault.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;install-vault&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#install-vault&#34;&gt;Install Vault&lt;span class=&#34;heading-hash&#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-ssh-certificate-authority&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#configure-ssh-certificate-authority&#34;&gt;Configure SSH Certificate Authority&lt;span class=&#34;heading-hash&#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;requesting-ssh-certificates&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#requesting-ssh-certificates&#34;&gt;Requesting SSH Certificates&lt;span class=&#34;heading-hash&#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;h2 id=&#34;conclusion&#34;&gt;&lt;a class=&#34;heading-link&#34; href=&#34;#conclusion&#34;&gt;Conclusion&lt;span class=&#34;heading-hash&#34;&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;This post covered configuring Vault to issue SSH certificates, setting up a remote server to accept these certificates, and streamlining the process of requesting SSH certificates. Use the knowledge from this post to enhance your environment&amp;#39;s security. Remember that this post describes a non-production setup of Vault and should be used for learning purposes only.&lt;/p&gt;&#xA;&lt;p&gt;Credits: The above post was written from knowledge and experience of using Vault. The instructions for docker configuration of vault are from the official Vault docker documentation.&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 to host web services. It can be used for more than just web services; it can also be used to run long-running tasks. I wanted to try a new way to run the Gitea Actions runner, and Fly.io would be interesting way to try out.&lt;/p&gt;&#xA;&lt;p&gt;Treat this as a proof of concept, I&amp;#39;m unsure if this is a good idea, but it&amp;#39;s fun to try out.&lt;/p&gt;&#xA;&lt;p&gt;To simplify things, I will run the runner in &amp;#34;host&amp;#34; mode, meaning that each job won&amp;#39;t be containerized but will run directly on the host. This was a decision made before getting started to limit the amount of debugging sorting out Docker in Docker.&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 that the runner can be registered and persist the registration token across restarts.&lt;/p&gt;&#xA;&lt;p&gt;Since there are no prebuilt Docker images (as of the time of publishing) 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;When running the container, the startup logic will check if the runner is already registered, and if not it will register it. The registration token is passed in as an environment variable. The runner will then be started.&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 really was a handful of lines to get a runner up and running. The runner is now running on Fly.io and can be used to run Gitea Actions. The only issue I ran into when setting this up, was that Fly.io will terminate apps if they run out of memory. This is a problem because what I was testing used a lot of memory. I ended up increasing the memory limit. Maybe a different hosting would handle OOMs differently, but I was pretty satisfied with the result. The blog you are reading right now is built using this runner.&lt;/p&gt;&#xA;</content:encoded>
    </item>
  </channel>
</rss>