<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Mike Street's Blog &amp; Notes</title>
		<link>https://www.mikestreety.co.uk</link>
		<description>Blog posts, notes and links from Mike Street (mikestreety.co.uk)</description>
		<language>en-gb</language>
		<pubDate>Sat, 12 Sep 2026 12:27:59 GMT</pubDate>
		<lastBuildDate>Sat, 12 Sep 2026 12:27:59 GMT</lastBuildDate>
		<atom:link href="https://www.mikestreety.co.uk/rss-all.xml" rel="alternate" type="application/xml" />
		<image>
			<url>https://www.mikestreety.co.uk/assets/img/favicon-512.png</url>
			<title>Mike Street's Blog &amp; Notes</title>
			<link>https://www.mikestreety.co.uk</link>
			<width>144</width>
			<height>144</height>
			<description>Lead Developer and CTO</description>
		</image>
		
		
		<item>
			<title>Setting up Claude Code</title>
			<link>https://www.mikestreety.co.uk/blog/setting-up-claude-code/</link>
			<pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/setting-up-claude-code/</guid>
			<description><![CDATA[
<p>I've recently been utilising and customising Claude Code more - rather than just sticking with what comes out of the box. With more and more of my colleagues and friends adopting AI (and, specifically, Claude), I thought I would share my go-to config when setting it all up.</p>
<h2>Terminal</h2>
<p>Claude Code on the terminal is where I spend most of my time - I find it is more powerful than the <strong>Code</strong> element of the desktop app. Once Claude is installed, these are the settings I reach for:</p>
<ul>
<li><code>/model</code>: <strong>Sonnet</strong> - I've found Sonnet to be a good reliable workhorse without burning all the tokens</li>
<li><code>/effort</code>: <strong>High</strong> - Effort was explained to me like &quot;motivation&quot; - High is a good, solid balance</li>
<li><code>/advisor</code>: <strong>Opus</strong> - This is a killer feature - it does a lot of the thinking in Sonnet and then checks with Opus</li>
<li><code>/config</code> - Some settings I change from the default
<ul>
<li><code>Default permission mode</code>: <strong>Plan</strong> - Means I don't have to remember to switch, sometimes the plan gives you a solution without needing to burn more tokens implementing it</li>
<li><code>Push when actions required</code>: <strong>true</strong></li>
</ul>
</li>
</ul>
<h2>MCP / CLI tools</h2>
<p>The next thing I lean on is MCPs and CLIs to expand the capabilities of Claude</p>
<p><strong>MCPs</strong></p>
<p>Model Context Protocol (MCP) servers allow LLMs and AI tools to connect to live data to get more context for the task.</p>
<ul>
<li><a href="https://developer.clickup.com/docs/connect-an-ai-assistant-to-clickups-mcp-server">ClickUp</a> - This is our project management tool and allows Claude to get the full context of a task</li>
<li><a href="https://docs.gitlab.com/user/model_context_protocol/mcp_server/#connect-claude-code-to-the-gitlab-mcp-server">GitLab</a> - We use GitLab for our projects so allowing Claude to access Work Items (issues) and MR feedback helps speed up the feedback loop</li>
</ul>
<p><strong>CLIs</strong></p>
<p>With certain CLI tools, Claude is able to perform actions more efficiently which saves tokens</p>
<ul>
<li><a href="https://docs.gitlab.com/cli/">GitLab CLI</a> - Access issue and merge request information over a simplified CLI</li>
<li><a href="https://playwright.dev/docs/getting-started-cli">Playwright</a> - Preferred over the Playwright MCP</li>
</ul>
<h2>Skills &amp; Plugins</h2>
<p>Not so much a config or command in itself, but if you find yourself repeating particular instructions (e.g. how to make a PHPUnit test or code something in a particular way) consider making a skill.</p>
<p>The best way to do this is to ask Claude itself:</p>
<p>&quot;Make a skill that helps me write consistent...&quot;</p>
<p>Don't forget to specify if it should be a project or user skill.</p>
<p>You can use the skill creator plugin to help you create skills:</p>
<ol>
<li>Type <code>/plugin</code></li>
<li>Search for <code>skill-creator</code>, press &quot;Enter&quot; and install it</li>
<li>Type <code>/reload-plugins</code> to register it</li>
<li>Type <code>/skill-creator:skill-creator</code> with a description to get you started</li>
</ol>
<p>It's worth looking through the <code>/plugin</code> list for plugins which may help with the type of coding you do.</p>
<h2>Learning</h2>
<p>Master.dev features a <a href="https://master.dev/courses/claude-code/">free course on Claude</a>. It doesn't go into deep complex architecture, but gives a great overview to help you understand Claude better and how to get the best of it</p>
<h2>Conclusion</h2>
<p>And that's it, I'll update the post as I find more customisations and tweaks. For now, this is how I set up my Claude code. If you've got this far, you might be interested to read my true <a href="/blog/my-thoughts-on-ai">feelings about AI</a>.</p>


<p><strong>Read time:</strong> 2 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>git rebase -i is not that scary</title>
			<link>https://www.mikestreety.co.uk/notes/git-rebase-i-is-not-that-scary/</link>
			<pubDate>Wed, 29 Jul 2026 10:05:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/notes/git-rebase-i-is-not-that-scary/</guid>
			<description><![CDATA[
<p>A guide to interactive rebasing.</p>

<p><a href="https://cachebag.sh/journal/interactive-rebasing/">https://cachebag.sh/journal/interactive-rebasing/</a></p>
<p><strong>Read time:</strong> 1 mins</p>

]]></description>
		</item>
		
		
		<item>
			<title>Old MacDonald outsourced his farm, AI, AI. Oh.</title>
			<link>https://www.mikestreety.co.uk/blog/old-macdonald-outsourced-his-farm-ai-ai-oh/</link>
			<pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/old-macdonald-outsourced-his-farm-ai-ai-oh/</guid>
			<description><![CDATA[
<p>No, I don't want AI added to every product, service and website. No, I do not want to have to shell out for 20 different subscriptions because <em>your</em> product has a specific esoteric LLM that I have to pay separately for just because it can integrate with your software better.</p>
<p>AI is a tool, it is not a solution. It doesn't fix the problems in your application, it just burns the world while it uses its baked-in polyfills to get around your bad UI. Too many apps push you to use their AI to help you find information and to &quot;get in touch&quot; - how about you have better navigation, sign-posting and UX so people can find it themselves without relying on some half-baked chatbot claiming to have natural language processing?</p>
<p>I was speaking with an accessibility consultant a while ago and he was bemoaning those widgets people can add to their website to &quot;improve&quot; accessibility. He was saying that, if he needed bigger text or different contrast, he would have his own tool to do it. You wouldn't expect someone who turns up to a shop in their own wheelchair to be forced to swap it for the shop-provided one. I feel the same about AI; I have an LLM I am paying for, don't force me to use yours - allow me to continue to use my personalised assistant within your property.</p>
<p>While I rant on about AI, you'd think I was some sort of hater - but I'm an avid user. It has its place, it is a useful tool but this scattergun multi-LLM approach the tech industry seems to be taking at the moment needs to stop. Allow us to bring our own LLM if we have one, expose your service or app as an MCP or plugin, so I can use my customised AI agent of choice to do the things you desperately need me to use an AI for.</p>
<p>I see AI as a force-multiplier, not a replacement. Companies laying off staff because they &quot;have AI&quot; are missing the point (<a href="https://www.bbc.co.uk/news/articles/cgrkd41n2v9o">even Ford realised their mistake</a>). AI increases the potential of anyone using it effectively, and raises the ceiling on how much complexity they can handle. It can churn away at the bug, it can chip away at the low &quot;grunt level&quot; tasks and allow the human to <em>think</em> [I really wanted to write &quot;transcend&quot; here]. I've found I've got a new capacity for thinking and problem solving and tackling bigger problems because AI is assessing and proposing a fix for that bug at the same time.</p>
<blockquote>
<p>AI can be good on its own, but it can be <em>great</em> with a human.</p>
</blockquote>
<p>Use it as a rubber-duck. Use it to solve that issue, tick off that lingering to-do or optimise that gnarly bash script. Use it to build internal tools for reporting and scratching that itch. Use it to aid you in solving production-ready code. Review the plans, hone the plans, craft the plans but then walk away while it does the grunt work.</p>
<p>Take it bowling, but keep the barriers up, use the ramp to push and get a near-guaranteed strike. <em>That's</em> how you get good results with it - by putting up guardrails and guides.</p>
<p>It's great for helping you solve that niche problem that you wouldn't find on a Reddit thread or to make sure you're not asking a ridiculous question. It's wonderful for helping my 7-year-old budding botanist identify plants and flowers.</p>
<p>I've tried a few of the popular LLMs and have settled on Claude as my favoured one. It seems a lot more code-focused (which is what I mainly use it for) although I will pop over to Gemini if I need/want to do any image/video/music creating or editing (this is normally for humorous effect rather than anything serious).</p>
<p>Now I've got that off my chest I can go away and have fun.</p>
<p>Note: This post was about getting my current thoughts on AI down as a snapshot. My views may, and will, change.</p>


<p><strong>Read time:</strong> 3 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>Comparing Bitwarden Secrets Manager to Infisical</title>
			<link>https://www.mikestreety.co.uk/blog/comparing-bitwarden-secrets-manager-to-infisical/</link>
			<pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/comparing-bitwarden-secrets-manager-to-infisical/</guid>
			<description><![CDATA[
<p>We've been reviewing our secret management process and have been evaluating tools to help with streamlining it.</p>
<p>Looking at our current services and platforms, we have narrowed it down to either Bitwarden or Infisical (more details below). This post discusses the differences and compares features.</p>
<p>We have a few specific requirements for our secret management tool; the main one being that the production site/application should not rely on it to run day-to-day.</p>
<div class="info">Although we will discuss pricing at various points, most of the below was carried out on their free tiers</div>
<h2>Overview</h2>
<h3>Bitwarden Secrets Manager</h3>
<p><a href="https://bitwarden.com/en-gb/products/secrets-manager/">Bitwarden Secrets Manager</a> is a service provided by the password management tool we use. If you don't use Bitwarden, I wouldn't expect you to be considering it. However, <a href="https://1password.com/developers/secrets-management">1Password</a> and other password managers often offer a secret management service which, I suspect, acts in the same way.</p>
<h3>Infisical</h3>
<p><a href="https://infisical.com/">Infisical</a> is a separate, dedicated, secret management platform.</p>
<h2>Sign up</h2>
<p>Signing up for both is fairly straightforward. It's worth noting that they both offer US/EU hosting (it seems to be a very small option on the sign up page for both of them). Unfortunately, for both services, it doesn't seem like it is possible to migrate your data between instances - so make sure you pick the right one.</p>
<p>If you already use Bitwarden, adding on the Secrets Manager is a case of visiting your billing profile. One thing that wasn't clear was that you don't have to pay for every person in your company - you can select who has access to the secrets manager and only pay for them.</p>
<h2>Adding secrets</h2>
<p>Infisical has the idea of projects and environments. Once you make a project, you can add a secret to an environment (one or many). You can then see which environments have which secrets and copy between (should you need to). When calling it, you can pass in an <code>--env</code> flag to get the different environment secrets.</p>
<p>Bitwarden is more of a flat structure - there is a project which has secrets. If you wish to have different &quot;environments&quot;, then you would use multiple projects. This is managed by passing in the <code>project-id</code>.</p>
<p>Infisical has an interface which allows you to paste an existing <code>.env</code> file and import the secrets from there. With Bitwarden, you have to add them individually.</p>
<h2>Local Installation</h2>
<p>Both Bitwarden and Infisical have a CLI to programmatically interact with secrets.</p>
<ul>
<li><a href="https://infisical.com/docs/cli/overview">Installing Infisical</a></li>
<li><a href="https://bitwarden.com/en-gb/help/secrets-manager-cli/">Installing Bitwarden Secrets Manager CLI</a></li>
</ul>
<p>From a MacOS perspective, the following was required:</p>
<h3>Infisical</h3>
<pre class="language-bash"><code class="language-bash">brew <span class="token function">install</span> infisical/get-cli/infisical
infisical login</code></pre>
<p>Infisical login is interactive and loads the browser for access.</p>
<p>For each project, you'll then need to run the following:</p>
<pre class="language-bash"><code class="language-bash">infisical init</code></pre>
<p>This gives you an interactive prompt to select the organisation &amp; project for the secrets - this creates a <code>.infisical.json</code> file which can be committed for other developers to take advantage of. This has several <a href="https://infisical.com/docs/cli/project-config">configuration options</a> you can set.</p>
<h3>Bitwarden</h3>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> https://bws.bitwarden.com/install <span class="token operator">|</span> <span class="token function">sh</span>
bws config server-base https://vault.bitwarden.eu <span class="token comment"># if you chose the EU one</span></code></pre>
<p>You then need to make a machine account and associate it with the projects to use.</p>
<p>Once done, you can set the auth token</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">export</span> <span class="token assign-left variable">BWS_ACCESS_TOKEN</span><span class="token operator">=</span><span class="token number">0</span>.48c78342-1<span class="token punctuation">..</span>.Iq6Bow<span class="token operator">==</span></code></pre>
<h2>Usage</h2>
<h3>ddev</h3>
<p>The first requirement is for local development - we use DDEV for our TYPO3 sites and both CLIs offer an out-of-the-box method of injecting the environment variables into the ddev container at runtime rather than leaving <code>.env</code> files lying around.</p>
<p>By adding an extra step, it is then a conscious decision to include the secrets, rather than them being there by default.</p>
<div class="note">When using a secrets manager with ddev, you need to specify each variable in your `.ddev/config.yaml` file:</div>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">web_environment</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> DB_PASSWORD=$<span class="token punctuation">{</span>DB_PASSWORD<span class="token punctuation">}</span>
  <span class="token punctuation">-</span> API_KEY=$<span class="token punctuation">{</span>API_KEY<span class="token punctuation">}</span></code></pre>
<h4>Infisical</h4>
<p>Once you have your <code>.infisical.json</code> file, you can run</p>
<pre class="language-bash"><code class="language-bash">infisical run <span class="token parameter variable">--env</span><span class="token operator">=</span>dev -- ddev start</code></pre>
<p>This can be shortened by setting <code>defaultEnvironment</code> and <code>gitBranchToEnvironmentMapping</code> in your JSON config.</p>
<h4>Bitwarden</h4>
<p>Bitwarden has no concept of storing the project locally, so it needs to be specified on launch. For example:</p>
<pre class="language-bash"><code class="language-bash">bws run --project-id 7b006643-89c1-4202-a5ca-90510f566030 -- ddev start</code></pre>
<h3>Environment Files (<code>.env</code>)</h3>
<p>The next requirement of ours was to build <code>.env</code> files for production. We want the projects to work whether our secrets management tool is online or not. For this, we need to export the secrets from the tool during the CI process.</p>
<h4>Infisical</h4>
<p>Infisical has a <code>.env</code> export for this very purpose:</p>
<pre class="language-bash"><code class="language-bash">infisical <span class="token builtin class-name">export</span> <span class="token parameter variable">--env</span><span class="token operator">=</span>prod <span class="token operator">></span> .env</code></pre>
<h4>Bitwarden</h4>
<p>Bitwarden too has a native <code>.env</code> export:</p>
<pre class="language-bash"><code class="language-bash">bws secret list 7b006643-89c1-4202-a5ca-90510f566030  <span class="token parameter variable">-o</span> <span class="token function">env</span> <span class="token operator">></span> .env</code></pre>
<h2>Cost</h2>
<p>At time of writing, for unlimited projects:</p>
<ul>
<li>Bitwarden is $6 per user per month (with up to 20 machine accounts)</li>
<li>Infisical is $20 per <em>identity</em> per month (note: even machine accounts count as identities)</li>
</ul>
<p>However, you can <a href="https://infisical.com/docs/self-hosting/overview">self-host</a> Infisical for the cost of a small VPS (around $8 a month) - although you have the added &quot;cost&quot; of maintenance and updates (and security).</p>
<h2>A note on projects</h2>
<p>With Infisical, the <code>.infisical.json</code> file is native - it keeps you and your teammates in sync on the project, and the environments can be switched with a <code>--env</code> flag.</p>
<p>With Bitwarden, the project ID needs to be stored somewhere, or accessed each time. This could be used as an extra security step but could lead to project secrets being injected into the wrong project. A minor issue in development but a major one if done on production.</p>
<h2>Conclusion</h2>
<p>For now, we're still in discussions as to how we want to work and what we want. We are already in the Bitwarden ecosystem, but the features of Infisical are appealing.</p>
<p>Let me know if you've faced a similar decision and what you ended up with.</p>


<p><strong>Read time:</strong> 4 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>Setting up a central bastion/jump server</title>
			<link>https://www.mikestreety.co.uk/blog/setting-up-a-central-bastion-jump-server/</link>
			<pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/setting-up-a-central-bastion-jump-server/</guid>
			<description><![CDATA[
<p>With several developers and a great deal of production servers we need access to, managing SSH keys and access across them all was getting complex with potential for mistakes and missed authentication.</p>
<p>Instead, we decided to set up a bastion/jump host (with a redundant backup) - this means the client servers need just 2 SSH keys added and we manage authentication and access for the team in a central location.</p>
<div class="note">Note: This post has been sitting in my drafts for years and I'm posting it out of posterity. We've since moved to a more robust solution which I'll blog about later</div>
<p>This is managed by a central, private, git repository which is synced to both servers regularly - enabling addition (and revocation) of clients and staff, with a central source of truth.</p>
<p>Our set up was mainly based off/inspired by <a href="https://zanderwork.com/blog/jump-host/">Zander's Jump Host</a> blog post.</p>
<h2>Setup</h2>
<p>This assumes a <strong>Debian</strong> VPS server. SSH in and update the basic packages along with installing required applications</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">apt</span> update <span class="token operator">&amp;&amp;</span> <span class="token function">apt</span> upgrade <span class="token parameter variable">-y</span>
<span class="token function">apt</span> <span class="token function">install</span> <span class="token function">curl</span> <span class="token function">vim</span> iptables fail2ban <span class="token parameter variable">-y</span></code></pre>
<h2>User creation</h2>
<p>Next, create a baseline user with no password and create an empty <code>authorized_keys</code> file</p>
<pre class="language-bash"><code class="language-bash">adduser --disabled-password <span class="token parameter variable">--gecos</span> <span class="token string">""</span> ingress
<span class="token function">su</span> - ingress
<span class="token builtin class-name">cd</span> ~
<span class="token function">mkdir</span> .ssh
<span class="token function">touch</span> .ssh/authorized_keys
<span class="token function">chmod</span> <span class="token number">600</span> .ssh/authorized_keys
<span class="token function">nano</span> .ssh/authorized_keys</code></pre>
<p>Inside <code>authorized_keys</code> file, put the keys of everyone who needs access to the jump server</p>
<h2>SSH Restrictions</h2>
<p>Next step is to restrict the SSH access and disable it for root. In doing so, SSH will be disabled - ensure you have access via your hosting (they normally have a web terminal you can access)</p>
<p>Edit the main SSHD Config file (<code>/etc/ssh/sshd_config</code>) - we replaced the whole contents.</p>
<details>
<summary><code>/etc/ssh/sshd_config</code></summary>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># Move SSH to a non-standard port.</span>
Port <span class="token number">2233</span>

<span class="token comment"># Increase the log verbosity</span>
LogLevel VERBOSE

<span class="token comment"># Disable root login</span>
PermitRootLogin no

<span class="token comment"># Enable authentication with keypairs</span>
PubkeyAuthentication <span class="token function">yes</span>

<span class="token comment"># Hardcode directory used for authorized public keys (this is expanded to $HOME/.ssh/authorized_keys)</span>
AuthorizedKeysFile .ssh/authorized_keys

<span class="token comment"># Disable host authentication</span>
HostbasedAuthentication no

<span class="token comment"># Explicitly disable .rhosts files</span>
IgnoreRhosts <span class="token function">yes</span>

<span class="token comment"># Disable password-based auth</span>
PasswordAuthentication no

<span class="token comment"># Disable empty passwords (this doesn't really matter because we disabled password auth but extra verbosity won't hurt)</span>
PermitEmptyPasswords no

<span class="token comment"># Disable challenge/response auth</span>
ChallengeResponseAuthentication no

<span class="token comment"># Enable PAM modules</span>
UsePAM <span class="token function">yes</span>

<span class="token comment"># Allow SSH clients to forward SSH agents to use this host as a proxy</span>
AllowAgentForwarding <span class="token function">yes</span>

<span class="token comment"># Disable SSH remote forwarding</span>
GatewayPorts no

<span class="token comment"># Disable X11 forwarding. Depending on your use case you may need to enable this.</span>
X11Forwarding no

<span class="token comment"># Disable printing the MOTD. This can be enabled/configured to your liking.</span>
PrintMotd no

<span class="token comment"># Accept locale variables from SSH client</span>
AcceptEnv <span class="token environment constant">LANG</span> LC_*

<span class="token comment"># Whitelist the lowpriv user. This (in combination with PermitRootLogin no) effectively disables all other users from SSHing in.</span>
AllowUsers ingress</code></pre>
</details>
<h2>Firewall</h2>
<p>Next is to lock down the firewall. If your team are accessing all from a predictable set of IPs, it would be an improvement to add them here to increase the security further</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">touch</span> /etc/network/if-up.d/00-firewall
<span class="token function">chmod</span> +x /etc/network/if-up.d/00-firewall
<span class="token function">nano</span> /etc/network/if-up.d/00-firewall</code></pre>
<details>
<summary><code>/etc/network/if-up.d/00-firewall</code></summary>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/bin/sh</span>

<span class="token comment">#</span>
<span class="token comment"># Firewall rules</span>
<span class="token comment">#</span>
iptables <span class="token parameter variable">-P</span> INPUT ACCEPT
iptables <span class="token parameter variable">-P</span> OUTPUT ACCEPT
iptables <span class="token parameter variable">-P</span> FORWARD ACCEPT

<span class="token comment">#</span>
<span class="token comment"># Delete all existing rules</span>
<span class="token comment">#</span>
iptables <span class="token parameter variable">-F</span>
iptables <span class="token parameter variable">-t</span> nat <span class="token parameter variable">-F</span>
iptables <span class="token parameter variable">-t</span> mangle <span class="token parameter variable">-F</span>
iptables <span class="token parameter variable">-X</span>

<span class="token comment">#</span>
<span class="token comment"># Enable free use of loopback interfaces</span>
<span class="token comment">#</span>
iptables <span class="token parameter variable">-A</span> INPUT <span class="token parameter variable">-i</span> lo <span class="token parameter variable">-j</span> ACCEPT
iptables <span class="token parameter variable">-A</span> OUTPUT <span class="token parameter variable">-o</span> lo <span class="token parameter variable">-j</span> ACCEPT

<span class="token comment">#</span>
<span class="token comment"># Allow established connections</span>
<span class="token comment">#</span>
iptables <span class="token parameter variable">-A</span> INPUT <span class="token parameter variable">-m</span> state <span class="token parameter variable">--state</span> ESTABLISHED <span class="token parameter variable">-j</span> ACCEPT

iptables <span class="token parameter variable">-A</span> INPUT <span class="token parameter variable">-p</span> tcp <span class="token parameter variable">--dport</span> <span class="token number">2233</span> <span class="token parameter variable">-j</span> ACCEPT

iptables <span class="token parameter variable">-A</span> INPUT <span class="token parameter variable">-j</span> DROP</code></pre>
</details>
<h2>Prevent Brute Forcing</h2>
<p>The next step prevents people repeatedly trying different passwords &amp; other access</p>
<pre class="language-bash"><code class="language-bash">systemctl start fail2ban
systemctl <span class="token builtin class-name">enable</span> fail2ban
<span class="token function">nano</span> /etc/fail2ban/jail.d/sshd.conf</code></pre>
<details>
<summary><code>/etc/fail2ban/jail.d/sshd.conf</code></summary>
<pre class="language-bash"><code class="language-bash"><span class="token punctuation">[</span>sshd<span class="token punctuation">]</span>
enabled <span class="token operator">=</span> <span class="token boolean">true</span>
port <span class="token operator">=</span> <span class="token number">2233</span>
filter <span class="token operator">=</span> sshd
logpath <span class="token operator">=</span> /var/log/auth.log
maxretry <span class="token operator">=</span> <span class="token number">3</span>
findtime <span class="token operator">=</span> <span class="token number">60</span>
bantime <span class="token operator">=</span> <span class="token number">1800</span></code></pre>
</details>
<h2>Unattended upgrades</h2>
<p>Installed unattended upgrades to ensure the server stays patched and up-to-date</p>
<p>Setup</p>
<p>To setup and configure</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">apt</span> update <span class="token operator">&amp;&amp;</span> <span class="token function">apt</span> upgrade <span class="token operator">&amp;&amp;</span> <span class="token function">apt</span> autoclean
<span class="token function">apt</span> <span class="token function">install</span> unattended-upgrades <span class="token parameter variable">-y</span>
systemctl <span class="token builtin class-name">enable</span> unattended-upgrades
systemctl start unattended-upgrades</code></pre>
<p>Edit the config file <code>/etc/apt/apt.conf.d/50unattended-upgrades</code></p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nano</span> /etc/apt/apt.conf.d/50unattended-upgrades</code></pre>
<ol>
<li>Uncomment out <code>&quot;origin=Debian,codename=${distro_codename}-updates&quot;;</code> (so it is enabled)</li>
<li>Set <code>Unattended-Upgrade::Mail &quot;YOUREMAIL&quot;;</code> - search for Mail and set the value</li>
<li>Set <code>Unattended-Upgrade::Remove-Unused-Dependencies &quot;true&quot;;</code></li>
</ol>
<p>Edit <code>/etc/apt/apt.conf.d/20auto-upgrades</code> and replace the contents with the following</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nano</span> /etc/apt/apt.conf.d/20auto-upgrades</code></pre>
<pre class="language-bash"><code class="language-bash">APT::Periodic::Update-Package-Lists <span class="token string">"1"</span><span class="token punctuation">;</span>
APT::Periodic::Unattended-Upgrade <span class="token string">"1"</span><span class="token punctuation">;</span>
APT::Periodic::AutocleanInterval <span class="token string">"7"</span><span class="token punctuation">;</span></code></pre>
<p>Test it with</p>
<pre class="language-bash"><code class="language-bash">unattended-upgrades --dry-run <span class="token parameter variable">--debug</span></code></pre>
<h2>Final checks</h2>
<ul>
<li>Restart SSH: <code>/etc/init.d/ssh restart</code> - Can you SSH in as <code>ingress</code> user?</li>
<li>Reboot the server - can you SSH in?</li>
</ul>


<p><strong>Read time:</strong> 4 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>Notify Slack when an SSH connection is made with sshpiper</title>
			<link>https://www.mikestreety.co.uk/blog/notify-slack-when-an-ssh-connection-is-made-with-sshpiper/</link>
			<pubDate>Fri, 17 Jul 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/notify-slack-when-an-ssh-connection-is-made-with-sshpiper/</guid>
			<description><![CDATA[
<p>Keeping track of which developer is connecting to which server can be a job in itself. There are plenty of ways to handle it, from sending logs to dedicated software to writing them to a file that someone has to check manually.</p>
<p>Our <a href="https://github.com/tg123/sshpiper">sshpiper</a> setup takes a different approach and sends a Slack notification to a dedicated channel whenever a connection happens. This means we can keep an eye on things and quickly confirm that connections are genuine and expected.</p>
<p>To make this work, we built a custom notification plugin that sits between the existing <a href="https://github.com/tg123/sshpiper/blob/master/plugin/failtoban">fail2ban</a> and <a href="https://github.com/tg123/sshpiper/blob/master/plugin/workingdir">workingdir</a> plugins.</p>
<div class="info">A working sshpiper install is required for this</div>
<h2>Overview</h2>
<p>The files used for this plugin are</p>
<ul>
<li><code>/plugins/authnotify/main.go</code> - The go code (which gets compiled)</li>
<li><code>/etc/sshpiperd/slack-webhook-url</code> - Text file with your incoming webhook</li>
</ul>
<h2>The plugin code</h2>
<p>This is a slightly trimmed down version of the one we have so you can get the basics going. There's an explanation at the end as to the additional checks we have. Feel free to <a href="https://www.mikestreety.co.uk/contact/">reach out</a> if you'd like an example.</p>
<p>Create a file/folder for you plugin (ours lives in <code>/plugins/authnotify/main.go</code>)</p>
<details>
<summary><code>main.go</code></summary>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> main

<span class="token keyword">import</span> <span class="token punctuation">(</span>
	<span class="token string">"bytes"</span>
	<span class="token string">"encoding/json"</span>
	<span class="token string">"fmt"</span>
	<span class="token string">"log/slog"</span>
	<span class="token string">"net"</span>
	<span class="token string">"net/http"</span>
	<span class="token string">"os"</span>

	<span class="token string">"github.com/tg123/sshpiper/libplugin"</span>
	<span class="token string">"github.com/urfave/cli/v2"</span>
	<span class="token string">"golang.org/x/crypto/ssh"</span>
<span class="token punctuation">)</span>

<span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
	libplugin<span class="token punctuation">.</span><span class="token function">CreateAndRunPluginTemplate</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>libplugin<span class="token punctuation">.</span>PluginTemplate<span class="token punctuation">{</span>
		Name<span class="token punctuation">:</span> <span class="token string">"authnotify"</span><span class="token punctuation">,</span>
		Flags<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>cli<span class="token punctuation">.</span>Flag<span class="token punctuation">{</span>
			<span class="token operator">&amp;</span>cli<span class="token punctuation">.</span>StringFlag<span class="token punctuation">{</span>Name<span class="token punctuation">:</span> <span class="token string">"authorized-keys"</span><span class="token punctuation">,</span> Value<span class="token punctuation">:</span> <span class="token string">"/etc/sshpiperd/shared/authorized_keys"</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
			<span class="token operator">&amp;</span>cli<span class="token punctuation">.</span>StringFlag<span class="token punctuation">{</span>Name<span class="token punctuation">:</span> <span class="token string">"slack-webhook-url-file"</span><span class="token punctuation">}</span><span class="token punctuation">,</span>
		<span class="token punctuation">}</span><span class="token punctuation">,</span>
		CreateConfig<span class="token punctuation">:</span> <span class="token keyword">func</span><span class="token punctuation">(</span>c <span class="token operator">*</span>cli<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>libplugin<span class="token punctuation">.</span>SshPiperPluginConfig<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
			authorizedKeysPath <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">String</span><span class="token punctuation">(</span><span class="token string">"authorized-keys"</span><span class="token punctuation">)</span>

			webhookURL <span class="token operator">:=</span> <span class="token string">""</span>
			<span class="token keyword">if</span> f <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">String</span><span class="token punctuation">(</span><span class="token string">"slack-webhook-url-file"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> f <span class="token operator">!=</span> <span class="token string">""</span> <span class="token punctuation">{</span>
				<span class="token keyword">if</span> b<span class="token punctuation">,</span> err <span class="token operator">:=</span> os<span class="token punctuation">.</span><span class="token function">ReadFile</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">==</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
					webhookURL <span class="token operator">=</span> <span class="token function">string</span><span class="token punctuation">(</span>bytes<span class="token punctuation">.</span><span class="token function">TrimSpace</span><span class="token punctuation">(</span>b<span class="token punctuation">)</span><span class="token punctuation">)</span>
				<span class="token punctuation">}</span>
			<span class="token punctuation">}</span>

			<span class="token keyword">return</span> <span class="token operator">&amp;</span>libplugin<span class="token punctuation">.</span>SshPiperPluginConfig<span class="token punctuation">{</span>

				<span class="token comment">// Runs once per key the client's SSH agent offers.</span>
				PublicKeyCallback<span class="token punctuation">:</span> <span class="token keyword">func</span><span class="token punctuation">(</span>conn libplugin<span class="token punctuation">.</span>ConnMetadata<span class="token punctuation">,</span> key <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token builtin">byte</span><span class="token punctuation">)</span> <span class="token punctuation">(</span><span class="token operator">*</span>libplugin<span class="token punctuation">.</span>Upstream<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
					identity <span class="token operator">:=</span> <span class="token function">lookupIdentity</span><span class="token punctuation">(</span>authorizedKeysPath<span class="token punctuation">,</span> key<span class="token punctuation">)</span>
					slog<span class="token punctuation">.</span><span class="token function">Info</span><span class="token punctuation">(</span><span class="token string">"auth attempt"</span><span class="token punctuation">,</span> <span class="token string">"identity"</span><span class="token punctuation">,</span> identity<span class="token punctuation">,</span> <span class="token string">"remote"</span><span class="token punctuation">,</span> conn<span class="token punctuation">.</span><span class="token function">RemoteAddr</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>

					<span class="token keyword">if</span> identity <span class="token operator">==</span> <span class="token string">""</span> <span class="token punctuation">{</span>
						<span class="token comment">// Not our key to judge — offer the agent's next key</span>
						<span class="token comment">// instead of rejecting (that would trip failtoban)</span>
						<span class="token comment">// or advancing (we'd miss whichever key actually works).</span>
						<span class="token keyword">return</span> <span class="token operator">&amp;</span>libplugin<span class="token punctuation">.</span>Upstream<span class="token punctuation">{</span>
							Auth<span class="token punctuation">:</span> libplugin<span class="token punctuation">.</span><span class="token function">CreateRetryCurrentPluginAuth</span><span class="token punctuation">(</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token builtin">string</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
						<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">nil</span>
					<span class="token punctuation">}</span>

					<span class="token comment">// Matched: hand off to workingdir, which makes the real</span>
					<span class="token comment">// accept/reject decision. Carry identity forward for later.</span>
					<span class="token keyword">return</span> <span class="token operator">&amp;</span>libplugin<span class="token punctuation">.</span>Upstream<span class="token punctuation">{</span>
						Auth<span class="token punctuation">:</span> libplugin<span class="token punctuation">.</span><span class="token function">CreateNextPluginAuth</span><span class="token punctuation">(</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token builtin">string</span><span class="token punctuation">{</span><span class="token string">"identity"</span><span class="token punctuation">:</span> identity<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
					<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">nil</span>
				<span class="token punctuation">}</span><span class="token punctuation">,</span>

				<span class="token comment">// Runs only once a connection is actually accepted and piped.</span>
				PipeStartCallback<span class="token punctuation">:</span> <span class="token keyword">func</span><span class="token punctuation">(</span>conn libplugin<span class="token punctuation">.</span>ConnMetadata<span class="token punctuation">)</span> <span class="token punctuation">{</span>
					identity <span class="token operator">:=</span> conn<span class="token punctuation">.</span><span class="token function">GetMeta</span><span class="token punctuation">(</span><span class="token string">"identity"</span><span class="token punctuation">)</span>
					ip<span class="token punctuation">,</span> <span class="token boolean">_</span><span class="token punctuation">,</span> <span class="token boolean">_</span> <span class="token operator">:=</span> net<span class="token punctuation">.</span><span class="token function">SplitHostPort</span><span class="token punctuation">(</span>conn<span class="token punctuation">.</span><span class="token function">RemoteAddr</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
					<span class="token function">postToSlack</span><span class="token punctuation">(</span>webhookURL<span class="token punctuation">,</span> identity<span class="token punctuation">,</span> conn<span class="token punctuation">.</span><span class="token function">User</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> ip<span class="token punctuation">)</span>
				<span class="token punctuation">}</span><span class="token punctuation">,</span>
			<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token boolean">nil</span>
		<span class="token punctuation">}</span><span class="token punctuation">,</span>
	<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>

<span class="token comment">// lookupIdentity re-reads and re-parses authorized_keys on every call. The</span>
<span class="token comment">// real plugin caches this on file mtime (see authnotify's authorizedKeysCache)</span>
<span class="token comment">// so the hot auth path isn't re-parsing the file per offered key — trimmed</span>
<span class="token comment">// here for clarity.</span>
<span class="token keyword">func</span> <span class="token function">lookupIdentity</span><span class="token punctuation">(</span>path <span class="token builtin">string</span><span class="token punctuation">,</span> offeredKeyBytes <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token builtin">byte</span><span class="token punctuation">)</span> <span class="token builtin">string</span> <span class="token punctuation">{</span>
	offeredKey<span class="token punctuation">,</span> err <span class="token operator">:=</span> ssh<span class="token punctuation">.</span><span class="token function">ParsePublicKey</span><span class="token punctuation">(</span>offeredKeyBytes<span class="token punctuation">)</span>
	<span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
		<span class="token keyword">return</span> <span class="token string">""</span>
	<span class="token punctuation">}</span>

	data<span class="token punctuation">,</span> err <span class="token operator">:=</span> os<span class="token punctuation">.</span><span class="token function">ReadFile</span><span class="token punctuation">(</span>path<span class="token punctuation">)</span>
	<span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
		<span class="token keyword">return</span> <span class="token string">""</span>
	<span class="token punctuation">}</span>

	rest <span class="token operator">:=</span> data
	<span class="token keyword">for</span> <span class="token function">len</span><span class="token punctuation">(</span>rest<span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">0</span> <span class="token punctuation">{</span>
		key<span class="token punctuation">,</span> comment<span class="token punctuation">,</span> <span class="token boolean">_</span><span class="token punctuation">,</span> remainder<span class="token punctuation">,</span> err <span class="token operator">:=</span> ssh<span class="token punctuation">.</span><span class="token function">ParseAuthorizedKey</span><span class="token punctuation">(</span>rest<span class="token punctuation">)</span>
		<span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
			<span class="token keyword">break</span>
		<span class="token punctuation">}</span>
		<span class="token keyword">if</span> key <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token operator">&amp;&amp;</span> bytes<span class="token punctuation">.</span><span class="token function">Equal</span><span class="token punctuation">(</span>key<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> offeredKey<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
			<span class="token keyword">return</span> comment
		<span class="token punctuation">}</span>
		rest <span class="token operator">=</span> remainder
	<span class="token punctuation">}</span>

	<span class="token keyword">return</span> <span class="token string">""</span>
<span class="token punctuation">}</span>

<span class="token keyword">func</span> <span class="token function">postToSlack</span><span class="token punctuation">(</span>webhookURL<span class="token punctuation">,</span> identity<span class="token punctuation">,</span> client<span class="token punctuation">,</span> ip <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
	<span class="token keyword">if</span> webhookURL <span class="token operator">==</span> <span class="token string">""</span> <span class="token operator">||</span> identity <span class="token operator">==</span> <span class="token string">""</span> <span class="token punctuation">{</span>
		<span class="token keyword">return</span>
	<span class="token punctuation">}</span>
	payload<span class="token punctuation">,</span> err <span class="token operator">:=</span> json<span class="token punctuation">.</span><span class="token function">Marshal</span><span class="token punctuation">(</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token builtin">string</span><span class="token punctuation">{</span>
		<span class="token string">"text"</span><span class="token punctuation">:</span> fmt<span class="token punctuation">.</span><span class="token function">Sprintf</span><span class="token punctuation">(</span><span class="token string">"SSH: %s connected to *%s* from %s"</span><span class="token punctuation">,</span> identity<span class="token punctuation">,</span> client<span class="token punctuation">,</span> ip<span class="token punctuation">)</span><span class="token punctuation">,</span>
	<span class="token punctuation">}</span><span class="token punctuation">)</span>
	<span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
		<span class="token keyword">return</span>
	<span class="token punctuation">}</span>
	http<span class="token punctuation">.</span><span class="token function">Post</span><span class="token punctuation">(</span>webhookURL<span class="token punctuation">,</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span> bytes<span class="token punctuation">.</span><span class="token function">NewReader</span><span class="token punctuation">(</span>payload<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
</details>
<h2>Build the plugin</h2>
<p>Once saved, we need to compile the plugin and move to a more suitable location (<code>/opt/sshpiper/bin/</code> is where we have compiled all of our sshpiper plugins)</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> plugins/authnotify <span class="token operator">&amp;&amp;</span> go build <span class="token parameter variable">-o</span> /opt/sshpiper/bin/authnotify <span class="token builtin class-name">.</span></code></pre>
<h2>Slack webhook</h2>
<p>Next, create a file (<code>/etc/sshpiperd/slack-webhook-url</code>) which contains your Slack incoming webhook (you'll need to <a href="https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/">make an app</a> to get this)</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">nano</span> /etc/sshpiperd/slack-webhook-url</code></pre>
<p>We then need to lock down the permissions as best we can on the file</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">chown</span> sshpiper:sshpiper /etc/sshpiperd/slack-webhook-url
<span class="token function">chmod</span> <span class="token number">600</span> /etc/sshpiperd/slack-webhook-url</code></pre>
<h2>Wire up the plugin</h2>
<p>We have our sshpiper instance running as a service, but however you initialise sshpiper, add the custom plugin</p>
<p>Note: Update the path to your <code>authorized_keys</code> file, as well as the <code>slack-webhook-url</code> if different.</p>
<pre class="language-bash"><code class="language-bash">/opt/sshpiper/bin/failtoban <span class="token punctuation">\</span>
    --max-failures<span class="token operator">=</span><span class="token number">20</span> --ban-duration<span class="token operator">=</span>15m --ignore-ip<span class="token operator">=</span><span class="token number">127.0</span>.0.1 <span class="token punctuation">\</span>
    -- <span class="token punctuation">\</span>
    /opt/sshpiper/bin/authnotify --authorized-keys /etc/sshpiperd/shared/authorized_keys <span class="token punctuation">\</span>
        --slack-webhook-url-file /etc/sshpiperd/slack-webhook-url  <span class="token punctuation">\</span>
    -- <span class="token punctuation">\</span>
    /opt/sshpiper/bin/workingdir <span class="token parameter variable">--root</span> /etc/sshpiperd/hosts --strict-hostkey</code></pre>
<h2>Additions</h2>
<p>There are a few additions to our plugin we have (removed from above for simplicity) which might be worth exploring:</p>
<ul>
<li><code>authorized_keys</code> caching - cache the <code>authorized_keys</code> file based on modified time to save loading it each connection</li>
<li>Grace period - ours inspects user, ip and target and, if it matches one from the last 30 seconds it skips sending the Slack message (handy for repeated or flaky connections)</li>
</ul>
<p>If you would like more information or an example of the expanding plugin, <a href="/contact/">let me know</a>.</p>


<p><strong>Read time:</strong> 6 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>GitHub/Gitlab terminal application </title>
			<link>https://www.mikestreety.co.uk/notes/githubgitlab-terminal-application/</link>
			<pubDate>Sat, 11 Jul 2026 20:02:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/notes/githubgitlab-terminal-application/</guid>
			<description><![CDATA[
<p>A terminal user interface (TUI) for GitLab/GitHub, built on top of glab/gh. Browse issues, merge requests, pipelines, runners, and releases without leaving your terminal</p>

<p><a href="https://github.com/rcieri/glab-tui">https://github.com/rcieri/glab-tui</a></p>
<p><strong>Read time:</strong> 1 mins</p>

]]></description>
		</item>
		
		
		<item>
			<title>Set up a private packagist using a server and open source</title>
			<link>https://www.mikestreety.co.uk/blog/set-up-a-private-packagist-using-a-server-and-open-source/</link>
			<pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/set-up-a-private-packagist-using-a-server-and-open-source/</guid>
			<description><![CDATA[
<p>Publishing private composer packages is a fiddly business - especially if you want a usable UI along with it.</p>
<p>After much research I came across <a href="https://github.com/vtsykun/packeton">Packeton</a> - an open source fork of Packagist which you can run on a web server of your choosing.</p>
<p>This walkthrough sets Packeton up with Docker which requires the least amount of server setup.</p>
<p>The following how-to runs through setting it up and some hurdles I came across. It expects CLI experience and you need to be comfortable with SSH.</p>
<h2>Where to run</h2>
<p>You need a server or VPS for this - I opted for a cloud server from <a href="https://www.hetzner.com/">Hetzner</a> with Ubuntu 24 running.</p>
<h2>Server set up</h2>
<p>Update the server applications and install caddy (which allows web traffic to docker images) and docker itself.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">apt</span> update <span class="token operator">&amp;&amp;</span> <span class="token function">apt</span> upgrade <span class="token parameter variable">-y</span>
<span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> caddy
<span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://get.docker.com <span class="token operator">|</span> <span class="token function">sh</span></code></pre>
<h2>DNS</h2>
<p>Point your domain (e.g. <code>packages.yourdomain.com</code>) at the server's public IP</p>
<h2>Firewall</h2>
<p>Set up a firewall with the following inbound rules - I used the firewall built into the Hetzner control panel</p>
<table>
<thead>
<tr>
<th>Port</th>
<th>Protocol</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>22</td>
<td>TCP</td>
<td>Any IPv4, Any IPv6</td>
</tr>
<tr>
<td>80</td>
<td>TCP</td>
<td>Any IPv4, Any IPv6</td>
</tr>
<tr>
<td>443</td>
<td>TCP</td>
<td>Any IPv4, Any IPv6</td>
</tr>
</tbody>
</table>
<h2>Generate an app secret</h2>
<p>This can be run on the server or your local machine - you just need a 32 character string</p>
<pre class="language-bash"><code class="language-bash">openssl rand <span class="token parameter variable">-hex</span> <span class="token number">32</span></code></pre>
<p>Copy the output for use in the next step. Keep it static — it's used to encrypt SSH keys in the database.</p>
<h2>Create your Docker compose file</h2>
<p>I chose to keep all my Packeton-related files in <code>/opt/packeton</code>. Start off by making the folder &amp; file</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> <span class="token parameter variable">-p</span> /opt/packeton
<span class="token function">nano</span> /opt/packeton/docker-compose.yml</code></pre>
<p>This utilises a few different settings &amp; configuration. Some points worth noting</p>
<ul>
<li>This include configuration for using Mailgun (we use it on the free tier) for sending the password reset emails</li>
<li>This includes <code>watchtower</code> which will keep packeton updated</li>
</ul>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">services</span><span class="token punctuation">:</span>
  <span class="token key atrule">packeton</span><span class="token punctuation">:</span>
    <span class="token key atrule">image</span><span class="token punctuation">:</span> packeton/packeton<span class="token punctuation">:</span>latest
    <span class="token key atrule">container_name</span><span class="token punctuation">:</span> packeton
    <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
    <span class="token key atrule">environment</span><span class="token punctuation">:</span>
      <span class="token key atrule">APP_SECRET</span><span class="token punctuation">:</span> &lt;output from step 5<span class="token punctuation">></span>
      <span class="token key atrule">ADMIN_USER</span><span class="token punctuation">:</span> admin
      <span class="token key atrule">ADMIN_PASSWORD</span><span class="token punctuation">:</span> changeme
      <span class="token key atrule">ADMIN_EMAIL</span><span class="token punctuation">:</span> you@yourdomain.com
      <span class="token key atrule">PACKAGIST_DIST_HOST</span><span class="token punctuation">:</span> https<span class="token punctuation">:</span>//packages.yourdomain.com
      <span class="token key atrule">TRUSTED_PROXIES</span><span class="token punctuation">:</span> 127.0.0.1
      <span class="token key atrule">MAILER_DSN</span><span class="token punctuation">:</span> smtp<span class="token punctuation">:</span>//you%40yourdomain.com<span class="token punctuation">:</span>PASSWORD@smtp.eu.mailgun.org<span class="token punctuation">:</span><span class="token number">587</span>
      <span class="token key atrule">MAILER_FROM</span><span class="token punctuation">:</span> Your Name &lt;you@yourdomain.com<span class="token punctuation">></span>
    <span class="token key atrule">ports</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> <span class="token string">'127.0.0.1:8080:80'</span>
    <span class="token key atrule">volumes</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> ./data<span class="token punctuation">:</span>/data
  <span class="token key atrule">watchtower</span><span class="token punctuation">:</span>
    <span class="token key atrule">image</span><span class="token punctuation">:</span> containrrr/watchtower
    <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
    <span class="token key atrule">environment</span><span class="token punctuation">:</span>
      <span class="token key atrule">DOCKER_API_VERSION</span><span class="token punctuation">:</span> <span class="token string">"1.40"</span>
    <span class="token key atrule">volumes</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> /var/run/docker.sock<span class="token punctuation">:</span>/var/run/docker.sock
    <span class="token key atrule">command</span><span class="token punctuation">:</span> <span class="token punctuation">-</span><span class="token punctuation">-</span>interval 86400</code></pre>
<div class="info"><strong>Note:</strong> <code>ADMIN_USER</code> and <code>ADMIN_PASSWORD</code> only apply on first run. Change the password afterwards via the console (see Post-setup).</div>
<div class="info"><strong>Note:</strong> If using Mailgun, make sure you use the EU SMTP host (<code>smtp.eu.mailgun.org</code>) if your domain is on the EU region. Use the full email address as the SMTP username, URL-encoding the <code>@</code> as <code>%40</code>.</div>
<h2>Configure Caddy</h2>
<p>Caddy allows a domain name to be forwarded to a running docker container.</p>
<p>Replace the entire contents of <code>/etc/caddy/Caddyfile</code> with:</p>
<pre><code>packages.yourdomain.com {
    reverse_proxy localhost:8080
}
</code></pre>
<p>Then reload caddy:</p>
<pre class="language-bash"><code class="language-bash">systemctl reload caddy</code></pre>
<h2>Start Packeton</h2>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> /opt/packeton
<span class="token function">docker</span> compose up <span class="token parameter variable">-d</span></code></pre>
<h2>Verify it all works</h2>
<p>Visit <code>https://packages.yourdomain.com</code> and log in with the admin credentials you set.</p>
<h2>Post-setup</h2>
<h3>Change the admin password</h3>
<p>The <code>ADMIN_PASSWORD</code> env var only applies on first run. Change it via the console:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> <span class="token builtin class-name">exec</span> <span class="token parameter variable">-it</span> packeton bin/console packagist:user:manager admin <span class="token parameter variable">--password</span><span class="token operator">=</span>newpassword</code></pre>
<h3>Create additional admin users</h3>
<pre class="language-bash"><code class="language-bash"><span class="token function">docker</span> <span class="token builtin class-name">exec</span> <span class="token parameter variable">-it</span> packeton bin/console packagist:user:manager newusername <span class="token parameter variable">--password</span><span class="token operator">=</span>newpassword <span class="token parameter variable">--admin</span> --no-interaction</code></pre>
<h3>Configure GitLab OAuth</h3>
<p>First, create a GitLab OAuth application at <code>https://gitlab.com/-/profile/applications</code>:</p>
<ul>
<li><strong>Redirect URIs:</strong><pre><code>https://packages.yourdomain.com/oauth2/gitlab/install
https://packages.yourdomain.com/oauth2/gitlab/check
</code></pre>
</li>
<li><strong>Scopes:</strong> <code>api</code>, <code>read_user</code>, <code>read_repository</code></li>
</ul>
<p>Then create <code>/opt/packeton/data/config.yaml</code>:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">packeton</span><span class="token punctuation">:</span>
    <span class="token key atrule">integrations</span><span class="token punctuation">:</span>
        <span class="token key atrule">gitlab</span><span class="token punctuation">:</span>
            <span class="token key atrule">base_url</span><span class="token punctuation">:</span> <span class="token string">'https://gitlab.com/'</span>
            <span class="token key atrule">clone_preference</span><span class="token punctuation">:</span> <span class="token string">'clone_https'</span>
            <span class="token key atrule">gitlab</span><span class="token punctuation">:</span>
                <span class="token key atrule">client_id</span><span class="token punctuation">:</span> <span class="token string">'xxx'</span>
                <span class="token key atrule">client_secret</span><span class="token punctuation">:</span> <span class="token string">'xxx'</span></code></pre>
<p>Restart Packeton to apply:</p>
<pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> /opt/packeton
<span class="token function">docker</span> compose restart packeton</code></pre>
<p>Then go to the Packeton integrations page in the UI and click Install Integration, then Connect to complete the OAuth flow.</p>
<h2>Bonus Notes</h2>
<h3>Data</h3>
<p>All Packeton data lives in <code>/opt/packeton/data</code> on the host, mapped to <code>/data</code> inside the container. Back this directory up — it contains the database, config, and any stored artifacts.</p>
<h3>Ongoing maintenance</h3>
<p>Watchtower checks for a new <code>packeton/packeton:latest</code> image daily and recreates the container automatically. No action needed.</p>
<p>Monitor the <a href="https://github.com/vtsykun/packeton/releases">Packeton releases</a> for any updates that require manual migration steps before they land.</p>


<p><strong>Read time:</strong> 4 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>GitButler</title>
			<link>https://www.mikestreety.co.uk/notes/gitbutler/</link>
			<pubDate>Wed, 01 Apr 2026 16:23:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/notes/gitbutler/</guid>
			<description><![CDATA[
<p>Interesting Git tool allowing for Parallel branches and with a strong AI integration</p>

<p><a href="https://gitbutler.com/">https://gitbutler.com/</a></p>
<p><strong>Read time:</strong> 1 mins</p>

]]></description>
		</item>
		
		
		<item>
			<title>CSS Refactoring with an AI Safety Net</title>
			<link>https://www.mikestreety.co.uk/notes/css-refactoring-with-an-ai-safety-net/</link>
			<pubDate>Thu, 26 Mar 2026 11:49:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/notes/css-refactoring-with-an-ai-safety-net/</guid>
			<description><![CDATA[
<p>Claude Code was used to refactor a messy CSS codebase into a clean architecture across 7 phases, with a Playwright script capturing screenshots of 9 distinct app states before and after each phase.</p>

<p><a href="https://danielabaron.me/blog/css-refactoring-with-an-ai-safety-net/">https://danielabaron.me/blog/css-refactoring-with-an-ai-safety-net/</a></p>
<p><strong>Read time:</strong> 1 mins</p>

]]></description>
		</item>
		
		
		<item>
			<title>Checking your websites with the BLAT test</title>
			<link>https://www.mikestreety.co.uk/blog/checking-your-websites-with-the-blat-test/</link>
			<pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/checking-your-websites-with-the-blat-test/</guid>
			<description><![CDATA[
<p>You've been staring at the same project for weeks. Your design eye is shot and the finish line is in sight. You can't see the wood for the trees.</p>
<p>This is exactly when you need fresh eyes. At Liquid Light, that's what the BLAT test is for.</p>
<p>BLAT is a timeboxed, no-holds-barred review where everyone gets a go at one of our nearly finished websites. Each person gets an hour to click around, prod things, and use the site as a real person would. The goal is to find holes.</p>
<p>It might be a personal preference. It might be a niggle. It might be the tiniest of nitpicks. Doesn't matter. It goes on the list.</p>
<p>The project manager then reviews the list and decides what to action, postpone, or drop. It's not personal. It's priorities.</p>
<p>Things that tend to come up:</p>
<ul>
<li>Spacing between two particular elements</li>
<li>Accessibility of a link in a specific context</li>
<li>Unexpected (or missing) behaviour from an interaction</li>
<li>Image sizes affecting performance</li>
<li>Print styles nobody tested</li>
<li>Odd flows between pages</li>
<li>Future website improvements or additions</li>
</ul>
<p>Anything goes, as long as the note includes:</p>
<ul>
<li>A link</li>
<li>A description</li>
<li>A screenshot where possible</li>
</ul>
<p>Side note: No, BLAT doesn't actually stand for anything. We all know what it means (although out of all the recommendations from Claude my favourite was <strong>B</strong>rutally <strong>L</strong>ook <strong>A</strong>t <strong>T</strong>hings )</p>


<p><strong>Read time:</strong> 1 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>Completely remove DDEV from your computer</title>
			<link>https://www.mikestreety.co.uk/blog/completely-remove-ddev-from-your-computer/</link>
			<pubDate>Fri, 06 Feb 2026 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/completely-remove-ddev-from-your-computer/</guid>
			<description><![CDATA[
<p>I recently ran into an issue with DDEV 1.25 and was upgrading and downgrading between the two versions to test, check and verify.</p>
<p>Eventually, my DDEV got confused and started producing 404s. With mixed version images &amp; config, I wanted to remove everything and start again.</p>
<p>With the help of Claude, I created a bash script which will run through and delete every DDEV related configuration.</p>
<p><a href="https://gist.github.com/mikestreety/07d531b346ab8ce9c62ec655dd4274a4" class="button">Completely remove DDEV</a></p>
<h2>Steps</h2>
<ol>
<li>Copy the contents or download the zip</li>
<li>Make the file executeable - <code>cd path/to/file</code> and <code>chmod +x ./remove-ddev.sh</code></li>
<li>Run the script <code>./remove-ddev.sh</code> - there is <code>--help</code> and <code>--dry-run</code> flags available</li>
</ol>


<p><strong>Read time:</strong> 1 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>Playwriter - Browser Automation MCP</title>
			<link>https://www.mikestreety.co.uk/notes/playwriter-browser-automation-mcp/</link>
			<pubDate>Tue, 06 Jan 2026 15:53:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/notes/playwriter-browser-automation-mcp/</guid>
			<description><![CDATA[
<p>Like Playwright MCP but via extension. 90% less context window. 10x more capable (full playwright API)</p>

<p><a href="https://github.com/remorses/playwriter">https://github.com/remorses/playwriter</a></p>
<p><strong>Read time:</strong> 1 mins</p>

]]></description>
		</item>
		
		
		<item>
			<title>2025 In Review</title>
			<link>https://www.mikestreety.co.uk/blog/2025-in-review/</link>
			<pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/blog/2025-in-review/</guid>
			<description><![CDATA[
<p>Like 2024, 2025 passed without major incident or upheaval. All in all, it was an enjoyable year - seeing some firsts for us all.</p>
<h2>Life</h2>
<p>Plenty of goings-on with the Street family this year. Ruby, our youngest, started school which meant another shift in routines and schedules. Alfie moved up to Beavers and Ruby started Squirrels, which now means I'm the only one of our family to not be currently invested in a scouting section.</p>
<p>Alfie has become a classic &quot;kid&quot; and discovered Minecraft. He'd been talking about it at school and we finally gave in and bought a copy for the XBOX. I've also enjoyed playing it, trying to actually build things and thinking about layout (although it's still enjoyable to build a tower of TNT and blog it up).</p>
<p>There were some small home improvements - I redecorated the garden office and painted the &quot;TV corner&quot; of the lounge with matching bookcases. The biggest change was the demolition of the back garden patio and building of a deck - carried out by my dad and me.</p>
<p>To finish off the year, our car decided to give us a Christmas present of breaking. We couldn't get it booked in until the 5th Jan, however my mother-in-law leant us her car for the festive period which meant Christmas was saved.</p>
<h2>Trip and Holidays</h2>
<p>For our main holiday this year we took the kids to Disneyland Paris. It was a first for me - going on the channel tunnel and driving on foreign soil in my own car. Disneyland was hectic and expensive and fun and chaos. We went with my wife's family which did mean we had the opportunity to leave the kids with each other and head off to the big rides.</p>
<p>Little trips included taking Ruby to London for the first time, a boat trip out to the <a href="https://en.wikipedia.org/wiki/Rampion_Wind_Farm">Rampion Wind Farm</a> and a visit to Brooklands Museum in Weybridge.</p>
<p>We also spent a week in a static caravan in the New Forest - it was a classic &quot;caravan park&quot; style holiday, with on-site swimming, golf and evening entertainment. We sprinkled in some day trips to Paultons Park and a miniature steam railway. We also found a pub round the corner with an <em>incredible</em> outdoor play area for the kids (while Chilly and I kicked back with a book and a beer).</p>
<p>My attendance to gigs sky-rocketed this year as I got the taste for it last year. This year saw a few shows with the kids along with seeing OneRepublic (my first trip to the 02 since it was the millennium dome), Nizlopi (a birthday present), Self Esteem and it seems I can't go through a year without seeing Bastille. The last gig of the year was with my mum and family to see Stereophonics at the 02 again.</p>
<h2>Stats Analysis</h2>
<p><a href="/stats/">Visit the stats page</a>.</p>
<p>Cycling was a big part this year - recording the highest ever number of miles done in a year since I started recording. A big part of this was the turbo trainer I purchased at the end of last year, with just 85 miles separating virtual and real-world bike rides (and I rode more virtual miles then I did on an eBike)!</p>
<p>I also hit a few big rides this year, doing the London to Brighton bike ride (for the third time), a 65 mile bike ride in August and 70 miles around the Isel of Wight in October. I was pretty happy that I was able to pull these out the bag without <em>much</em> concious training. It seems cycling to and from work combined with the turbo proved to be a pretty effective training plan. I'd like to hit <strong>4000 miles</strong> again in 2026.</p>
<p>My Geocaching suffered this year - only finding 2 at the beginning of the year. I need to get some caching days booked in in 2026 to get those numbers back up. I feel like <strong>75 Geocaches</strong> is a good target.</p>
<p>Everything else, such as blog posts, steps and music streams stayed steady.</p>


<p><strong>Read time:</strong> 3 mins</p>
<p><strong>Tags:</strong></p>
]]></description>
		</item>
		
		
		<item>
			<title>Oh My Posh</title>
			<link>https://www.mikestreety.co.uk/notes/oh-my-posh/</link>
			<pubDate>Tue, 30 Dec 2025 08:36:00 GMT</pubDate>
			<guid>https://www.mikestreety.co.uk/notes/oh-my-posh/</guid>
			<description><![CDATA[
<p>The most customizable and fastest prompt engine for any shell</p>

<p><a href="https://ohmyposh.dev/">https://ohmyposh.dev/</a></p>
<p><strong>Read time:</strong> 1 mins</p>

]]></description>
		</item>
		

	</channel>
</rss>
