<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>nicholas.cloud</title><link>https://nicholas.cloud/</link><description>Posts from Nicholas Whittaker</description><generator>Hugo 0.152.2</generator><language>en-us</language><lastBuildDate>Sat, 12 Jul 2025 16:48:01 +1000</lastBuildDate><image><url>https://nicholas.cloud/favicon_hu_e151497447b87e9d.png</url><title>nicholas.cloud</title><link>https://nicholas.cloud/</link><width>128</width><height>128</height></image><item><title>Pets have names, livestock is tagged</title><link>https://nicholas.cloud/blog/pets-have-names-livestock-is-tagged/</link><pubDate>Sat, 12 Jul 2025 16:48:01 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/pets-have-names-livestock-is-tagged/</guid><description>&lt;p&gt;There&amp;rsquo;s an oft-quoted phrase in the cloud/DevOps space.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Treat infrastructure as cattle, not pets.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To me, the advice is broad and emblematic for a lot of modern practices. Prefer disposable containers over long-lived hosts. Build infrastructure that scales horizontally to accomodate demand. Design architectures where components can be swapped out on the fly.&lt;/p&gt;
&lt;p&gt;I thought I&amp;rsquo;d recap some recent changes to how I provision and manage this blog - addressing a few places where hostnames were hardcoded for convenience.&lt;/p&gt;
&lt;p&gt;This blog runs on a DigitalOcean droplet managed with Ansible. The inventory of hosts for Ansible to manage is generated from querying my Tailscale tailnet.&lt;/p&gt;
&lt;p&gt;Previously, that inventory was a 1:1 mapping of machine names to hostnames. Hosts in an inventory can be grouped as well though. What if each Tailscale machine is tagged with the projects it hosts?&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/pets-have-names-livestock-is-tagged/tailscale-machines.png" alt="A web console listing several machines, each with tags corresponding to different software projects" loading="lazy" width="1516" height="660" /&gt;
&lt;/p&gt;
&lt;p&gt;Mapping each of these tags to a group in the Ansible needs only &lt;a href="https://github.com/nchlswhttkr/hosting/blob/07cd9a23575b92abf3115a9020676390cf7fad32/deploy/inventory.py"&gt;a small bit of wrangling with the inventory script&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;project_buildkite_uploader&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hosts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;gyro.tailbf155.ts.net.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;project_opentelemetry_collector&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hosts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;gyro.tailbf155.ts.net.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;project_vault&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hosts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;gyro.tailbf155.ts.net.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;project_blog&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hosts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;blog-bfab9eeb.tailbf155.ts.net.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;project_writefreely&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hosts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;boyd.tailbf155.ts.net.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;_meta&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now I can swap out the hardcoded &lt;code&gt;blog&lt;/code&gt; hostname, instead matching against &lt;em&gt;all&lt;/em&gt; hosts in the &lt;code&gt;project_blog&lt;/code&gt; group.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - name: Deploy Blog (https://nicholas.cloud/)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- hosts: blog
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt;&lt;span class="gi"&gt;+ hosts: project_blog
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a few more benefits in addressing this reliance on static hostnames:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It&amp;rsquo;s easier to support multiple hosts for a project&lt;/li&gt;
&lt;li&gt;Adding/removing hosts is done by tagging, rather than code changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s a similar change to the build pipeline for my blog&amp;rsquo;s content, which now gets &lt;a href="https://github.com/nchlswhttkr/website/blob/1e41ea007f60ea95d2e08dcc6eb69a3bf98260d3/.buildkite/deploy.sh"&gt;deployed to all tagged targets&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All in all, a nice little win for idle tinkering.&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/ansible">ansible</category><category domain="https://nicholas.cloud/tags/devops">devops</category><category domain="https://nicholas.cloud/tags/tailscale">tailscale</category></item><item><title>Addendum to my blogroll escapade</title><link>https://nicholas.cloud/blog/addendum-to-my-blogroll-escapade/</link><pubDate>Sun, 18 May 2025 20:24:04 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/addendum-to-my-blogroll-escapade/</guid><description>&lt;p&gt;Following on from &lt;a href="../committing-xml-horrors-to-style-my-blogroll/"&gt;setting up my blogroll&lt;/a&gt; last week, I&amp;rsquo;ve realised it doesn&amp;rsquo;t render as a pretty webpage in most web browsers. Serves me right for only testing in Firefox!&lt;/p&gt;
&lt;p&gt;To style the document, I wrote the XLST in an &lt;code&gt;xsl:stylesheet&lt;/code&gt; element with an &lt;code&gt;id=&amp;quot;blogroll&amp;quot;&lt;/code&gt; attribute. A processing instruction applies the stylesheet to the document.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&amp;lt;?xml-stylesheet type=&amp;#34;text/xsl&amp;#34; href=&amp;#34;#blogroll&amp;#34;?&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;XML processors will look for the stylesheet by its ID, &lt;code&gt;blogroll&lt;/code&gt; in this case. But when looking at an element, how do know if one of its attributes is an ID? The XML spec &lt;a href="https://www.w3.org/TR/xslt20/#embedded"&gt;goes into detail about this&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In order for such an attribute value to be used as a fragment identifier in a URI, the XDM attribute node must generally have the is-id property [&amp;hellip;]. This property will typically be set if the attribute is defined in a DTD as being of type ID, or if is defined in a schema as being of type xs:ID.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Turns out, most processors need the ID attribute to be explicitly labelled. So I added the necessary document type declaration (DTD) explicitly specifying the &lt;code&gt;ID&lt;/code&gt; attribute for &lt;code&gt;xsl:stylesheet&lt;/code&gt; elements - in this case called &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE OPML [&amp;lt;!ATTLIST xsl:stylesheet id ID #REQUIRED&amp;gt;&lt;/span&gt;]&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In Firefox&amp;rsquo;s case, the browser seems to assume &lt;code&gt;id&lt;/code&gt; will be the ID attribute. As other browsers show though, that isn&amp;rsquo;t a safe assumption.&lt;/p&gt;
&lt;p&gt;Anyway with &lt;a href="https://github.com/nchlswhttkr/website/commit/2570f66992e5bc7b1f31b6ba0a507e2449329245"&gt;a fix now in place&lt;/a&gt;, I hope you enjoy perusing my blogroll!&lt;/p&gt;</description></item><item><title>Committing XML horrors to style my blogroll</title><link>https://nicholas.cloud/blog/committing-xml-horrors-to-style-my-blogroll/</link><pubDate>Sat, 10 May 2025 20:25:13 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/committing-xml-horrors-to-style-my-blogroll/</guid><description>&lt;p&gt;For a while, I&amp;rsquo;ve provided a list of suggested blog/feeds on my website in a blogroll.&lt;/p&gt;
&lt;p&gt;This blogroll takes the form of an OPML file that RSS readers can import. It&amp;rsquo;s pretty neat, but making it browser-friendly has always been on my wishlist.&lt;/p&gt;
&lt;p&gt;So it was pretty cool to stumble across XSLT - a means for transforming XML documents.&lt;/p&gt;
&lt;p&gt;In this case, XSLT offers a means to transform the OPML blogroll into HTML that a browser can render and style. I got the inspiration from Ruben Schade and his guide on &lt;a href="https://rubenerd.com/styling-opml-and-rss-with-xslt/"&gt;styling OPML with XSLT&lt;/a&gt;, which he used on his own blogroll.&lt;/p&gt;
&lt;p&gt;Applying this to my blogroll gave me the best of both worlds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The blogroll resembles my website&amp;rsquo;s regular HTML pages in a web browser&lt;/li&gt;
&lt;li&gt;The file itself is still valid OPML which RSS readers can import&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As an additional challenge, I wanted to continue generating the blogroll &lt;em&gt;dynamically&lt;/em&gt; in Hugo rather than hand-curating the XML. I&amp;rsquo;ve been doing this with a custom output format, listing feeds in the page&amp;rsquo;s metadata.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Blogroll&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;opml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;feeds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Lachlan Jacob&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://blog.etopiei.com/feed.php&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://blog.etopiei.com/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Generating these OPML/XSLT documents dynamically came with another benefit, as I could avoid duplicating the base HTML template used for the rest of my website. There were a few roadbumps, but they were pretty negligible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XML is stricter with self-closing elements, like &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Common HTML entities like &lt;code&gt;&amp;amp;amp;&lt;/code&gt; need to be defined explicitly or removed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Aside from that, the main challenge was bundling the XSLT into the existing document. Linking it from a static file is an option, but I wanted to include templating logic in the XSLT as well. Fortunately, XLST can be &lt;a href="https://www.w3.org/TR/xslt20/#embedded"&gt;embedded and referenced from within an existing OPML document&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34;?&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&amp;lt;?xml-stylesheet type=&amp;#34;text/xsl&amp;#34; href=&amp;#34;#blogroll&amp;#34;?&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;opml&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;2.0&amp;#34;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;&amp;lt;!-- OPML head --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;&amp;lt;!-- OPML body --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;xsl:stylesheet&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;blogroll&amp;#34;&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;2.0&amp;#34;&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsl=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;http://www.w3.org/1999/XSL/Transform&amp;#34;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;&amp;lt;!-- XSLT transformation --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/xsl:stylesheet&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/opml&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that addition my blogroll now renders in-browser, and looks like most pages of my website!&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/committing-xml-horrors-to-style-my-blogroll/blogroll.png" alt="A screenshot of my website, listing a number of blogs to subscribe to" loading="lazy" width="1490" height="1529" /&gt;
&lt;/p&gt;
&lt;p&gt;As a last step, I did also need to add an Nginx directive for browsers to treat the OPML file as a XML document.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Serve OPML files as XML to force in-browser viewing
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/blogroll.opml&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;default_type&lt;/span&gt; &lt;span class="s"&gt;application/xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So if you&amp;rsquo;re interested in reading or following other interesting feeds now, have a read of my &lt;a href="https://nicholas.cloud/blogroll.opml"&gt;blogroll&lt;/a&gt;!&lt;/p&gt;</description></item><item><title>Using Buildkite OIDC with Hashicorp Vault</title><link>https://nicholas.cloud/blog/using-buildkite-oidc-with-hashicorp-vault/</link><pubDate>Tue, 19 Sep 2023 17:54:23 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/using-buildkite-oidc-with-hashicorp-vault/</guid><description>&lt;p&gt;Earlier this year, Buildkite announced &lt;a href="https://buildkite.com/changelog/197-oidc-support-is-now-available"&gt;support for OpenID Connect tokens&lt;/a&gt;. Briefly, a Buildkite agent can request a signed JWT (JSON Web Token) from Buildkite representing details (claims) about its current job. This JWT can then be used to authenticate with systems that accept it.&lt;/p&gt;
&lt;p&gt;For Hashicorp Vault, services typically authenticate using &lt;a href="https://developer.hashicorp.com/vault/docs/auth/approle"&gt;the AppRole method&lt;/a&gt; with a senstive set of credentials. It&amp;rsquo;s fine to use this flow on a Buildkite agent to access Vault secrets, but the credentials for this are long-lived.&lt;/p&gt;
&lt;p&gt;The new OIDC flow removes to need to manage these long-lived credentials, and also makes it possible to craft fine-grained policies for a Buildkite agent without requiring multiple sets of login credentials!&lt;/p&gt;
&lt;p&gt;I currently run my general-purpose CI workloads on &lt;a href="https://buildkite.com/docs/agent/v3/elastic-ci-aws/elastic-ci-stack-overview"&gt;a single autoscaling cluster of Buildkite agents&lt;/a&gt;. This lean approach is great for me since I don&amp;rsquo;t have the need to scale or segment my CI setup like a larger organisation might.&lt;/p&gt;
&lt;p&gt;There are shortcomings though to all my pipelines sharing these agents. Each pipeline may have its own secrets, but agents must have access to &lt;em&gt;all of them&lt;/em&gt; because they could run jobs from &lt;em&gt;any&lt;/em&gt; pipeline. Even if secrets follow least privilege practices, an agent&amp;rsquo;s overly broad access grows with each new secret.&lt;/p&gt;
&lt;p&gt;As an example, here&amp;rsquo;s a policy that allows secrets in a key-value store matching the path &lt;code&gt;buildkite/*&lt;/code&gt; to be read.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-terraform" data-lang="terraform"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vault_policy&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;buildkite_agent&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;buildkite-agent&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-POLICY&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; path &amp;#34;kv/data/buildkite/*&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; capabilities = [&amp;#34;read&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="o"&gt;POLICY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An authenticated agent running a job for the &lt;code&gt;chocolate&lt;/code&gt; pipeline can read the &lt;code&gt;buildkite/chocolate&lt;/code&gt; secret, but there&amp;rsquo;s nothing stopping it from &lt;em&gt;also&lt;/em&gt; reading the &lt;code&gt;buildkite/strawberry&lt;/code&gt; secret.&lt;/p&gt;
&lt;p&gt;Without dedicated agents for each pipeline, jobs have unchecked access to all &lt;code&gt;buildkite/*&lt;/code&gt; secrets. Setting up OIDC authentication for my agents instead provides an alternative method to enforce stricter policies and limit access.&lt;/p&gt;
&lt;p&gt;First off, we need to create the Vault backend that will accept the JWTs obtained from Buildkite.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-terraform" data-lang="terraform"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vault_jwt_auth_backend&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;buildkite&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;buildkite&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;oidc_discovery_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://agent.buildkite.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A new role for this backend dictates the requirements to log in with a JWT. Setting the &lt;code&gt;bound_audiences&lt;/code&gt; and &lt;code&gt;bound_claims&lt;/code&gt; is important to ensure the JWTs are intended for my Vault instance and that they belong to my Buildkite organisation.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-terraform" data-lang="terraform"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vault_jwt_auth_backend_role&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;buildkite_agent&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vault_jwt_auth_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buildkite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;role_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;buildkite-agent&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;token_policies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;default&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vault_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buildkite_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;role_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;jwt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;user_claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sub&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;bound_audiences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;vault.nicholas.cloud&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;bound_claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;organization_slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;nchlswhttkr&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;claim_mappings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;pipeline_slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pipeline_slug&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;claim_mappings&lt;/code&gt; block above specifies metadata to be copied from the JWT&amp;rsquo;s claims, in this case the &lt;code&gt;pipeline_slug&lt;/code&gt; denoting the pipeline the job belongs to.&lt;/p&gt;
&lt;p&gt;The metadata can be referenced in a role&amp;rsquo;s policies using &lt;a href="https://developer.hashicorp.com/vault/docs/concepts/policies#templated-policies"&gt;template syntax&lt;/a&gt;, which we can use to limit the &lt;code&gt;buildkite-agent&lt;/code&gt; policy from before.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; resource &amp;#34;vault_policy&amp;#34; &amp;#34;buildkite_agent&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; name = &amp;#34;buildkite-agent&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; policy = &amp;lt;&amp;lt;-POLICY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- path &amp;#34;kv/data/buildkite/*&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt;&lt;span class="gi"&gt;+ path &amp;#34;kv/data/buildkite/{{identity.entity.aliases.${vault_jwt_auth_backend.buildkite.accessor}.metadata.pipeline_slug}}&amp;#34; {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;&lt;/span&gt; capabilities = [&amp;#34;read&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; POLICY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The template itself is a little cumbersome, but the resulting policy works like a charm. For a job to read the &lt;code&gt;buildkite/chocolate&lt;/code&gt; secret now, it must originate from the &lt;code&gt;chocolate&lt;/code&gt; pipeline.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;kv/data/buildkite/{{identity.entity.aliases.auth_jwt_e9c0606b.metadata.pipeline_slug}}&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; capabilities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;read&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Logging into Vault now requires only the short-lived token from Buildkite!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vault write auth/buildkite/login &lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;buildkite-agent &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; &lt;span class="nv"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;buildkite-agent oidc request-token --audience vault.nicholas.cloud&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description><category domain="https://nicholas.cloud/tags/buildkite">buildkite</category><category domain="https://nicholas.cloud/tags/hashicorp-vault">hashicorp-vault</category></item><item><title>Signing Terraform provider releases with a local Buildkite agent</title><link>https://nicholas.cloud/blog/signing-terraform-provider-releases-with-a-local-buildkite-agent/</link><pubDate>Mon, 17 Jul 2023 10:54:19 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/signing-terraform-provider-releases-with-a-local-buildkite-agent/</guid><description>&lt;p&gt;For a while now, I&amp;rsquo;ve built and published &lt;a href="https://github.com/nchlswhttkr/terraform-provider-pass/"&gt;my own Terraform provider&lt;/a&gt; for retrieving secrets from &lt;a href="https://passwordstore.org/"&gt;a pass store&lt;/a&gt;. One of the requirements to publish a Terraform provider is that every release must be signed with a GPG key.&lt;/p&gt;
&lt;p&gt;I have a Buildkite pipeline to build and publish these releases to GitHub. A step in this pipeline has access to a private key for signing, but it&amp;rsquo;s a different key from the one I use on my own machine. I consider the latter too sensitive to expose freely to my Buildkite agents.&lt;/p&gt;
&lt;p&gt;With that said, managing a second key &lt;em&gt;just&lt;/em&gt; to publish my Terraform provider is quite irksome when it has no other use for me. However, it&amp;rsquo;s unfortunately necessary if I don&amp;rsquo;t want to expose my regular key to my CI environment.&lt;/p&gt;
&lt;p&gt;But if the worry is around exposing a secret to Buildkite agents running &lt;em&gt;outside&lt;/em&gt; my machine, why not introduce an agent that runs &lt;em&gt;specifically&lt;/em&gt; on my machine?&lt;/p&gt;
&lt;p&gt;Buildkite agents are commonly run on easy-to-reproduce hosts, such as virtual machines or containers in a cluster. They &lt;em&gt;can&lt;/em&gt; be run on any host with a supported OS/architecture though, which includes my daily driver MacBook. Running an agent locally is doable, but there are also good reasons why it &lt;em&gt;usually&lt;/em&gt; isn&amp;rsquo;t suitable.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developer environments tend be volatile, with tools and dependencies that might be missing or on an incompatible version&lt;/li&gt;
&lt;li&gt;They tend to have poor availability, only being online so long as the agent is running and the machine is not shut down&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Given I&amp;rsquo;ll only be needing the agent infrequently when I&amp;rsquo;m making a release, and it only needs to sign the release on GitHub, these concerns are minimal. Time to get cracking!&lt;/p&gt;
&lt;p&gt;With &lt;a href="https://github.com/nchlswhttkr/terraform-provider-pass/commit/dd20738dfd522a1c19cc2278357fae6822769c9c"&gt;the old build script cleaned up and a new step to handle the release signing&lt;/a&gt;, the only key change I need to make is to target the Buildkite agent running locally rather than using the default queue.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - label: &amp;#34;:github: Sign and publish release&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; key: sign-release
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; depends_on: create-release
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command: .buildkite/sign-release.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if: build.env(&amp;#34;BUILDKITE_TAG&amp;#34;) =~ /^v\d/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ agents:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ queue: nchlswhttkr
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When a build runs the brunt of the work is done on default agents, but the signing step targets the agent with matching tags that I&amp;rsquo;m running locally.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/signing-terraform-provider-releases-with-a-local-buildkite-agent/build.png" alt="A set of steps in Buildkite building a number of Golang binaries and publishing them to GitHub, the final signing step is running on a unique agent" loading="lazy" width="2109" height="1799" /&gt;
&lt;/p&gt;
&lt;p&gt;Locally, I can see the Buildkite agent running the job.&lt;/p&gt;
&lt;p&gt;
&lt;img src="agent.png" alt="Logs from a Buildkite agent starting and successfully running a job" loading="lazy" width="1778" height="1066" /&gt;
&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m prompted to unlock my GPG key by the pinentry program I wrote for myself.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/signing-terraform-provider-releases-with-a-local-buildkite-agent/pinentry.png" alt="A macOS Touch ID prompt to unlock a GPG key" loading="lazy" width="740" height="828" /&gt;
&lt;/p&gt;
&lt;p&gt;Finally, the signed checksum is uploaded to the GitHub release. The new provider version can now be downloaded and used by Terraform!&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/signing-terraform-provider-releases-with-a-local-buildkite-agent/release.png" alt="A release on GitHub with a number of files attached, including a signature file" loading="lazy" width="1698" height="1003" /&gt;
&lt;/p&gt;
&lt;p&gt;With signing on the local agent working, I can do away with the second key I needed before. All of my releases can now be signed with my regular GPG key, and better yet without compromising on how I limit access to it!&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/buildkite">buildkite</category></item><item><title>My Apple Watch, six months in</title><link>https://nicholas.cloud/blog/my-apple-watch-six-months-in/</link><pubDate>Sun, 16 Jul 2023 22:24:24 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/my-apple-watch-six-months-in/</guid><description>&lt;p&gt;Since late September, I&amp;rsquo;ve been working to establish an exercise routine as I improve my fitness. One of my decisions in that journey has been to buy a smart watch, and having just ticked over six months since purchase I thought it would be an opportune time to reflect on some of my highlights.&lt;/p&gt;
&lt;p&gt;Seeing as I predominantly use Apple devices already and wanted to avoid any great deal of setup, I opted for an Apple Watch. They&amp;rsquo;re still pricier than a nice Fitbit or Garmin watch, but with the recent release of the more afforable Apple Watch SE, I was happy taking the plunge. While it doesn&amp;rsquo;t have all the features of the flagship watches, it has essentials for me like fall detection and a battery life that can easily last a full day out.&lt;/p&gt;
&lt;p&gt;With the watch in hand and the sunk cost as a motivation to be active, I&amp;rsquo;ve been using it to record my workouts and my daily activity. All the data is synced across to my phone, visible in the Health app alongside a host of other derived and self-reported measures like resting heart rate and my weight. I try not to focus on the day-to-day numbers too much, but seeing where I&amp;rsquo;m trending is excellent motivation to continue my work.&lt;/p&gt;
&lt;p&gt;Recording activities has also meant I can use Strava to share my activities with my friends, which in turns drives me to keep up my habits. I&amp;rsquo;ve paid for an annual membership, which has been useful for setting myself weekly goals and identifying how hard I&amp;rsquo;ve been pushing myself.&lt;/p&gt;
&lt;h2 id="riding"&gt;Riding&lt;/h2&gt;
&lt;p&gt;Late last year I started riding my bike again, looking for more intense activity than just a brisk walk. I&amp;rsquo;m fortunate to live close to the bay trail along Melbourne&amp;rsquo;s east coast, so I worked myself up travelling further and further along the coast until I could reach the CBD or go down to Mordialloc. Weekends have also been a good opportunity to go on longer rides!&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/2023-04-29.jpg" alt="Statistics from a Strava ride overlaid on a sculpture of arched rainbow pipes, 56.8km in 2 hours 40 minutes" loading="lazy" width="957" height="717" /&gt;
&lt;/p&gt;
&lt;p&gt;With these more intense rides, it&amp;rsquo;s been useful to see how much I&amp;rsquo;m exerting myself on my watch. I&amp;rsquo;ve actually switched over the default display I use for riding to one that shows my heart rate and Apple&amp;rsquo;s approximation of my zones.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/workout.png" alt="A watch face showing a coloured gradient for heart rate zones, currently in zone 2 with 136 BPM and 131 BPM average so far on a ride" loading="lazy" width="368" height="448" /&gt;
&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m working towards completing the 100km ride for &lt;a href="https://bicyclenetwork.com.au/rides-and-events/around-the-bay/"&gt;Around The Bay&lt;/a&gt; in October as well, and the group training rides I&amp;rsquo;ve done so far have been good both for the exercise and the social coffee afterwards.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/2023-06-10.jpg" alt="Statistics from a Strava ride overlaid on hot chcolate in a mug, 36.7km in 1 hour 44 minutes" loading="lazy" width="957" height="717" /&gt;
&lt;/p&gt;
&lt;p&gt;Riding at higher speeds and as part of a group is still quite daunting, but the folks at Bicycle Network organising the rides have been welcoming to newcomers like myself. It feels pretty awesome to go zooming along Beach Road bright and early on a Saturday!&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/2023-07-15.jpg" alt="Statistics from a Strava ride overlaid on a large group of cyclists waving, 44.9km in 1 hour 46 minutes" loading="lazy" width="957" height="1266" /&gt;
&lt;/p&gt;
&lt;h2 id="running"&gt;Running&lt;/h2&gt;
&lt;p&gt;Back in May, I went on a holiday to Phillip Island to compete in a lawn bowls tournament. I had hoped to borrow a bike in Cowes, but with the winter weather and no gear I was content wandering the town. The day after arriving though I had some slack time before dinner, so I decided to go for a run along the beach to close my rings for the day.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/2023-05-01.jpg" alt="Statistics from a Strava run overlaid on an ocean pier backdrop, 2.3km in 17 minutes 34 seconds" loading="lazy" width="957" height="717" /&gt;
&lt;/p&gt;
&lt;p&gt;That first run was pretty unpleasant, and after a very overeager starting pace I was glad that I&amp;rsquo;d opted to walk after the first kilometre. The second leg of the run was also grueling, particularly with a couple of hills on the way back to my hotel room. I was glad to be done and collapsed into my bed when I got back. The parma for dinner that night was particularly satisfying.&lt;/p&gt;
&lt;p&gt;Luckily my legs were feeling alright for the tournament the next day, although I couldn&amp;rsquo;t handle the wind and was eliminated in the group stage.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/2023-05-03.jpg" alt="Statistics from a Strava run overlaid on a beach backdrop, 2.5km in 19 minutes 27 seconds" loading="lazy" width="957" height="717" /&gt;
&lt;/p&gt;
&lt;p&gt;On my last day in Cowes, I went for another run before checking out. Although it was also pretty tough, extending the distance to run a little further and see more along the coast made it more enjoyable.&lt;/p&gt;
&lt;p&gt;Since then, I&amp;rsquo;ve tried to run at least twice a week. My route varies, but I&amp;rsquo;ve found a flat-ish 2.5km circuit around my local park where I can focus on balancing my pace against my exertion. My exercise group will sometimes do weekend runs, mixing distance with exercises like stair climbing.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still yet to hit 5km, but most recently a double lap of Caulfield Park was a good progress check. Running with my friend helped me push myself, and being able to run twice the distance I did two months ago (with a 5 minute rest!) is a proud accomplishment for me.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/2023-07-09.jpg" alt="A circuit of a park on Strava, 4.8km in 29 minutes 5 seconds" loading="lazy" width="957" height="957" /&gt;
&lt;/p&gt;
&lt;p&gt;As with cycling, it&amp;rsquo;s been insightful to see the analysis my watch and Strava provide. Running is much more intense for me, so being able to see the changes measured and visualised is in itself rewarding. My recovery has improved, my heart rate climbs at a slower rate than before, and my pace over the same course each week has improved dramatically.&lt;/p&gt;
&lt;p&gt;Seeing the slow trend up as I run more each week feels great too.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/running.jpg" alt="A graph of weekly distance covered, consistently climbing from 4km to 10km over 8 weeks" loading="lazy" width="1125" height="734" /&gt;
&lt;/p&gt;
&lt;h1 id="closing-my-rings"&gt;Closing my rings&lt;/h1&gt;
&lt;p&gt;Lastly, being able to able to see a general measure of my acitivty visualised with the three rings Apple uses has itself been a good driver to keep myself active. Even if I&amp;rsquo;m having a day off, it feels satisfying to close my rings with a walk so I know I&amp;rsquo;m not spending the entire day inside sitting idle at my desk.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/activity.jpg" alt="" loading="lazy" width="1125" height="1260" /&gt;
&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also pretty cool that I can scroll back through my activity and see six months of progress. I might have been sick from time to time, but at the end of the day I&amp;rsquo;ve built a strong habit since I got my watch.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/my-apple-watch-six-months-in/rings.png" alt="" loading="lazy" width="1125" height="8075" /&gt;
&lt;/p&gt;</description></item><item><title>Fixing a slow Tailscale SSH connection</title><link>https://nicholas.cloud/blog/fixing-a-slow-tailscale-ssh-connection/</link><pubDate>Sun, 25 Sep 2022 22:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/fixing-a-slow-tailscale-ssh-connection/</guid><description>&lt;p&gt;I&amp;rsquo;ve been trying out &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; recently to simplify networking between my devices. With the beta launch of &lt;a href="https://tailscale.com/blog/tailscale-ssh/"&gt;Tailscale SSH&lt;/a&gt; offering the ability to connect to my DigitalOcean droplet without SSH keypairs, I was eager to incorporate it into my setup.&lt;/p&gt;
&lt;p&gt;Said setup is a matter for another time, but with Tailscale SSH enabled for my droplet I was able to remote in with a plain &lt;code&gt;ssh nicholas@gandra-dee&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;However, there was a visible half-second delay when entering commands. While not a dealbreaker, it made each session a frustrating experience!&lt;/p&gt;
&lt;p&gt;I started off by getting a gauge of the problem. How responsive was the connection? It wasn&amp;rsquo;t &lt;em&gt;too&lt;/em&gt; slow, but it wasn&amp;rsquo;t fast either.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ping -qc &lt;span class="m"&gt;10&lt;/span&gt; gandra-dee
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PING gandra-dee.nchlswhttkr.github.beta.tailscale.net &lt;span class="o"&gt;(&lt;/span&gt;100.79.138.83&lt;span class="o"&gt;)&lt;/span&gt;: &lt;span class="m"&gt;56&lt;/span&gt; data bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;--- gandra-dee.nchlswhttkr.github.beta.tailscale.net ping statistics ---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;10&lt;/span&gt; packets transmitted, &lt;span class="m"&gt;10&lt;/span&gt; packets received, 0.0% packet loss
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;round-trip min/avg/max/stddev &lt;span class="o"&gt;=&lt;/span&gt; 353.175/535.999/1690.879/398.688 ms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fortunately in this debugging adventure, I &lt;em&gt;did&lt;/em&gt; have a suspect: I&amp;rsquo;d recently added a firewall on the DigitalOcean side, so there was a good chance I&amp;rsquo;d missed a rule along the way. It was strange that I could still connect to the droplet despite the firewall likely blocking some of my requests.&lt;/p&gt;
&lt;p&gt;Glancing through &lt;a href="https://tailscale.com/kb/1082/firewall-ports/"&gt;the Tailscale documentation on firewall rules&lt;/a&gt; revealed a likely cause for the slower than expected connection.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;However, when both devices are on difficult networks Tailscale may not be able to connect devices peer-to-peer. You’ll still be able to send and receive traffic, thanks to our secure relays (DERP), but the relayed connection won’t be as fast as a peer-to-peer one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bingo. A quick &lt;code&gt;tailscale status&lt;/code&gt; confirmed the connection was over a relay.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ tailscale status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.79.138.83 gandra-dee nchlswhttkr@ linux -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.82.154.68 flugelhorn nchlswhttkr@ iOS offline
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.88.173.96 phoenix nchlswhttkr@ macOS active&lt;span class="p"&gt;;&lt;/span&gt; relay &lt;span class="s2"&gt;&amp;#34;syd&amp;#34;&lt;/span&gt;, tx &lt;span class="m"&gt;1764024&lt;/span&gt; rx &lt;span class="m"&gt;22460936&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While I&amp;rsquo;d remembered to add a rule to &lt;code&gt;ufw&lt;/code&gt; on the droplet to allow Tailscale traffic, the firewall rules on the DigitalOcean side were blocking traffic beyond that. Even though Tailscale couldn&amp;rsquo;t connect over its typical port it was able to fall back to the HTTPS relay, which the firewall allowed.&lt;/p&gt;
&lt;p&gt;The fix is thankfully quick, adding a couple of firewall exceptions with Terraform.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-terraform" data-lang="terraform"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Allow inbound Tailscale requests
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt;&lt;span class="nx"&gt;inbound_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;udp&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;port_range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;41641&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;source_addresses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.0.0.0/0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;::/0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="c1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Allow outbound Tailscale requests
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt;&lt;span class="nx"&gt;outbound_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;udp&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;port_range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;41641&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;destination_addresses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0.0.0.0/0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;::/0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Reconnecting to the instance now after applying the change, the remote session starts noticeably faster! The response time to my typing is immediate, and my calm is restored. Running &lt;code&gt;tailscale status&lt;/code&gt; again confirms the problem is fixed. (&lt;em&gt;Omitting my IP, of course&lt;/em&gt;)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ tailscale status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.79.138.83 gandra-dee nchlswhttkr@ linux -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.82.154.68 flugelhorn nchlswhttkr@ iOS offline
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.88.173.96 phoenix nchlswhttkr@ macOS active&lt;span class="p"&gt;;&lt;/span&gt; direct 192.0.2.0:22697, tx &lt;span class="m"&gt;1794276&lt;/span&gt; rx &lt;span class="m"&gt;22486396&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What&amp;rsquo;s the connection like now?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ping -qc &lt;span class="m"&gt;10&lt;/span&gt; gandra-dee
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PING gandra-dee.nchlswhttkr.github.beta.tailscale.net &lt;span class="o"&gt;(&lt;/span&gt;100.79.138.83&lt;span class="o"&gt;)&lt;/span&gt;: &lt;span class="m"&gt;56&lt;/span&gt; data bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;--- gandra-dee.nchlswhttkr.github.beta.tailscale.net ping statistics ---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;10&lt;/span&gt; packets transmitted, &lt;span class="m"&gt;10&lt;/span&gt; packets received, 0.0% packet loss
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;round-trip min/avg/max/stddev &lt;span class="o"&gt;=&lt;/span&gt; 89.109/121.486/405.295/94.605 ms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bliss.&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/tailscale">tailscale</category></item><item><title>Happy anniversary to Journey!</title><link>https://nicholas.cloud/blog/happy-anniversary-to-journey/</link><pubDate>Mon, 14 Mar 2022 16:00:00 +1100</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/happy-anniversary-to-journey/</guid><description>&lt;p&gt;As I&amp;rsquo;ve just found learned, today is the anniversary of Journey&amp;rsquo;s release ten years ago. It&amp;rsquo;s a video game from thatgamecompany where you guide a mysterious wanderer in robes on a pilgrimage to climb a distant mountain.&lt;/p&gt;
&lt;p&gt;Journey is one of the games that&amp;rsquo;s stuck with me a lot since I&amp;rsquo;ve played it.&lt;/p&gt;
&lt;p&gt;Largely a single player experience, it&amp;rsquo;s possible to encounter another lone human-controlled character on your travel. Communication is limited to a single button for chirping, so you and your partner must figure out your own language as you adventure together. Short repeated chittering might indicate excitement or urgency, whereas longer humming leans closer to a conversation.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also the first game I went for completion in, earning the white robe for my character by collecting all the hidden sigils. Even then, I&amp;rsquo;ve still replayed it from time to time in the years since.&lt;/p&gt;
&lt;div class="youtube"&gt;
&lt;span class="center-text"&gt;
&lt;em&gt;JOURNEYQUEST&lt;/em&gt;
by
&lt;a
href="https://youtube.com/channel/UCbDUjJuv1kfyDOFTwExxhJQ"
target="_blank"
rel="noopener noreferrer"
&gt;
NightMargin
&lt;/a&gt;
&lt;/span&gt;
&lt;a
href="https://youtube.com/watch?v=DRui67tPyMk"
target="_blank"
rel="noopener noreferrer"
&gt;
&lt;img
loading="lazy"
alt="Thumbnail for YouTube video JOURNEYQUEST"
width="480"
height="270"
src='https://nicholas.cloud/hqdefault_16598314823177177991_hu_e15c602b996d9b5f.jpg'
/&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;Thinking back to when I first saw Journey on the video game review show &lt;em&gt;Good Game&lt;/em&gt;, I&amp;rsquo;m reminded of how I was a decade ago. I used to avidly tune into the ABC on Tuesday evenings to see the new game reviews. These days, Hex has moved on and Bajo streams on Twitch on those same Tuesday evenings.&lt;/p&gt;
&lt;p&gt;In the end, I&amp;rsquo;m left with memories of Journey that I hope will stay with me for a long time. I&amp;rsquo;ve got the soundtrack from Austin Wintory on vinyl and digital, and I look forwrad to playing it again one day.&lt;/p&gt;
&lt;a href="https://austinwintory.bandcamp.com/album/journey"&gt;
Listen to Journey, by Austin Wintory on Bandcamp
&lt;/a&gt;</description><category domain="https://nicholas.cloud/tags/gaming">gaming</category></item><item><title>A good start for October</title><link>https://nicholas.cloud/blog/a-good-start-for-october/</link><pubDate>Fri, 01 Oct 2021 16:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/a-good-start-for-october/</guid><description>&lt;p&gt;Somehow, we&amp;rsquo;re now into the final quarter of the year. Here&amp;rsquo;s a few things that interest me, and may interest you!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.digitalocean.com/blog/hacktoberfest-is-back-2021/"&gt;Hacktoberfest is back for another year&lt;/a&gt;, hopefully the controls they introduced after the terrible spam-fest of 2020 will avoid creating too much work for maintainers. It&amp;rsquo;s nice to see that they&amp;rsquo;re making merge requests on GitLab eligible too, and highlighting financial contributions as a way to support open source software.&lt;/p&gt;
&lt;p&gt;A friend of mine in the UK is running &lt;a href="https://www.kickstarter.com/projects/peachsage/cute-pikachu-pins"&gt;a Kickstarter campaign to sell enamel Pikachu pins&lt;/a&gt;, if pins are your thing.&lt;/p&gt;
&lt;p&gt;Today is also &lt;a href="https://isitbandcampfriday.com"&gt;Bandcamp Friday&lt;/a&gt;, where Bandcamp waives their service fee for sales. It&amp;rsquo;s an excellent opportunity to support smaller/local artists and independent labels.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s five albums I&amp;rsquo;ve been listening to recently.&lt;/p&gt;
&lt;a href="https://nadjenoordhuis.bandcamp.com/album/indigo"&gt;
Listen to Indigo, by Nadje Noordhuis &amp;#43; James Shipp on Bandcamp
&lt;/a&gt;
&lt;p&gt;I saw Nadje &lt;a href="https://nicholas.cloud/blog/live-music-has-returned-to-melbourne/"&gt;perform with Luke Howard&lt;/a&gt; when we weren&amp;rsquo;t in lockdown earlier this year. The world needs more gentle trumpet and flugel soloists, her sound is honey for my ears.&lt;/p&gt;
&lt;a href="https://beastlykeysbrad.bandcamp.com/album/the-samba-and-crunk-ep"&gt;
Listen to THE SAMBA AND CRUNK EP, by brad bellard on Bandcamp
&lt;/a&gt;
&lt;p&gt;Brad&amp;rsquo;s a Melbourne-based funk keys player, he &lt;a href="https://www.twitch.tv/beastlykeysbrad"&gt;livestreams a few times each week on Twitch&lt;/a&gt;. In fact, he&amp;rsquo;s live as I write this!&lt;/p&gt;
&lt;a href="https://hellosatellites.bandcamp.com/album/theres-a-field"&gt;
Listen to There&amp;amp;#39;s a Field, by Hello Satellites on Bandcamp
&lt;/a&gt;
&lt;p&gt;Hello Satellites was &lt;a href="https://www.instagram.com/p/CQdaEO9jGkJ/"&gt;one of the performances I attended at Tempo Rubato in Brunswick&lt;/a&gt; before the most recent bout of lockdowns. First time I&amp;rsquo;ve seen a harp live too!&lt;/p&gt;
&lt;a href="https://jordanrakei.bandcamp.com/album/what-we-call-life"&gt;
Listen to What We Call Life, by Jordan Rakei on Bandcamp
&lt;/a&gt;
&lt;p&gt;Jordan Rakei&amp;rsquo;s voice is magical, and his new album sounds incredible. I&amp;rsquo;ve been thoroughly enjoying it this last week.&lt;/p&gt;
&lt;a href="https://theokatzman.bandcamp.com/album/my-heart-is-live-in-berlin"&gt;
Listen to My Heart is Live in Berlin, by Theo Katzman on Bandcamp
&lt;/a&gt;
&lt;p&gt;I love him on Vulfpeck and I love him as a solo artist, Theo is a powerhouse with his voice and guitar. He&amp;rsquo;s got three great albums under his belt now, and this live album is great introduction in my opinion.&lt;/p&gt;
&lt;p&gt;Happy listening!&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/music">music</category></item><item><title>A suspicious email from Cloudflare</title><link>https://nicholas.cloud/blog/a-suspicious-email-from-cloudflare/</link><pubDate>Thu, 23 Sep 2021 20:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/a-suspicious-email-from-cloudflare/</guid><description>&lt;p&gt;I received an email from Cloudflare today about an account that had been created with my email address. Going by the details and the headers, it certainly seemed to have originated from Cloudflare. It looked like someone else had signed up with my details.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/a-suspicious-email-from-cloudflare/1.png" alt="An email from Cloudflare with an account verification link reading &amp;ldquo;Hi. To complete your sign up, please verify your email. Thank you, Cloudflare Team&amp;rdquo;" loading="lazy" width="908" height="584" /&gt;
&lt;/p&gt;
&lt;p&gt;Now, it&amp;rsquo;s worth noting that I do already use Cloudflare to manage this website. The email address I use for that account is hosted separately though, otherwise I&amp;rsquo;m one DNS mistake away from making my inbox unreachable!&lt;/p&gt;
&lt;p&gt;A bit of investigating online later, and it looks like this certainly is malicious behaviour.&lt;/p&gt;
&lt;p&gt;Going by &lt;a href="https://twitter.com/andrewfergusson/status/1440617619845877771"&gt;this Twitter thread&lt;/a&gt;, it seems unverified accounts are able to provision API credentials. Unlike an authenticated browser session, these credentials persist &lt;em&gt;even when&lt;/em&gt; you reset an account&amp;rsquo;s password.&lt;/p&gt;
&lt;p&gt;If a target went through the reset flow to gain control of the account created in their name, the attacker would still have near-complete account access with existing API credentials. These would continue to persist until deleted from the account.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure why an attacker would target me, but I assume they just got my email as part of a list. Fortunately, it&amp;rsquo;s straightforward to reset the account&amp;rsquo;s password (&lt;em&gt;without&lt;/em&gt; verifying the email address) and request &lt;a href="https://developers.cloudflare.com/fundamentals/account-and-billing/account-maintenance/delete-account/"&gt;the account be deleted&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Backing up and restoring a self-hosted Plausible instance</title><link>https://nicholas.cloud/blog/backing-up-and-restoring-a-self-hosted-plausible-instance/</link><pubDate>Sat, 14 Aug 2021 11:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/backing-up-and-restoring-a-self-hosted-plausible-instance/</guid><description>&lt;p&gt;I&amp;rsquo;ve been using &lt;a href="https://plausible.io/"&gt;Plausible Analytics&lt;/a&gt; on this website &lt;a href="https://nicholas.cloud/blog/im-now-using-plausible-analytics/"&gt;for a few months now&lt;/a&gt; and I&amp;rsquo;m a fan for three key reasons.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They&amp;rsquo;re open source&lt;/li&gt;
&lt;li&gt;They explicitly detail how they &lt;a href="https://plausible.io/data-policy"&gt;track web traffic in a privacy-preserving manner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Customers can &lt;a href="https://github.com/plausible/hosting"&gt;self-host their own Plausible instances&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Being able to self-host Plausible gives me ownership of the data it collects, but it also makes me responsible for storing this data and backing it up. I manage backups for my instance with several Ansible playbooks, but the same can be done with plain shell commands.&lt;/p&gt;
&lt;p&gt;A self-hosted Plausible instance is run as a collection of Docker containers and volumes managed by Docker Compose. As a result, a full backup of each volume gives you a copy of all of the data you&amp;rsquo;ll need for a restore.&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s dive in! To start, bring down your running Plausible instance.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; hosting &lt;span class="c1"&gt;# your clone of github.com/plausible/hosting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker-compose down
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With Plausible stopped, you can take a copy of each Docker volume. I do this by mounting each volume to a plain Ubuntu container and running &lt;code&gt;tar&lt;/code&gt;, writing to a mounted directory on the host.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir &lt;span class="nv"&gt;$HOME&lt;/span&gt;/backups/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;source=hosting_db-data,destination=/var/lib/postgresql/data,readonly&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;type=bind,source=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/backups,destination=/backups&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; ubuntu &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; tar --gzip --create --file /backups/plausible-user-data.tar.gz --directory /var/lib/postgresql/data/ .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;source=hosting_event-data,destination=/var/lib/clickhouse,readonly&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;type=bind,source=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/backups,destination=/backups&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; ubuntu &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; tar --gzip --create --file /backups/plausible-event-data.tar.gz --directory /var/lib/clickhouse/ .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To restore a backup, you can remove the old volumes and extract your tarballs into new volumes.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker volume rm hosting_db-data hosting_event-data
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;source=hosting_db-data,destination=/var/lib/postgresql/data&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;type=bind,source=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/backups,destination=/backups,readonly&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; ubuntu &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; tar --extract --file /backups/plausible-user-data.tar.gz --directory /var/lib/postgresql/data/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;source=hosting_event-data,destination=/var/lib/clickhouse&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --mount &lt;span class="s2"&gt;&amp;#34;type=bind,source=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/backups,destination=/backups,readonly&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; ubuntu &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; tar --extract --file /backups/plausible-event-data.tar.gz --directory /var/lib/clickhouse/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All that&amp;rsquo;s left after this is to restart the Plausible containers. You may also want to pull changes from Plausible&amp;rsquo;s hosting repo and the latest Docker images.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Optionally, update your clone and pull latest images&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git pull
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker-compose pull
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker-compose up --detach
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From here, it&amp;rsquo;s up to you what you do with your backups. I&amp;rsquo;d suggest moving them to an external store, whether that&amp;rsquo;s your machine via &lt;code&gt;rsync&lt;/code&gt; or a storage service with your provider of choice.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/docker">docker</category></item><item><title>Exporting iCloud reminders to Fastmail</title><link>https://nicholas.cloud/blog/exporting-icloud-reminders-to-fastmail/</link><pubDate>Sat, 24 Jul 2021 15:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/exporting-icloud-reminders-to-fastmail/</guid><description>&lt;p&gt;If you&amp;rsquo;ve snooped the MX records for this site recently, you might have noticed that I&amp;rsquo;ve moved to Fastmail. In addition to email hosting, Fastmail also offers CalDAV accounts for users, so I&amp;rsquo;m trying it out for my calendar and reminders.&lt;/p&gt;
&lt;p&gt;While the Apple ecosystem supports CalDAV accounts, they don&amp;rsquo;t make it easy for you to export your reminders from iCloud into your account of choice. A Reddit post points out that it &lt;em&gt;is&lt;/em&gt; possible to copy these reminders across with the Shortcuts app though, so I decided to give that a try. Here&amp;rsquo;s a test run with a simple list of reminders.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/exporting-icloud-reminders-to-fastmail/1.png" alt="Two lists of reminders, one labelled &amp;ldquo;Test&amp;rdquo; containing two items, and an empty one label &amp;ldquo;Test Fastmail&amp;rdquo;" loading="lazy" width="640" height="200" /&gt;
&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a few catches with the approach to be aware of, since iCloud reminders have some exclusive (CalDAV-incompatible) features.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the reminder has notes attached, they need to be included in the &lt;em&gt;Notes&lt;/em&gt; of the &amp;ldquo;Create reminder&amp;rdquo; widget or they won&amp;rsquo;t be copied&lt;/li&gt;
&lt;li&gt;Some details can&amp;rsquo;t be transferred - namely attached photos, due dates and URLs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When selecting the reminders to migrate, you&amp;rsquo;ll want to filter out completed reminders. When the reminders are created in your new list, they&amp;rsquo;ll be marked as incomplete.&lt;/p&gt;
&lt;p&gt;Interestingly, a confirmation prompt shows up when deleting reminders. If you&amp;rsquo;re deleting many at once (I had one list of about ~70 reminders), you&amp;rsquo;ll have to OK it several times before the shortcut proceeds.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/exporting-icloud-reminders-to-fastmail/2.png" alt="A confirmation prompt, reading &amp;ldquo;Remove 2 reminders? This is a permanent action. Are you sure you want to remove these items?&amp;rdquo;" loading="lazy" width="320" height="200" /&gt;
&lt;/p&gt;
&lt;p&gt;Afterwards the shortcut finishes running, all of your reminders will have moved across and you&amp;rsquo;ll be good to go!&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/exporting-icloud-reminders-to-fastmail/3.png" alt="The same list of reminders as before, but the reminders in the &amp;ldquo;Test&amp;rdquo; list have moved to the &amp;ldquo;Test Fastmail&amp;rdquo; list" loading="lazy" width="640" height="200" /&gt;
&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the shortcut I wrote, if you&amp;rsquo;d like to use it.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/exporting-icloud-reminders-to-fastmail/4.png" alt="" loading="lazy" width="320" height="1037" /&gt;
&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/ios-shortcuts">ios-shortcuts</category></item><item><title>A close call with Nginx and the alias directive</title><link>https://nicholas.cloud/blog/a-close-call-with-nginx-and-the-alias-directive/</link><pubDate>Thu, 15 Jul 2021 23:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/a-close-call-with-nginx-and-the-alias-directive/</guid><description>&lt;p&gt;When Nginx is serving this website, it&amp;rsquo;s usually serving static files from the local machine. One method to accomplish this is with the &lt;code&gt;alias&lt;/code&gt; directive, which substitutes the request location for a filepath. I use it to map requests to &lt;code&gt;nicholas.cloud/files/&lt;/code&gt; to a directory for public file-sharing.&lt;/p&gt;
&lt;p&gt;One catch with this setup is that you&amp;rsquo;ll get a 404 if you visit &lt;code&gt;nicholas.cloud/files&lt;/code&gt;, as it lacks a trailing slash. Users often overlook and forget this slash, so many websites these days choose to deal with it internally and show the right page.&lt;/p&gt;
&lt;p&gt;If this trailing slash is such an encumbrance, why not drop it from my own config? This way, if someone goes to &lt;code&gt;nicholas.cloud/files&lt;/code&gt;, they&amp;rsquo;ll end up on the right path.&lt;/p&gt;
&lt;p&gt;I figured it would be nice to have, so I made a small change to my Nginx config.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # FILES
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- location /files/ {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt;&lt;span class="gi"&gt;+ location /files {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;&lt;/span&gt; alias /home/nicholas/public-files/;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; add_header &amp;#34;Cache-Control&amp;#34; &amp;#34;public, max-age=0, s-maxage=60&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One reload later, and requests missing the trailing slash were successfully redirected! But while this change was convenient, I felt concerned at the time that it was a tad unsafe. I decided to experiment.&lt;/p&gt;
&lt;p&gt;Poor path matching logic is a common (and lucrative) vulnerability for webservers like this. If an attacker can access parent directories above the target folder, all manner of sensitive files on the host can be exposed.&lt;/p&gt;
&lt;p&gt;As it turns out, my change created exactly this opening.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/a-close-call-with-nginx-and-the-alias-directive/1.png" alt="A webpage reading &amp;ldquo;403 Forbidden&amp;rdquo;, the URL path shows a successful attempt to access parent directory contents" loading="lazy" width="1104" height="334" /&gt;
&lt;/p&gt;
&lt;p&gt;Lo and behold, Nginx was now trying to serve the TLS private key that sits in the root of my user directory. There&amp;rsquo;s two things to note here.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is not wise for these senstive files to be sitting in a such an exposed location.&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;m &lt;em&gt;really&lt;/em&gt; glad I &lt;a href="https://github.com/nchlswhttkr/website/blob/d9220ae4d58eb87693a14fdb6038db015b5a75d1/droplet-config/ansible/manage-server.yml#L421"&gt;set the file&amp;rsquo;s permissions to be private&lt;/a&gt; now.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that moment, any path starting with &lt;code&gt;/files&lt;/code&gt; would follow the &lt;code&gt;alias&lt;/code&gt; directive. Nginx was accessing &lt;code&gt;/home/nicholas/public-files/../nicholas.cloud.key&lt;/code&gt;, and finding my key.&lt;/p&gt;
&lt;p&gt;Thankfully, the fix this time was only a quick revert away. If only it was always that easy. &amp;#x1f613;&lt;/p&gt;
&lt;p&gt;Next time, I think I&amp;rsquo;ll stick to writing a workaround rule rather than making such a reckless change. &amp;#x1f605;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;As a point of reflection, it&amp;rsquo;s worth noting that the &lt;a href="https://nginx.org/en/docs/http/ngx_http_core_module.html#alias"&gt;Nginx documentation for &lt;code&gt;alias&lt;/code&gt;&lt;/a&gt; specifically uses the term &amp;ldquo;replacement&amp;rdquo;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Defines a replacement for the specified location.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If that isn&amp;rsquo;t a big caution sign, I don&amp;rsquo;t know what is!&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/nginx">nginx</category></item><item><title>Managing secrets in a Rush monorepo with Pass</title><link>https://nicholas.cloud/blog/managing-secrets-in-a-rush-monorepo-with-pass/</link><pubDate>Mon, 14 Jun 2021 14:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/managing-secrets-in-a-rush-monorepo-with-pass/</guid><description>&lt;p&gt;For a while now, I&amp;rsquo;ve used &lt;a href="https://rushjs.io/"&gt;Rush&lt;/a&gt; to manage and deploy a number of Cloudflare Workers from a monorepo. It&amp;rsquo;s been great for me so far, offering incremental builds and support for alternative package managers like PNPM.&lt;/p&gt;
&lt;p&gt;One thing Rush leaves to the discretion of maintainers is secrets management. Given tooling and infrastructure can vary drastically between organisations and &lt;em&gt;even individual projects&lt;/em&gt;, there&amp;rsquo;s nothing wrong with this decision. However, it has lead to me implementing my own less-than-desirable setup.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every project follows its own &lt;code&gt;build.sh&lt;/code&gt; script to load secrets, build and deploy&lt;/li&gt;
&lt;li&gt;Cloudflare-related credentials are read from a shared script&lt;/li&gt;
&lt;li&gt;Workers that need third-party API tokens read them from their own &lt;code&gt;.env&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This &lt;em&gt;works&lt;/em&gt;, but it has a number of shortcomings. What if a worker needs to be deployed to a different Cloudflare zone (website) from every other worker? How do I manage/keep track of all these &lt;code&gt;.env&lt;/code&gt; files?&lt;/p&gt;
&lt;p&gt;I ended up looking to the &lt;a href="https://www.passwordstore.org/"&gt;&lt;code&gt;pass&lt;/code&gt; password manager&lt;/a&gt;. I&amp;rsquo;ve found it convenient for my personal projects, as it leverages my existing GPG setup and makes it easy to store/retrieve secrets from the command line.&lt;/p&gt;
&lt;p&gt;A &lt;a href="https://github.com/nchlswhttkr/workers/compare/00ef2524f8fd62ffe8e85d577a308fb6b530e63e...a23ab94f1220bfdf0940f38963366c93956f9b5e"&gt;few changes later&lt;/a&gt;, and now the build scripts for each project are explicit about what secrets they need! Here&amp;rsquo;s an abridged example.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- source ../../set-cloudflare-secrets.sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt;&lt;span class="gi"&gt;+ export CF_ACCOUNT_ID=$(pass show workers/cloudflare-account-id)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ export CF_API_TOKEN=$(pass show workers/cloudflare-api-token)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ export CF_ZONE_ID=$(pass show workers/cloudflare-zone-id-nicholas.cloud)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;&lt;/span&gt;&lt;span class="gd"&gt;- source .env
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt;&lt;span class="gi"&gt;+ export MAILGUN_API_KEY=$(pass show workers/newsletter-subscription-form/mailgun-api-key)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ export EMAIL_SIGNING_SECRET=$(pass show workers/newsletter-subscription-form/email-signing-secret)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I did find an interesting interaction between Rush and the GPG agent. Rush attempts to build projects in parallel where possible, and if too many processes are decrypting secrets at once the GPG agent will return a &lt;code&gt;Cannot allocate memory&lt;/code&gt; error.&lt;/p&gt;
&lt;p&gt;Thankfully this can be fixed by adding the &lt;code&gt;--auto-expand-secmem&lt;/code&gt; option to the agent&amp;rsquo;s config. This allows gcrypt (used by GPG) to &lt;a href="https://dev.gnupg.org/T4255#120848"&gt;allocate secure memory as needed&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cfg" data-lang="cfg"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ~/.gnupg/gpg-agent.conf&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;auto-expand-secmem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With the GPG agent restarted, I can now build many projects with secrets in parallel! It&amp;rsquo;s also good to have my secrets sitting safely outside source control, stored in a place where I can easily back them up.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/managing-secrets-in-a-rush-monorepo-with-pass/1.png" alt="Terminal output from a full monorepo rebuild, with ten projects rebuilt successfully in eleven seconds. The slowest project took eight seconds to build." loading="lazy" width="1155" height="466" /&gt;
&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;pass&lt;/code&gt; to fetch and decrypt secrets does admittedly add a few seconds to each build. Thankfully, Rush&amp;rsquo;s parallelism keeps the overall build comparatively fast. In my eyes, the tradeoff is worth it.&lt;/p&gt;</description></item><item><title>I'm excited for The Great Ace Attorney Chronicles and the Story Mode feature</title><link>https://nicholas.cloud/blog/im-excited-for-the-great-ace-attorney-chronicles-and-the-story-mode-feature/</link><pubDate>Sat, 29 May 2021 23:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/im-excited-for-the-great-ace-attorney-chronicles-and-the-story-mode-feature/</guid><description>&lt;p&gt;Late last year, I grabbed the original Ace Attorney trilogy on discount after stumbling across &lt;a href="https://www.youtube.com/watch?v=vDMwDT6BhhE"&gt;a trending fan video&lt;/a&gt;. I&amp;rsquo;ve now played through all six mainline games, and I&amp;rsquo;m optimistic for a seventh to coincide with the series&amp;rsquo; 20th anniversary later this year.&lt;/p&gt;
&lt;p&gt;While a new game isn&amp;rsquo;t confirmed, Capcom will be releasing &lt;a href="https://www.youtube.com/watch?v=seA5Rc4lxzs"&gt;a localised bundle of The Great Ace Attorney 1 &amp;amp; 2&lt;/a&gt; in a few months. This duo was previously exclusive to Japan, and it&amp;rsquo;s exiting to see them getting an international release.&lt;/p&gt;
&lt;p&gt;A feature being introduced in the game is &lt;a href="https://twitter.com/aceattorneygame/status/1397598597152190466"&gt;Story Mode&lt;/a&gt;, where the game will automatically play itself on the player&amp;rsquo;s behalf. Some internet shadows are criticising the &lt;em&gt;optional&lt;/em&gt; feature, saying it removes the logic puzzles and problem solving the series is founded upon.&lt;/p&gt;
&lt;p&gt;A reply to the critics captures the motivation behind this feature simply and succinctly. Every player should have the opportunity to experience these games, without the needless trial-and-error that the games can sometimes be guilty (&lt;em&gt;ha!&lt;/em&gt;) of.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You won&amp;rsquo;t [need story mode], but some might. This isn&amp;rsquo;t for you, it&amp;rsquo;s for them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Anyway, I&amp;rsquo;m looking forward to contradiction hunting on July 27th!&lt;/p&gt;
&lt;div class="youtube"&gt;
&lt;span class="center-text"&gt;
&lt;em&gt;HERLOCK SHOLMES REAL&lt;/em&gt;
by
&lt;a
href="https://youtube.com/channel/UCPgROddFXOC5OzYqOefLAdA"
target="_blank"
rel="noopener noreferrer"
&gt;
Fudgenuggets
&lt;/a&gt;
&lt;/span&gt;
&lt;a
href="https://youtube.com/watch?v=8CvbmcyhZTQ"
target="_blank"
rel="noopener noreferrer"
&gt;
&lt;img
loading="lazy"
alt="Thumbnail for YouTube video HERLOCK SHOLMES REAL"
width="480"
height="270"
src='https://nicholas.cloud/hqdefault_620682881633712927_hu_a77be6fc94b77926.jpg'
/&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;&lt;em&gt;How do you navigate copyright issues around England&amp;rsquo;s greatest detective? Just call them Herlock Sholmes instead!&lt;/em&gt;&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/gaming">gaming</category></item><item><title>Pretty commits</title><link>https://nicholas.cloud/blog/pretty-commits/</link><pubDate>Tue, 25 May 2021 22:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/pretty-commits/</guid><description>&lt;p&gt;In a chance glance through the Git history of one of my projects last week, I found commit &lt;a href="https://github.com/nchlswhttkr/mandarin-duck/commit/53331294bf7e8460b1d05a87a96aa2968687cc9e"&gt;&lt;code&gt;53331294&lt;/code&gt;&lt;/a&gt;. A hash with eight leading numbers! A sequence like this isn&amp;rsquo;t particularly rare, but it&amp;rsquo;s also not an everyday occurence.&lt;/p&gt;
&lt;p&gt;Today at work, I notice the palindrome &lt;code&gt;39eee93&lt;/code&gt; while using &lt;code&gt;git reset&lt;/code&gt;&lt;sup&gt;1&lt;/sup&gt;. SHA-1 works in mysterious ways. :thinking_face:&lt;/p&gt;
&lt;p&gt;Of course, there are plenty of other pretty commits out in the wild. You can always try brute-forcing a commit with more leading zeroes than &lt;a href="https://github.com/seungwonpark/ghudegy-chain/commit/00000000000000c06d2e8c36f247206a9a4b1c63"&gt;&lt;code&gt;00000000000000&lt;/code&gt;&lt;/a&gt;, or cracking a joke with &lt;a href="https://github.com/bradfitz/deadbeef"&gt;&lt;code&gt;deadbeef&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re musically inclined, what melodies use the natural notes bar G? The lick is unfortunately not SHA-friendly.&lt;/p&gt;
&lt;p&gt;When you commit next, consider a pause to admire the hash. Who knows what pattern you might spot?&lt;/p&gt;
&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt; If you find yourself navigating the fog that is &lt;code&gt;git rebase&lt;/code&gt;, remember to throw out a tag here and there. It&amp;rsquo;s a great catch if (like me) you occasionally need to reset. &amp;#x1f605;&lt;/p&gt;</description></item><item><title>Live previewing Hugo sites with Cloudflare Tunnel</title><link>https://nicholas.cloud/blog/live-previewing-hugo-sites-with-cloudflare-tunnel/</link><pubDate>Sun, 09 May 2021 15:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/live-previewing-hugo-sites-with-cloudflare-tunnel/</guid><description>&lt;p&gt;Recently, Cloudflare made their Tunnel service (formerly Argo Tunnel) &lt;a href="https://blog.cloudflare.com/tunnel-for-everyone/"&gt;free and available for all Cloudflare users&lt;/a&gt;. For an existing free-tier user like myself, there&amp;rsquo;s no better time to try it out!&lt;/p&gt;
&lt;p&gt;Tunnels are designed for securely connecting an origin server to a web-facing endpoint. They&amp;rsquo;re particularly great if&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re a service operating from a network/zone that doesn&amp;rsquo;t allow inbound connections&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re a developer looking to expose your local server to the web&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are generic tools out there that fill this niche (&lt;a href="https://ngrok.com/"&gt;ngrok&lt;/a&gt; is great for quick demos, and there&amp;rsquo;s &lt;a href="https://docs.inlets.dev/"&gt;inlets&lt;/a&gt; if you prefer self-hosting), but Cloudflare&amp;rsquo;s offering works well for me because I already use them to manage my website.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Down to business&lt;/strong&gt; - I&amp;rsquo;ll be using my website for this example because it&amp;rsquo;s built with Hugo.&lt;/p&gt;
&lt;p&gt;After &lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation"&gt;installing the &lt;code&gt;cloudflared&lt;/code&gt; tunnel client&lt;/a&gt;, there&amp;rsquo;s a few commands to authenticate and create a tunnel. I&amp;rsquo;m calling my tunnel &lt;code&gt;preview&lt;/code&gt;, and exposing it at &lt;code&gt;tunnel.nicholas.cloud&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cloudflared tunnel login
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cloudflared tunnel create preview
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cloudflared tunnel route dns preview tunnel.nicholas.cloud
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With a tunnel created, we can start tunnelling traffic from port &lt;code&gt;1313&lt;/code&gt;, the default port for Hugo&amp;rsquo;s dev server.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cloudflared tunnel run --url localhost:1313 preview
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Visiting &lt;code&gt;https://tunnel.nicholas.cloud/&lt;/code&gt; now will show a &lt;code&gt;502 Bad gateway&lt;/code&gt; error, since our Hugo server isn&amp;rsquo;t running yet. Let&amp;rsquo;s start it up!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo server &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --baseURL&lt;span class="o"&gt;=&lt;/span&gt;https://tunnel.nicholas.cloud/ &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --liveReloadPort&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;&lt;/span&gt; --appendPort&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a few options we specify with this development server, seeing as we&amp;rsquo;re no longer serving traffic from &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Overriding the default URL to your chosen URL with &lt;code&gt;baseURL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Disabling &lt;code&gt;appendPort&lt;/code&gt;, since the tunnel&amp;rsquo;s exit responds to traffic on the default HTTPS port &lt;code&gt;443&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Likewise, the &lt;code&gt;liveReloadPort&lt;/code&gt; should be &lt;code&gt;443&lt;/code&gt; so pages will be reloaded as we make changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There you have it, a live preview of your Hugo site on your own domain thanks to Cloudflare Tunnel!&lt;/p&gt;
&lt;p&gt;Thanks to Hugo (not the site generator!) for his bit on &lt;a href="https://hugo.md/post/editing-in-github-codespaces/"&gt;previewing Hugo sites in GitHub Codespaces&lt;/a&gt;, which inspired me to try this out!&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/cloudflare">cloudflare</category><category domain="https://nicholas.cloud/tags/hugo">hugo</category><category domain="https://nicholas.cloud/tags/webdev">webdev</category></item><item><title>There's a lot of automation going on here</title><link>https://nicholas.cloud/blog/theres-a-lot-of-automation-going-on-here/</link><pubDate>Sat, 01 May 2021 21:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/theres-a-lot-of-automation-going-on-here/</guid><description>&lt;p&gt;With a little bit of invested time, I&amp;rsquo;ve figured out a nice little process for previewing changes to my site. The technical side of it is a writeup for another day, but it&amp;rsquo;s too cool to not to talk about here and now.&lt;/p&gt;
&lt;p&gt;I start with my local changes on a new branch, pushing it to GitHub. The response includes a link to create a new pull request.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push -u origin HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remote:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remote: Create a pull request for &amp;#39;disable-analytics-in-preview&amp;#39; on GitHub by visiting:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remote: https://github.com/nchlswhttkr/website/pull/new/disable-analytics-in-preview&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remote:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# To github.com:nchlswhttkr/website.git&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# * [new branch] HEAD -&amp;gt; disable-analytics-in-preview&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Branch &amp;#39;disable-analytics-in-preview&amp;#39; set up to track remote branch &amp;#39;disable-analytics-in-preview&amp;#39; from &amp;#39;origin&amp;#39;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The commit message determines the pull request&amp;rsquo;s title. Referencing an open issue in the PR&amp;rsquo;s description makes the two pages reference each other.&lt;/p&gt;
&lt;p&gt;PR opened. A workflow I&amp;rsquo;ve made with GitHub Actions kicks off, and my changes are deployed to a preview domain. A link to the deployment is included with the pull request.&lt;/p&gt;
&lt;p&gt;PR merged. The same workflow kicks off and cleans up the deployed preview, marking it as inactive. The merged branch is deleted. Since the description a reference to &amp;ldquo;close #41&amp;rdquo;, the corresponding issue is also closed. &lt;sup&gt;1&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s kinda amazing how much of a breeze previewing is for me now, only ever a pull request away. GitHub&amp;rsquo;s developer experience really shines here too.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://nicholas.cloud/blog/theres-a-lot-of-automation-going-on-here/1.png" alt="Screenshot from a merged pull request on GitHub, titled &amp;ldquo;Don&amp;rsquo;t include analytics in main site&amp;rdquo;. A deployment is created and later marked as inactive. The source branch is deleted after the pull request is merged." loading="lazy" width="1280" height="897" /&gt;
&lt;/p&gt;
&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt; Fun fact: GitHub doesn&amp;rsquo;t like it when you &lt;a href="https://github.com/ianstormtaylor/slate/pull/3093#issuecomment-559313932"&gt;close 100 issues at once with a single pull request&lt;/a&gt;.&lt;/p&gt;</description><category domain="https://nicholas.cloud/tags/automation">automation</category><category domain="https://nicholas.cloud/tags/github">github</category></item><item><title>No FLoC for me please Google</title><link>https://nicholas.cloud/blog/no-floc-for-me-please-google/</link><pubDate>Fri, 23 Apr 2021 22:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/no-floc-for-me-please-google/</guid><description>&lt;p&gt;&lt;a href="https://www.eff.org/deeplinks/2021/04/fighting-floc-and-fighting-monopoly-are-fully-compatible"&gt;A new approach to online tracking being trialed by Google&lt;/a&gt; has been under scrutiny this week, from developers and privacy advocates alike.&lt;/p&gt;
&lt;p&gt;Along with others, I&amp;rsquo;m explicitly opting-out my website out of this program. Thankfully it&amp;rsquo;s a one-liner in my Nginx config.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ add_header &amp;#34;Permissions-Policy&amp;#34; &amp;#34;interest-cohort=()&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;According to Google, this tracking method is activated (&lt;em&gt;for the time being&lt;/em&gt;) when sites do either of the following.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Call &lt;code&gt;document.interestCohort()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Load ads or ad-related resources (whatever Google determines this to be)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that said, there&amp;rsquo;s no saying when or how this will change. Taking the hardline approach to avoid this behaviour seems sensible to me.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got more to say on the stances companies like Google take on privacy, but that&amp;rsquo;s a longer piece of writing for another day.&lt;/p&gt;
&lt;p&gt;Thanks to Ruben Schade for &lt;a href="https://rubenerd.com/opting-out-of-googles-floc/"&gt;his bit on FLoC&lt;/a&gt; too!&lt;/p&gt;</description></item><item><title>Traversing the sources of a song</title><link>https://nicholas.cloud/blog/traversing-the-sources-of-a-song/</link><pubDate>Fri, 16 Apr 2021 23:00:00 +1000</pubDate><author>Nicholas Whittaker</author><guid>https://nicholas.cloud/blog/traversing-the-sources-of-a-song/</guid><description>&lt;p&gt;I stumbled across an old animation from a few years ago yesterday, set to a rather funky beat. Not the most fascinating thing on its own. Interesting though, how my perspective has changed a bit after looking into the music a little bit more.&lt;/p&gt;
&lt;p&gt;By itself, the song is great. There&amp;rsquo;s more lurking beneath the surface though.&lt;/p&gt;
&lt;p&gt;It uses the vocals from &lt;em&gt;Fire Fly&lt;/em&gt; by Childish Gambino.&lt;/p&gt;
&lt;p&gt;The vocals are mashed up with the instrumental from MF DOOM&amp;rsquo;s &lt;em&gt;Coffin Nails&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;DOOM uses a sample from Dave Matthews&amp;rsquo; guitar solo in &lt;em&gt;Space Oddity&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Travere the tree, and you&amp;rsquo;re listening to a mashup with a sample from a cover of a David Bowie song!&lt;/p&gt;
&lt;p&gt;Isn&amp;rsquo;t it funny how deep the roots of a song can reach?&lt;/p&gt;
&lt;div class="youtube"&gt;
&lt;span class="center-text"&gt;
&lt;em&gt;bingo sandbelt for senator&lt;/em&gt;
by
&lt;a
href="https://youtube.com/channel/UCpclRlEJ2oh6JDEJy68UjKA"
target="_blank"
rel="noopener noreferrer"
&gt;
an0nymooose
&lt;/a&gt;
&lt;/span&gt;
&lt;a
href="https://youtube.com/watch?v=pTu6S8t_GsI"
target="_blank"
rel="noopener noreferrer"
&gt;
&lt;img
loading="lazy"
alt="Thumbnail for YouTube video bingo sandbelt for senator"
width="480"
height="270"
src='https://nicholas.cloud/hqdefault_1950681990675526268_hu_d5da77609f74c320.jpg'
/&gt;
&lt;/a&gt;
&lt;/div&gt;</description></item></channel></rss>