<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://freddysblog.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://freddysblog.com/" rel="alternate" type="text/html" /><updated>2026-08-19T10:10:38+02:00</updated><id>https://freddysblog.com/feed.xml</id><title type="html">Freddys blog</title><subtitle>Learn something new… – then Teach someone!</subtitle><author><name>Freddy Kristiansen</name></author><entry><title type="html">New feature in Fkh: A KeyVault and secrets handling</title><link href="https://freddysblog.com/2026/08/19/new-feature-in-fkh-keyvault-and-secrets-handling/" rel="alternate" type="text/html" title="New feature in Fkh: A KeyVault and secrets handling" /><published>2026-08-19T09:00:00+02:00</published><updated>2026-08-19T09:00:00+02:00</updated><id>https://freddysblog.com/2026/08/19/new-feature-in-fkh-keyvault-and-secrets-handling</id><content type="html" xml:base="https://freddysblog.com/2026/08/19/new-feature-in-fkh-keyvault-and-secrets-handling/"><![CDATA[<p>This post is about a new feature that lands when you update to the latest preview - a <strong>KeyVault</strong> of your own and proper <strong>secrets handling</strong> in Fkh.</p>

<p><img src="/assets/images/2026-08-19-new-feature-in-fkh-keyvault-and-secrets-handling/2026-08-19-08-45-22.png" alt="Fkh secrets in the KeyVault" /></p>

<h2 id="a-keyvault-of-your-own">A KeyVault of your own</h2>

<p>Up until now, Fkh has been very deliberate about <strong>not</strong> storing humanly created secrets. As I described in <a href="/2026/08/01/the-security-model-of-fkh/">The security model of Fkh</a>, everything runs on managed identities, federated credentials and just-in-time access - and that principle hasn’t changed.</p>

<p>What has changed is that there are situations where you genuinely need a secret. A container admin password is the obvious example, but there are plenty of others - license references, tokens for third party services, connection details you don’t want to type over and over again.</p>

<p>When you update to the latest preview, Fkh now deploys an <strong>Azure Key Vault</strong> as part of your deployment. Just like everything else in Fkh, it lives in <strong>your own Azure subscription</strong>, and that is where your secrets are stored - under your control.</p>

<h2 id="standard-or-premium">Standard or Premium</h2>

<p>The Key Vault comes in two flavours, and you get to choose which one you want when you deploy:</p>

<ul>
  <li><strong>Standard</strong> (the default) - a standard, software-protected Key Vault. This is more than enough for the vast majority of scenarios.</li>
  <li><strong>Premium</strong> - backed by hardware security modules (HSM) for those who have compliance or policy requirements that mandate HSM-protected keys (needed if you want to use this for code signing).</li>
</ul>

<p>You control this with a single configuration option in your deployment (<code class="language-plaintext highlighter-rouge">keyvault_sku</code>). If you don’t set anything, you get <strong>Standard</strong>, so there is nothing you have to do to get going.</p>

<p>Because it is RBAC-enabled and wired directly into the Fkh backend, you never have to hand out access to the Key Vault yourself. The backend authenticates and authorizes you exactly like it does for every other operation - GitHub for who you are, GitHub team membership for what you are allowed to do.</p>

<h2 id="managing-secrets-from-vs-code">Managing secrets from VS Code</h2>

<p>Once you are on the latest preview, you get three new operations to work with secrets, and they are all available right where you already work - in <strong>VS Code</strong>:</p>

<ul>
  <li><strong>List secrets</strong> - see which secrets exist</li>
  <li><strong>Set a secret</strong> - create, update or remove a secret value</li>
  <li><strong>Get a secret</strong> - read a secret value back</li>
</ul>

<p><img src="/assets/images/2026-08-19-new-feature-in-fkh-keyvault-and-secrets-handling/2026-08-19-08-52-45.png" alt="Listing and setting secrets from VS Code" /></p>

<p>These are just Fkh commands, so you invoke them the same way as everything else - from the command palette or the Fkh views. When you set a secret, the value is stored in the Key Vault in your subscription; when you read one back, the value is fetched on demand.</p>

<p>Secrets can be <strong>your own personal secrets</strong> or <strong>shared across everyone in your organization</strong>, so admins can maintain a common set of secrets while individuals can override and/or have their own. When a secret is looked up, Fkh checks your personal secrets first and falls back to the shared ones.</p>

<h2 id="and-from-the-cli">…and from the CLI</h2>

<p>If you prefer scripting - or you want to use secrets from a pipeline - the exact same operations are available in the <strong>Fkh CLI</strong>. As I explained in <a href="/2026/08/04/the-fkh-cli/">The Fkh CLI</a>, the CLI and VS Code are just different faces on the same backend, so nothing new is happening here - it is the same functionality, exposed on the command line.</p>

<p>A few examples of what that looks like:</p>

<pre><code class="language-pwsh"># List the secrets you have access to
fkh listsecrets

# Set (create or update) a secret
fkh setsecret --name adminPassword --secret (Read-Host "adminPassword")

# Read a secret back
fkh getsecret --name adminPassword
</code></pre>

<p>In a future version, you will also be able to use this KeyVault for your AL-Go repositories and have one single source for all your secrets, right in your own Azure Subscription.</p>

<h2 id="using-secrets-as-values-for-parameters">Using secrets as values for parameters</h2>

<p>Storing and reading secrets is useless in itself, you need to be able to use the secrets. This first version wires secrets into the parameters of the operations you already run.</p>

<p>Take <strong>CreateContainer</strong> as an example. Creating a container needs an admin password, and until now you had to provide it every time. With secrets in place, you can store the password once and then reference it in the parameter instead of typing the value.</p>

<p>Anywhere a parameter value is written as <code class="language-plaintext highlighter-rouge">@secretName@</code>, Fkh substitutes it with the corresponding secret from the Key Vault before the operation runs. So instead of putting a real password into the <code class="language-plaintext highlighter-rouge">adminPassword</code> parameter of CreateContainer, you simply put:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@adminPassword@
</code></pre></div></div>

<p>Fkh resolves <code class="language-plaintext highlighter-rouge">@adminPassword@</code> to the stored value at the moment it creates the container. The same mechanism works for any parameter, not just <code class="language-plaintext highlighter-rouge">adminPassword</code>.</p>

<h2 id="defaulting-parameters-with-secrets">Defaulting parameters with secrets</h2>

<p>As explained in <a href="/2026/07/31/creating-containers-in-vs-code-using-fkh/">Creating containers in VS Code using Fkh</a> you can default parameters for container creation and also here, you can use secrets. If you specify:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"CreateContainer.adminPassword"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@adminPassword@"</span><span class="w">
</span></code></pre></div></div>

<p>In your AL-Go settings, the admin password will now be read from the keyvault and inserted right into the Create Container dialog and you can press the Show/Hide button to see the value.</p>

<p><img src="/assets/images/2026-08-19-new-feature-in-fkh-keyvault-and-secrets-handling/2026-08-19-09-01-17.png" alt="Defaulting parameters" /></p>

<blockquote>
  <p><strong>Note:</strong> If you haven’t set up the secret, the mandatory field will be there for you to fill out.</p>
</blockquote>

<h2 id="the-foundation-for-more">The foundation for more</h2>

<p>As handy as list/set/get and parameter defaulting are on their own, the most interesting part is what this <strong>enables</strong>. Having a proper, trusted Key Vault in every Fkh deployment is the foundation for a couple of things I have on my mind:</p>

<ul>
  <li>
    <p><strong>Secrets handling for AL-Go for GitHub</strong> - letting Fkh act as the secure store that AL-Go for GitHub reaches into for the secrets it needs, so your pipelines can get what they need without secrets being copied around.</p>
  </li>
  <li>
    <p><strong>KeyVault access for containers</strong> - giving containers a controlled way to reach into the Key Vault, so an app running inside a container can consume secrets the same secure way. This actually comes from <a href="https://github.com/Freddy-DK/Fkh/issues/69">Issue 69</a>.</p>
  </li>
</ul>

<p>And probably more scenarios will pop up. Neither of those is in this preview - this post is about the foundation that makes them possible. Whether and when they show up depends a lot on what partners and customers using Fkh actually need, so if this is interesting to you, let me know.</p>

<h2 id="to-use-ai-or-not-to-use-ai-thats-the-question">To use AI or not to use AI, that’s the question</h2>

<p>During development of Fkh, I obviously use AI. I could have asked AI to solve Issue 69 - and I am fairly convinced that it would have found a way to fix that, but I am also convinced that AI wouldn’t do this the way I wanted it done, using a sound and reusable architecture, which can be used as the foundation for other things. AI has never before written a tool like Fkh - and it will probably never do so. It doesn’t understand the use cases, nor the complexity of this tool.</p>

<p>It isn’t any different from asking a very skilled junior developer with no experience to build a tool like this - they will fail.</p>

<p>I use AI as my team of engineers, who implement the features I have architected and described.</p>

<p>Update to the latest preview to try it out, and take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><category term="KeyVault" /><category term="Security" /><summary type="html"><![CDATA[This post is about a new feature that lands when you update to the latest preview - a KeyVault of your own and proper secrets handling in Fkh.]]></summary></entry><entry><title type="html">Which Tasks Should You Delegate to AI Agents?</title><link href="https://freddysblog.com/2026/08/17/which-tasks-should-you-delegate-to-ai-agents/" rel="alternate" type="text/html" title="Which Tasks Should You Delegate to AI Agents?" /><published>2026-08-17T11:00:00+02:00</published><updated>2026-08-17T11:00:00+02:00</updated><id>https://freddysblog.com/2026/08/17/which-tasks-should-you-delegate-to-ai-agents</id><content type="html" xml:base="https://freddysblog.com/2026/08/17/which-tasks-should-you-delegate-to-ai-agents/"><![CDATA[<p>Asking which jobs AI will replace is probably the wrong question. A job is a bundle of dozens of tasks, held together by a title, a team, and a person who takes responsibility for the outcome. An agent doesn’t take a job – it takes a task, does it, and hands the result back. So the question worth asking is:</p>

<blockquote>
  <p><strong>Which tasks should I delegate to an AI agent – and which ones should I keep, at least for now?</strong></p>
</blockquote>

<p><img src="/assets/images/2026-08-17-which-tasks-should-you-delegate-to-ai-agents/2026-08-17-09-46-02.png" alt="Diagram showing the four dimensions for delegating tasks to AI agents" /></p>

<p>What follows is a framework for answering that – simple enough to keep in your head, but sharp enough to actually change your decisions. It has four dimensions, each with a question behind it, a way to score them, a rule that ties them together, and three outcomes it produces.</p>

<h2 id="why-not-just-use-ai-for-everything">Why not just “use AI for everything”?</h2>

<p>The naive approach is: try to delegate everything, and keep whatever fails. That works, sort of, but it’s expensive and occasionally dangerous. Some failures are cheap and obvious – the agent writes a bad paragraph, you notice, you rewrite it. Other failures are silent and costly – the agent confidently produces a number that is wrong, nobody checks it, and it ends up in a report that drives a decision.</p>

<p>The reason “use AI for everything” is a bad rule is that the tasks where delegation goes wrong are exactly the tasks where you <em>can’t easily tell it went wrong</em>. So we need something better than gut feeling. We need a way to look at a task <strong>before</strong> we delegate it and ask: is this a good candidate, a candidate with conditions, or something I shouldn’t hand over yet?</p>

<p>That’s what this framework does.</p>

<h2 id="the-core-idea-four-dimensions">The core idea: four dimensions</h2>

<p>Every task can be scored along four dimensions. Each dimension answers exactly one question:</p>

<table>
  <thead>
    <tr>
      <th>Dimension</th>
      <th>The question it answers</th>
      <th>What it measures</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Economic attractiveness</strong></td>
      <td><em>Is it worth delegating?</em></td>
      <td>Value</td>
    </tr>
    <tr>
      <td><strong>Agent feasibility</strong></td>
      <td><em>Can an agent realistically do it?</em></td>
      <td>Capability</td>
    </tr>
    <tr>
      <td><strong>Verifiability</strong></td>
      <td><em>Can we tell whether it did it correctly?</em></td>
      <td>Observability</td>
    </tr>
    <tr>
      <td><strong>Risk &amp; responsibility</strong></td>
      <td><em>Can we safely allow it to do this?</em></td>
      <td>Safety</td>
    </tr>
  </tbody>
</table>

<p>A task can score brilliantly on one and terribly on another, and that’s precisely why you need all four – any single one of them, on its own, will mislead you.</p>

<p>For each dimension I’ll give the underlying question, the sub-questions that let you score it, and what a high and a low score actually look like.</p>

<h2 id="dimension-1---economic-attractiveness">Dimension 1 - Economic attractiveness</h2>

<p><strong>The question: <em>Is it worth delegating?</em></strong></p>

<p>This is the dimension people usually start with, and it’s the easiest to feel intuitively. If a task is rare, trivial, and takes you ten seconds, there is no point building an agent for it. If a task is frequent, tedious, and eats hours of expensive human attention every week, it’s screaming to be delegated.</p>

<p>Sub-questions that let you score it:</p>

<table>
  <thead>
    <tr>
      <th>Sub-question</th>
      <th>What it asks</th>
      <th>Low → High</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Frequency</strong></td>
      <td>How often does this task occur?</td>
      <td>Once a year → Many times a day</td>
    </tr>
    <tr>
      <td><strong>Time cost</strong></td>
      <td>How long does it take a human each time?</td>
      <td>Seconds → Hours</td>
    </tr>
    <tr>
      <td><strong>Skill level tied up</strong></td>
      <td>Whose time does it consume?</td>
      <td>Cheap/junior → Expensive specialist</td>
    </tr>
    <tr>
      <td><strong>Volume &amp; scalability</strong></td>
      <td>Would you do more of it if it were cheap?</td>
      <td>Fixed, low volume → Unlocks new scale</td>
    </tr>
    <tr>
      <td><strong>Consistency value</strong></td>
      <td>Does doing it the same way every time help?</td>
      <td>Doesn’t matter → Highly valuable</td>
    </tr>
    <tr>
      <td><strong>Setup cost</strong></td>
      <td>How much effort to get an agent doing it?</td>
      <td>Bespoke integration → Point it and go</td>
    </tr>
    <tr>
      <td><strong>Running cost</strong></td>
      <td>What does each run cost to operate?</td>
      <td>Expensive per run → Negligible</td>
    </tr>
  </tbody>
</table>

<p><strong>A high score</strong> looks like: a frequent, time-consuming, tedious task currently done by people whose time is better spent elsewhere, where doing more of it (or doing it more consistently) would genuinely help – and where the cost to set up and run an agent is comfortably outweighed by that value.</p>

<p><strong>A low score</strong> looks like: a rare, quick, low-effort task where the setup cost of delegation will never pay itself back. These aren’t <em>unsuitable</em> for agents – they’re just not <em>worth</em> the effort.</p>

<blockquote>
  <p>Important: a high economic score is what makes a task <strong>interesting</strong>, but on its own it tells you nothing about whether you <em>should</em> delegate. Plenty of extremely attractive tasks are terrible delegation candidates because they fail one of the other three dimensions. That’s the trap this dimension sets, and the next three are how you avoid it.</p>
</blockquote>

<h2 id="dimension-2---agent-feasibility">Dimension 2 - Agent feasibility</h2>

<p><strong>The question: <em>Can an agent realistically do it?</em></strong></p>

<p>Forget for a moment whether it’s worth it or safe – can the technology, as it exists today, actually perform this task to an acceptable standard?</p>

<p>Sub-questions that let you score it:</p>

<table>
  <thead>
    <tr>
      <th>Sub-question</th>
      <th>What it asks</th>
      <th>Low → High</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Clarity of the task</strong></td>
      <td>Can it be described unambiguously?</td>
      <td>Vague &amp; judgment-heavy → Crisp &amp; well-defined</td>
    </tr>
    <tr>
      <td><strong>Availability of context</strong></td>
      <td>Can the agent reach what it needs to know?</td>
      <td>Tacit → Accessible</td>
    </tr>
    <tr>
      <td><strong>Tool access</strong></td>
      <td>Can it reach the tools to actually do the work?</td>
      <td>No tools → Fully wired up</td>
    </tr>
    <tr>
      <td><strong>Bounded scope</strong></td>
      <td>Is the task self-contained?</td>
      <td>Open-ended &amp; sprawling → Narrow &amp; bounded</td>
    </tr>
    <tr>
      <td><strong>Tolerance for imperfection</strong></td>
      <td>Must it be right every time?</td>
      <td>Zero margin → “Usually great” is fine</td>
    </tr>
  </tbody>
</table>

<p><strong>A high score</strong> looks like: a clearly-describable task, with all necessary context and tools available, bounded in scope, where the current generation of models genuinely performs well.</p>

<p><strong>A low score</strong> looks like: a fuzzy, open-ended task that depends on context the agent can’t reach, or requires a level of reliability the technology can’t yet deliver.</p>

<blockquote>
  <p>The crucial thing about feasibility is that it is the <strong>most time-dependent</strong> of the four dimensions. A task that scores low today can score high in six months – not because you did anything, but because models improved, tools matured, or the context finally got connected. We’ll come back to this when we talk about <em>“not yet.”</em></p>
</blockquote>

<h2 id="dimension-3---verifiability">Dimension 3 - Verifiability</h2>

<p><strong>The question: <em>Can we tell whether it did it correctly?</em></strong></p>

<p>People consistently underrate this one – and it’s often what turns an obvious “yes” into a cautious “not yet.”</p>

<p>Verifiability is about <strong>observability of correctness</strong>. After the agent finishes, how do you know whether the result is good? And more pointedly: how <em>expensive</em> is it to find that out?</p>

<p>Sub-questions that let you score it:</p>

<table>
  <thead>
    <tr>
      <th>Sub-question</th>
      <th>What it asks</th>
      <th>Low → High</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Existence of a ground truth</strong></td>
      <td>Is there a correct answer to check against?</td>
      <td>Subjective → Definite right answer</td>
    </tr>
    <tr>
      <td><strong>Cost of checking</strong></td>
      <td>How much effort is verification vs. doing it?</td>
      <td>As costly as doing → Far cheaper</td>
    </tr>
    <tr>
      <td><strong>Automatability of the check</strong></td>
      <td>Can correctness be verified automatically?</td>
      <td>Manual review only → Fully automatable</td>
    </tr>
    <tr>
      <td><strong>Detectability of subtle errors</strong></td>
      <td>Will a wrong answer look wrong?</td>
      <td>Plausibly hidden → Glaringly obvious</td>
    </tr>
    <tr>
      <td><strong>Feedback loop</strong></td>
      <td>How fast do you find out it’s wrong?</td>
      <td>Much later → Immediately</td>
    </tr>
  </tbody>
</table>

<p><strong>A high score</strong> looks like: a task with a clear notion of correctness, where checking is cheap or automatable, and where mistakes announce themselves loudly.</p>

<p><strong>A low score</strong> looks like: a task where correctness is subjective or hidden, where verification is as much work as the task itself, and where a wrong result looks indistinguishable from a right one.</p>

<blockquote>
  <p>Here’s why verifiability is such a powerful gate: <strong>an agent you can’t check is an agent you have to trust blindly.</strong> Blind trust is fine when the stakes are low and fine when the model is perfect – and it’s a disaster in every case in between. If you can’t verify, you’re not really delegating; you’re gambling.</p>
</blockquote>

<h2 id="dimension-4---risk--responsibility">Dimension 4 - Risk &amp; responsibility</h2>

<p><strong>The question: <em>Can we safely allow it to do this?</em></strong></p>

<p>The final dimension is about <strong>consequences and accountability</strong>. Even a feasible, verifiable, economically attractive task can be one you shouldn’t hand to an autonomous agent, because of what happens if it goes wrong – or because of who is answerable when it does.</p>

<p>Sub-questions that let you score it (here, <em>high</em> means low risk – safe to delegate):</p>

<table>
  <thead>
    <tr>
      <th>Sub-question</th>
      <th>What it asks</th>
      <th>Low → High</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Blast radius</strong></td>
      <td>How bad is a mistake?</td>
      <td>Sends money/deletes data → An unread draft</td>
    </tr>
    <tr>
      <td><strong>Reversibility</strong></td>
      <td>Can the action be undone?</td>
      <td>Irreversible → Fully reversible</td>
    </tr>
    <tr>
      <td><strong>Accountability</strong></td>
      <td>Must a human stay answerable?</td>
      <td>Legally required → No external stakes</td>
    </tr>
    <tr>
      <td><strong>Human participation</strong></td>
      <td>Is a person’s judgment, relationship, or leadership itself part of the value?</td>
      <td>Essential → Irrelevant</td>
    </tr>
    <tr>
      <td><strong>Sensitive access</strong></td>
      <td>Does it touch confidential data or critical systems?</td>
      <td>Highly sensitive → None</td>
    </tr>
    <tr>
      <td><strong>Failure containment</strong></td>
      <td>Can one mistake cascade?</td>
      <td>Chain reaction → Fully contained</td>
    </tr>
  </tbody>
</table>

<p><strong>A high score</strong> (meaning <em>low</em> risk) looks like: a task whose mistakes are minor, reversible, and contained, that doesn’t touch sensitive systems, and where no external party is harmed if it’s wrong.</p>

<p><strong>A low score</strong> (meaning <em>high</em> risk) looks like: a task that acts irreversibly on the outside world, touches sensitive data or critical systems, or requires a human to remain legally or ethically accountable.</p>

<blockquote>
  <p>A useful way to think about this dimension: verifiability asks <em>“will I notice the mistake?”</em>, while risk asks <em>“how much does the mistake cost me before I can undo it?”</em> They’re related, but distinct – and a task can pass one while failing the other.</p>
</blockquote>

<h2 id="the-gating-rule">The gating rule</h2>

<p><strong>Delegation is not about the average of the four scores.</strong></p>

<p>It’s tempting to add the four scores up, take an average, and delegate anything above some line. That’s exactly the wrong thing to do, because it lets a brilliant score on one dimension paper over a fatal weakness on another. Instead, look at the <strong>weakest</strong> dimension:</p>

<blockquote>
  <p><strong>The weakest dimension determines the maximum level of autonomy you can safely grant.</strong></p>
</blockquote>

<p>That turns the four scores into a simple scale:</p>

<ul>
  <li><strong>Very weak on any dimension → Not yet.</strong> Don’t delegate this autonomously.</li>
  <li><strong>Medium on the weakest → Delegate with guardrails.</strong> Hand it over, but with human review or restricted autonomy.</li>
  <li><strong>Strong across all four → Delegate autonomously.</strong></li>
</ul>

<p>The average might look great; it doesn’t matter. The weakest link sets the ceiling.</p>

<p>This is what turns four vague feelings into a decision. You’re no longer asking <em>“does this feel like a good AI task?”</em> You’re asking four specific questions, and the weakest answer tells you exactly how much autonomy the task can take.</p>

<h2 id="not-suitable-vs-not-yet-suitable">Not suitable vs. not yet suitable</h2>

<p>The gating rule gives you something subtle and valuable: a distinction between <strong>“not suitable”</strong> and <strong>“not yet suitable.”</strong></p>

<p>When a task fails on a dimension, don’t just reject it – ask <em>why</em>, and <em>whether that reason is permanent</em>. Because the four dimensions age very differently:</p>

<ul>
  <li>A low <strong>economic</strong> score is usually stable. If the task is rare and trivial, it will probably stay rare and trivial. This is the closest thing to “not suitable, period.”</li>
  <li>A low <strong>feasibility</strong> score is often temporary. Models get better. Tools mature. The context that wasn’t available gets connected. Today’s “the agent can’t do this” quietly becomes tomorrow’s “actually, now it can.”</li>
  <li>A low <strong>verifiability</strong> score is frequently <em>fixable by you</em>. Add automated tests. Introduce a structured review step. Build a reconciliation check. You can often <em>engineer</em> verifiability into a task that didn’t have it.</li>
  <li>A low <strong>risk</strong> score is often <em>fixable by design</em>. Add an approval gate so a human signs off before anything irreversible happens. Restrict what the agent is allowed to execute. Limit its access to sensitive systems. You can frequently shrink the blast radius until the task becomes safe.</li>
</ul>

<p>So a failing score isn’t a dead end – it’s a <strong>diagnosis</strong>. It tells you exactly what would have to change before the task becomes agent-ready, which is what makes the framework useful in practice rather than just a way to say no.</p>

<blockquote>
  <p><strong>NOTE:</strong> If the task fails, decompose it and score the individual steps…</p>
</blockquote>

<h2 id="putting-it-to-work">Putting it to work</h2>

<p>You don’t need a spreadsheet for this, but if you like scoring things properly you can <a href="/assets/files/2026-08-17-which-tasks-should-you-delegate-to-ai-agents/ai-delegation-scorecard.xlsx">download my scorecard here</a>. In practice, though, the framework works as four questions you run through in your head whenever you’re tempted to hand something to an agent – or tempted to dismiss the idea:</p>

<ol>
  <li><strong>Is it worth delegating?</strong> (economic attractiveness)</li>
  <li><strong>Can an agent realistically do it?</strong> (feasibility)</li>
  <li><strong>Can I verify the result?</strong> (verifiability)</li>
  <li><strong>Can I safely allow it?</strong> (risk &amp; responsibility)</li>
</ol>

<p>Then apply the gate. Put the four answers together with the “not yet” thinking, and every task lands in one of three buckets:</p>

<h3 id="1-delegate-now">1. Delegate now</h3>
<p>All four dimensions clear the threshold. The task is worth doing, an agent can do it, you can verify the result, and the risk is acceptable. Hand it over with confidence, and spend your saved time on something an agent <em>can’t</em> do.</p>

<h3 id="2-delegate-with-guardrails">2. Delegate with guardrails</h3>
<p>The task is attractive and mostly ready, but one or more dimensions need support. This is where most of the interesting real-world tasks live. Let the weak dimension tell you where to put the guardrail. It might mean:</p>

<ul>
  <li><strong>A human review step</strong>, because verifiability is only medium – the agent does the work, a person signs off before it counts.</li>
  <li><strong>Restricted autonomy</strong>, because risk is elevated – the agent can <em>propose</em> the action but not <em>execute</em> it, or it can act only within tightly bounded limits.</li>
  <li><strong>An approval gate on the irreversible step</strong>, so everything up to the point of no return is automated, and a human makes the final call.</li>
</ul>

<p>The task still gets delegated. You just don’t grant <em>full</em> autonomy – you grant <em>bounded</em> autonomy, and you use the weak dimension to decide where the boundary goes.</p>

<blockquote>
  <p><strong>Note:</strong> When I say guardrails, I mean enforced guardrails, like permissions, approval gates, access restrictions, validation checks - not merely instructions in a prompt.</p>
</blockquote>

<h3 id="3-not-yet">3. Not yet</h3>
<p>At least one dimension is too weak to make delegation sensible or safe, and you can’t (or won’t) fix it right now. The right move is not to force it. Instead, note <em>which</em> dimension failed and <em>what would have to change</em>, and revisit the task when it does. “Not yet” is a parking space, not a rejection.</p>

<h2 id="closing-thought">Closing thought</h2>

<p>The reason I like this framework is that it’s honest about time. The technology is moving fast, and a large share of today’s <em>“not yet”</em> tasks will quietly turn into <em>“delegate now”</em> tasks – some because models improve, and some because <em>you</em> improved the task by adding tests, review steps, or approval gates.</p>

<p>So the framework isn’t a one-time verdict. It’s a lens you keep coming back to. Score a task today, act on the score today – and every so often, score it again. The tasks worth delegating are a moving target, and the four questions are how you keep up.</p>

<blockquote>
  <p><strong>Remember:</strong> If the task isn’t suited for delegation, try to decompose it and score the individual steps…</p>
</blockquote>

<p>AI isn’t taking your job. But it’s ready to take a growing list of your tasks – and now you have a way to decide exactly which ones.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>

<h2 id="addendum---a-worked-example-or-two">Addendum - a worked example or two</h2>

<p>Let’s run a couple of tasks through the framework to see how it feels in practice.</p>

<p><strong>Example A - Summarising a batch of support tickets into weekly themes.</strong></p>

<ul>
  <li><em>Economic:</em> High. It’s done every week, it’s tedious, and it currently eats a chunk of a team lead’s time. ✔</li>
  <li><em>Feasibility:</em> High. The tickets are text, they’re accessible, and models are very good at this. ✔</li>
  <li><em>Verifiability:</em> Medium-high. The summary is easy to skim against the source, and mistakes tend to be visible. ✔</li>
  <li><em>Risk:</em> Low. The output is an internal document nobody acts on blindly, and it’s trivially reversible. ✔</li>
</ul>

<p>All four clear the bar → <strong>Delegate now.</strong></p>

<p><strong>Example B - Drafting and sending replies to customer emails.</strong></p>

<ul>
  <li><em>Economic:</em> High. High volume, real time saved. ✔</li>
  <li><em>Feasibility:</em> High. Drafting a reply is well within reach. ✔</li>
  <li><em>Verifiability:</em> Medium. A wrong-but-plausible reply can look perfectly fine. ⚠</li>
  <li><em>Risk:</em> Low if it <em>drafts</em>, high if it <em>sends</em> – an irreversible action reaching a real customer. ✖ (for full autonomy)</li>
</ul>

<p>The economic and feasibility scores are excellent, but risk on the <em>send</em> step vetoes full autonomy. The fix is obvious once you see it: split the task. Let the agent draft; let a human approve the send. → <strong>Delegate with guardrails.</strong></p>

<p><strong>Example C - Reconciling month-end financial figures that feed a regulatory filing.</strong></p>

<ul>
  <li><em>Economic:</em> High. ✔</li>
  <li><em>Feasibility:</em> Possibly high. ✔</li>
  <li><em>Verifiability:</em> Depends – if there’s a hard reconciliation check, high; if not, dangerously low. ⚠</li>
  <li><em>Risk:</em> High. Regulatory accountability means a human must remain answerable, and errors are costly and hard to reverse. ✖</li>
</ul>

<p>Even with strong economics and feasibility, the risk dimension gates it. You might get to “delegate with guardrails” by having the agent <em>prepare</em> and a qualified human <em>own and sign</em>, but never to fully autonomous. → <strong>Delegate with guardrails, at most.</strong></p>

<p>Notice how in all three cases, the <em>weakest</em> dimension – not the average – decided the outcome and the shape of the guardrails.</p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Freddy" /><summary type="html"><![CDATA[Asking which jobs AI will replace is probably the wrong question. A job is a bundle of dozens of tasks, held together by a title, a team, and a person who takes responsibility for the outcome. An agent doesn’t take a job – it takes a task, does it, and hands the result back. So the question worth asking is:]]></summary></entry><entry><title type="html">The Engineering Stairway to Heaven</title><link href="https://freddysblog.com/2026/08/15/the-engineering-stairway-to-heaven/" rel="alternate" type="text/html" title="The Engineering Stairway to Heaven" /><published>2026-08-15T08:00:00+02:00</published><updated>2026-08-15T08:00:00+02:00</updated><id>https://freddysblog.com/2026/08/15/the-engineering-stairway-to-heaven</id><content type="html" xml:base="https://freddysblog.com/2026/08/15/the-engineering-stairway-to-heaven/"><![CDATA[<p>The word “engineering” has been through quite a journey over the last few years. When I started out, engineering meant writing code - carefully, line by line, and I have spent 40+ years of my life doing that. Today, a big part of engineering is about telling an AI what to do, giving it the right context, building the machinery around it and orchestrating the whole thing.</p>

<p>I’ve been thinking about this as a kind of stairway - a set of steps, where each step builds on the one below it. You don’t throw away the previous step when you climb to the next one, you build on top of it. And the further up you go, the more you move from doing the work yourself to designing the system that does the work for you.</p>

<p><img src="/assets/images/2026-08-15-the-engineering-stairway-to-heaven/2026-08-15-14-13-21.png" alt="" /></p>

<p>So let me try to walk up the stairway with you, step by step. For each step I’ll try to describe what it is, what <strong>you</strong> are doing and what the <strong>AI</strong> is doing - and then compare it to the step below.</p>

<h2 id="step-1---old-school-engineering">Step 1 - Old school engineering</h2>

<p>This is where we all come from. You sit down, you understand the problem, and you write the code. You design the architecture, you pick the patterns, you name the variables, you handle the edge cases, you write the tests and you fix the bugs.</p>

<ul>
  <li><strong>What you do:</strong> Everything. You are the one turning an idea into working software, one keystroke at a time.</li>
  <li><strong>What the AI does:</strong> Nothing - or at most it gives you autocomplete and a squiggly line when you forget a semicolon.</li>
</ul>

<p>Old school engineering is craftsmanship. It’s slow, it’s deep, and it gives you a complete understanding of every corner of your solution. The downside is obvious - it doesn’t scale with the amount of stuff we want to build today. But make no mistake - this is still the foundation. Everything above this step assumes that somebody, somewhere, understands what good code actually looks like.</p>

<h2 id="step-2---prompt-engineering">Step 2 - Prompt engineering</h2>

<p>The first step up the stairway is learning how to ask. Instead of writing the code yourself, you describe what you want and let the AI write it for you. It sounds trivial, but anyone who has tried knows that the quality of what you get back is directly tied to the quality of what you put in.</p>

<ul>
  <li><strong>What you do:</strong> You formulate the request. You choose the words, the constraints, the examples and the tone. You iterate on the prompt until the output is good enough.</li>
  <li><strong>What the AI does:</strong> It interprets your prompt and produces a result - code, text, a plan, whatever you asked for.</li>
</ul>

<p>Compared to old school engineering, you’ve handed over the typing, but you’ve taken on a new skill - the ability to express intent precisely. The trap here is thinking that a clever one-liner prompt is enough. It rarely is, and that’s exactly why the next step exists.</p>

<h2 id="step-3---context-engineering">Step 3 - Context engineering</h2>

<p>Prompting quickly runs into a wall - the AI simply doesn’t know enough about <strong>your</strong> world. It doesn’t know your codebase, your conventions, your customers or the decision you made three months ago and never wrote down. Context engineering is the art of feeding the AI the right information at the right time.</p>

<ul>
  <li><strong>What you do:</strong> You decide what the AI needs to know and make sure it gets it - relevant files, documentation, examples, coding guidelines, memory from previous conversations. You curate. You leave out the noise and bring in the signal.</li>
  <li><strong>What the AI does:</strong> It reasons over the context you provided and produces a result that actually fits your situation instead of a generic answer.</li>
</ul>

<p>Where prompt engineering is about the question, context engineering is about the knowledge behind the question. This is where a lot of the real magic happens today - a mediocre prompt with great context beats a great prompt with no context almost every time.</p>

<p><img src="/assets/images/2026-08-15-the-engineering-stairway-to-heaven/2026-08-15-14-22-21.png" alt="" /></p>

<h2 id="step-4---harness-engineering">Step 4 - Harness engineering</h2>

<p>Once you’ve got prompting and context under control, you realize the AI is much more powerful when it can actually <strong>do</strong> things - read files, run commands, call APIs, search the web, execute tests. The harness is the machinery around the model - the tools, the guardrails, the permissions and the plumbing that turn a chat box into an agent that can act.</p>

<ul>
  <li><strong>What you do:</strong> You build and configure the environment. You decide which tools the AI has access to, what it’s allowed to do, where the boundaries are and how results flow back in. You design for safety and for capability at the same time.</li>
  <li><strong>What the AI does:</strong> It uses the tools you gave it to take actions in the real world, observe the results and adjust.</li>
</ul>

<p>Compared to context engineering, you’re no longer just feeding information in - you’re giving the AI hands and letting it interact with its environment. This is a big jump, because now the AI can create side effects, and that means the quality of your harness directly determines how much you can trust it to run on its own.</p>

<h2 id="step-5---loop-engineering">Step 5 - Loop engineering</h2>

<p>A single request-and-response is nice, but the really interesting things happen when the AI can work in a loop - try something, check the result, learn from it and try again - until the goal is reached. Loop engineering is about designing that cycle. Plan, act, observe, correct, repeat.</p>

<ul>
  <li><strong>What you do:</strong> You design the feedback loop. You define what “done” looks like, what success and failure mean, how the AI should evaluate its own work and when it should stop. You build the checks and the exit conditions.</li>
  <li><strong>What the AI does:</strong> It runs the loop - executing, evaluating, self-correcting and iterating toward the goal, often across many steps, without you in the driver’s seat for every single one.</li>
</ul>

<p>The difference from harness engineering is autonomy over time. A harness gives the AI hands; a loop gives it persistence. This is where you stop babysitting every action and start trusting the system to grind through a problem on its own - which of course only works if the loop is designed well enough to catch its own mistakes.</p>

<h2 id="step-6---graph-engineering">Step 6 - Graph engineering</h2>

<p>At the top of the stairway, a single agent in a single loop is no longer enough. Real problems have many parts, and the most powerful systems are made up of many agents, tools and loops wired together into a graph - specialists that hand work off to each other, run in parallel, and combine their results. Some people call this workflow or orchestration engineering, but I like the picture of a graph.</p>

<ul>
  <li><strong>What you do:</strong> You design the whole system. You decide what the nodes are, how they connect, who does what, how work flows and where the decisions are made. You architect the collaboration between many moving parts.</li>
  <li><strong>What the AI does:</strong> The agents in the graph carry out their specialized roles, coordinate, pass information along the edges and collectively solve a problem far bigger than any single one of them could handle alone.</li>
</ul>

<p>Compared to loop engineering, you’ve gone from designing one loop to designing an entire organization of loops. You’ve moved almost completely out of the code and into the architecture. Interestingly, this feels a lot like old school system design again - you’re back to thinking about components, boundaries and responsibilities, except now the components are intelligent.</p>

<h2 id="are-there-more-steps">Are there more steps?</h2>

<p>Almost certainly. This stairway isn’t finished, and I don’t think it ever will be. People are already talking about <strong>evaluation engineering</strong> (how do you systematically measure whether any of this actually works?), <strong>memory engineering</strong> (how do systems remember and learn across time?) and <strong>environment engineering</strong> (how do you build the sandbox the agents live in?). New steps will keep appearing as fast as we can name them.</p>

<p>And if you look closely, you’ll notice something funny about the whole stairway - the higher you climb, the more it starts to look like classic engineering again. At the top you’re doing architecture, system design and orchestration - just with a very different kind of building block. The fundamentals never went away. They just changed clothes.</p>

<h2 id="but-what-about-vibe-coding">But what about vibe coding?</h2>

<p>You might be wondering where “vibe coding” fits on this stairway. And the honest answer is - it doesn’t really get its own step. Vibe coding is what happens when you climb onto the prompt step, let go of the handrail and stop looking down. You describe what you want, you accept whatever the AI hands back, you run it, and if it feels right you move on - without ever really reading or understanding the code underneath.</p>

<p>And this is where I can’t help thinking about the old fairy tale from H.C. Andersen about the Emperor’s New Clothes.</p>

<p><img src="/assets/images/2026-08-15-the-engineering-stairway-to-heaven/2026-08-15-14-18-19.png" alt="" /></p>

<p>You remember the story. Two swindlers promise the emperor the finest suit imaginable - a fabric so special that it is invisible to anyone who is stupid or unfit for their job. Of course there is no fabric at all, but nobody dares to say so. The ministers admire it, the emperor parades through town in it, and the whole crowd cheers at the magnificent clothes - because nobody wants to be the one who can’t see what everybody else is pretending to see. It takes a small child to shout out that the emperor isn’t wearing anything at all.</p>

<p>Vibe coding can feel a lot like that parade. The demo runs, the screen looks great, everybody nods and says how amazing it is - and for a while nobody wants to be the one asking “but has anyone actually looked at the code?” It all works beautifully right up until a little child - or a security review, or a production incident, or the customer’s data - points out that there was never really anything holding it together.</p>

<p>Now, I’m not saying vibe coding is useless. Far from it. For a prototype, a demo or a throwaway experiment it is fantastic, and it can get you to “wow” faster than anything we’ve ever had. The danger is only when we forget that we’re vibing - when we let the cheering crowd convince us that the emperor is fully dressed and ship it straight to production. The bottom step of the stairway, actually knowing what good code looks like, is the child in the crowd. Keep that voice around, and vibe coding becomes a superpower instead of a very well-attended parade.</p>

<h2 id="vibe-coding-with-guardrails">Vibe coding with guardrails</h2>

<p>Here’s the interesting part though - the industry noticed the missing handrail too, and a whole category of tools has appeared to put it back. Tools like <a href="https://lovable.dev">Lovable</a>, <a href="https://bolt.new">Bolt</a>, <a href="https://v0.dev">v0</a>, <a href="https://replit.com">Replit</a> and others let you vibe your way to a working application - but with guardrails baked into the experience.</p>

<p>What makes these tools so fascinating is that they don’t just spit out code and leave you to figure out the rest. They wrap the whole thing in structure. They scaffold a sensible project, wire up a database, handle authentication, deploy it somewhere, keep the pieces consistent and gently steer you away from the worst mistakes. With Lovable in particular, it almost feels like you have an architect sitting next to you while you build - one who quietly takes care of the plumbing, the conventions and the structure, so you can stay focused on describing what you actually want.</p>

<p>Vibe-coding platforms like this are a little like the flying trunk in Hans Christian Andersen’s tale. You climb in, say where you want to go, and suddenly you can travel farther and faster than your own abilities would normally allow. The magic is real — the trunk really does fly. But you didn’t build it, and you don’t entirely know how it works. As long as the trunk keeps flying, that hardly matters. The interesting question comes when it doesn’t.</p>

<p><img src="/assets/images/2026-08-15-the-engineering-stairway-to-heaven/2026-08-15-14-36-06.png" alt="" /></p>

<p>In stairway terms, this is clever - these tools take the prompt step, and instead of removing the handrail, they build the handrail into the product. The context, the harness and quite a bit of the loop are all handled for you, under the hood, by the tool itself. You get the speed and the joy of vibe coding, but a lot of the engineering rigor that vibe coding usually throws away is quietly being done on your behalf.</p>

<p>In fact, it almost feels less like a stairway and more like an escalator. You step on at the bottom, and the tool carries you up past prompt, context, harness and loop without you having to climb each step yourself. It’s a wonderful ride - just remember that an escalator only goes where it was built to go, and if it ever breaks down, the only way to keep climbing is to know how to walk the steps on your own.</p>

<p>It’s a genuinely powerful place to be. You can go from an idea to a running, deployed app in an afternoon, and the result is far more solid than what raw vibe coding would give you. But I’d still add one small note of caution - the architect is real, but it’s an architect you can’t see. The guardrails are there, but they were designed by someone else, for the general case, not for your specific problem. That’s absolutely fine for a huge number of applications - just remember that when the day comes where you need to understand exactly what your system does and why, the bottom step of the stairway is still waiting for you.</p>

<h2 id="what-about-business-central">What about Business Central?</h2>

<p>This is where it gets personal for me, because I can’t talk about a stairway like this without looking at the world I come from - Business Central.</p>

<p>If I’m completely honest, our current toolset is struggling to keep up with the pace of this stairway. The rest of the industry is stepping onto escalators - vibing full applications into existence, with context, harness and loop handled for them - while we in the AL world are still doing a lot of the climbing by hand. The tooling, the AI support, the context that models have about AL and Business Central, the harnesses and the agents - all of it is trailing behind what developers on mainstream stacks now take for granted. That gap is real, and it isn’t shrinking on its own.</p>

<p>So we need to get our act together. We need to collaborate - across partners, across Microsoft, across the community - to build the context, the tooling and the agents that bring AL and Business Central up onto the same steps everybody else is climbing. If we each try to solve this alone, in our own little corner, it’s not innovation, it’s waste.</p>

<p><strong>Please Note: I will do my utmost to ensure that we have sessions covering all of these aspects at Directions EMEA 2026 in Paris!</strong></p>

<h2 id="where-are-you-on-the-stairway">Where are you on the stairway?</h2>

<p>So here’s my question to you, and I genuinely want to know.</p>

<p><strong>Where are you on the engineering stairway to heaven?</strong></p>

<ul>
  <li>Are you still happily writing every line yourself, and proud of it?</li>
  <li>Have you learned to prompt, but not yet cracked context?</li>
  <li>Are you feeding your agents rich context and building harnesses around them?</li>
  <li>Are you designing loops and letting things run on their own?</li>
  <li>Or are you already wiring up graphs of agents and orchestrating the whole show?</li>
</ul>

<p>There’s no shame in being on any particular step - we’re all climbing at our own pace, and honestly nobody has reached the top, because there isn’t one. What matters is that you keep climbing, and that you never forget that the bottom step - actually knowing what good software looks like - is what holds the whole staircase up.</p>

<p>Drop a comment and let me know which step you’re standing on. I’m curious to see where this community is.</p>

<p>Enjoy</p>

<p><em><strong>/Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Freddy" /><category term="AI" /><category term="Development" /><summary type="html"><![CDATA[The word “engineering” has been through quite a journey over the last few years. When I started out, engineering meant writing code - carefully, line by line, and I have spent 40+ years of my life doing that. Today, a big part of engineering is about telling an AI what to do, giving it the right context, building the machinery around it and orchestrating the whole thing.]]></summary></entry><entry><title type="html">The Fkh Web Client</title><link href="https://freddysblog.com/2026/08/13/the-fkh-web-client/" rel="alternate" type="text/html" title="The Fkh Web Client" /><published>2026-08-13T11:00:00+02:00</published><updated>2026-08-13T11:00:00+02:00</updated><id>https://freddysblog.com/2026/08/13/the-fkh-web-client</id><content type="html" xml:base="https://freddysblog.com/2026/08/13/the-fkh-web-client/"><![CDATA[<p>In my <a href="/2026/08/06/accessing-fkh-containers-using-a-terminal/">previous post</a> I showed how to open a terminal to one of your containers in <a href="https://github.com/Freddy-DK/Fkh"><strong>Fkh - Freddy’s Kubernetes Helper</strong></a>. This post is about another one of the faces of Fkh - the <strong>Web Client</strong>.</p>

<h2 id="a-web-client-really">A web client? Really?</h2>

<p>At first glance it might seem a bit weird that Fkh has a web client at all. Most of the work happens in VS Code, some of it in the CLI - so why on earth would you need a web app on top of that?</p>

<p>The answer is that it is actually pretty handy. When the web client is installed and running on your phone, you can perform some tasks in Fkh right from your phone - without opening a laptop, without a terminal, without VS Code.</p>

<p><img src="/assets/images/2026-08-13-the-fkh-web-client/2026-08-13-22-46-08.png" alt="Screenshot of the Fkh web client on a phone" /></p>

<p>At Bunker Holding, every feature developed must be code reviewed and tested in a test environment. The process that for every Pull Request created, you can ask for a review from a consultant, which will trigger the generation of a test environment, which is using AAD authentication. No sharing passwords, no need to submit passwords, secure by design. These PR environments are also shut down at 18:00 every evening and the consultant doesn’t have VS Code and should certainly not need to install the Fkh CLI, so… a Web Client was born.</p>

<h2 id="why-it-is-so-simple">Why it is so simple</h2>

<p>Just like every other interface to Fkh, the web client is <em>only</em> an interface. As I explained in <a href="/2026/08/04/the-fkh-cli/">The Fkh CLI</a>, <strong>all the functionality lives in the backend</strong>, and every interface talks to that same backend.</p>

<p>That has two very concrete consequences for the web client:</p>

<ul>
  <li>The <strong>authentication model is GitHub</strong> - the exact same GitHub authentication and group membership that the VS Code extension and the CLI use. There is nothing new to learn, no separate account, no extra secrets.</li>
  <li><strong>All functionality is in the backend</strong> - the web client doesn’t implement any logic of its own, it just calls the backend and displays the result.</li>
</ul>

<p>Because of this, the web client really is very simple. It doesn’t need to be clever - it authenticates you with GitHub, calls the backend, and the backend does all the authentication, authorization and validation exactly as it does for every other interface.</p>

<h2 id="what-can-you-do-today">What can you do today?</h2>

<p>You find the URL to the Web Client when you deploy Fkh:</p>

<p><img src="/assets/images/2026-08-13-the-fkh-web-client/2026-08-13-22-32-44.png" alt="" /></p>

<p>Right now, the web client is intentionally focused. You can:</p>

<ul>
  <li><strong>Start and stop individual containers</strong></li>
  <li><strong>Start and stop the entire Fkh cluster</strong></li>
</ul>

<p>That covers the scenarios where a phone is genuinely the most convenient tool - the quick “start this before my demo” or “stop everything, I’m done for the day” kind of tasks.</p>

<h2 id="a-few-screenshots-from-my-phone">A few screenshots from my phone</h2>

<p>3 containers, 2 stopped, one running with the option to stop it.</p>

<p><img src="/assets/images/2026-08-13-the-fkh-web-client/2026-08-13-22-37-58.png" alt="" /></p>

<p>You can shut down the entire Fkh cluster to save money:</p>

<p><img src="/assets/images/2026-08-13-the-fkh-web-client/2026-08-13-22-40-07.png" alt="" /></p>

<p>When the Fkh cluster is shut down, you can start it from your phone:</p>

<p><img src="/assets/images/2026-08-13-the-fkh-web-client/2026-08-13-22-39-27.png" alt="" /></p>

<h2 id="who-knows-what-the-future-holds">Who knows what the future holds</h2>

<p>The web client is deliberately small today, but that doesn’t mean it stays that way. Because it is just another face on the same backend, adding functionality is mostly a matter of surfacing existing backend operations.</p>

<p>So today it is starting and stopping containers and the entire Fkh cluster - but who knows what is needed in the future. If partners and customers using Fkh find that they need more from their phone, it is easy to add.</p>

<h2 id="wrapping-up">Wrapping up</h2>

<p>The web client is a small but handy addition to the Fkh family. It rides on the same principles as everything else in Fkh - GitHub for authentication, GitHub team membership for authorization, and one backend that does all the work. That is exactly what makes it so simple.</p>

<p>As always, take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><category term="Web" /><summary type="html"><![CDATA[In my previous post I showed how to open a terminal to one of your containers in Fkh - Freddy’s Kubernetes Helper. This post is about another one of the faces of Fkh - the Web Client.]]></summary></entry><entry><title type="html">Accessing Fkh containers using a terminal</title><link href="https://freddysblog.com/2026/08/06/accessing-fkh-containers-using-a-terminal/" rel="alternate" type="text/html" title="Accessing Fkh containers using a terminal" /><published>2026-08-06T12:00:00+02:00</published><updated>2026-08-06T12:00:00+02:00</updated><id>https://freddysblog.com/2026/08/06/accessing-fkh-containers-using-a-terminal</id><content type="html" xml:base="https://freddysblog.com/2026/08/06/accessing-fkh-containers-using-a-terminal/"><![CDATA[<p>In my <a href="/2026/08/05/the-pricing-of-fkh/">previous post</a> I broke down what it costs to run <a href="https://github.com/Freddy-DK/Fkh"><strong>Fkh - Freddy’s Kubernetes Helper</strong></a>. This post is about something more hands-on: how you open a terminal to one of your containers.</p>

<h2 id="why-you-dont-have-direct-access-to-the-container">Why you don’t have direct access to the container</h2>

<p>Just like the SQL Server we looked at in the <a href="/2026/08/02/just-in-time-database-access-in-fkh/">just-in-time database access post</a>, your Business Central containers run as pods inside the Kubernetes cluster in your own Azure subscription. They are <strong>not</strong> exposed to the public internet, and there is (by default) no permanent open door from the outside into a container.</p>

<p><img src="/assets/images/2026-08-06-accessing-fkh-containers-using-a-terminal/2026-08-09-21-44-51.png" alt="" /></p>

<p>This is by design. The containers talk to each other and to the SQL Server from within the cluster, but from the outside there are no standing credentials and no public endpoint to reach in through, and while you could specify 5986 in the ports to open to your container, it could come with a risk.</p>

<blockquote>
  <p><strong>Note:</strong> Fkh containers are set up with a Windows account lockout policy, meaning that if you decide to open the container to the internet, three failed attempts within a 15-minute timeframe would lock the account for 15 minutes, making brute-force attacks unlikely, but can make your containers useless. For shortlived containers in pipelines, this might be an acceptable trade-off.</p>
</blockquote>

<p>Most of the time you don’t need terminal access. You create containers, publish apps and work with them straight from VS Code, the Web App or the CLI. But sometimes you genuinely need a shell inside the container - to inspect something, run some PowerShell interactively, or troubleshoot an issue that only shows up on the container itself.</p>

<p>We already saw how Fkh can create a just-in-time tunnel to the SQL Server. Getting into a container follows the same philosophy, and there are really two good ways to do it.</p>

<h2 id="option-1-open-a-terminal-with-fkh-cli-and-kubectl-for-this-you-need-access-to-the-azure-portal-and-kubernetes">Option 1: Open a terminal with fkh CLI and kubectl (for this you need access to the Azure Portal and Kubernetes)</h2>

<p>If you have <code class="language-plaintext highlighter-rouge">kubectl</code> installed and you have access to the cluster, the most direct way to get into a container is to use <code class="language-plaintext highlighter-rouge">kubectl exec</code>. Since the containers are Windows containers, you open a PowerShell session inside the pod:</p>

<pre><code class="language-pwsh">fkh open --name &lt;containername&gt;
</code></pre>

<p>fkh will check whether you have kubectl installed and if you have - it will check that you are authenticated to the right subscription and that you do have access.
If you do not have access (as many people wont), you will be redirected to the <code class="language-plaintext highlighter-rouge">poormansterminal</code>, which basically allows you to type in commands and have them executed in the container.</p>

<blockquote>
  <p><strong>Note:</strong> you can add –poormansterminal to the fkh open to force the poor mans terminal, but you still need the fkh CLI installed.</p>
</blockquote>

<p>As this mechanism requires you to install the fkh CLI, kubectl and it requires you to have backend access, it is only for admins or infrastructure people.</p>

<h2 id="option-2-open-a-winrm-session-with-allowwinrmaccess-using-fkh-cli">Option 2: Open a WinRM session with allowwinrmaccess using fkh CLI</h2>

<p>The second way is the one that fits the Fkh model best, and it mirrors exactly how just-in-time database access works. Instead of exposing SQL over a temporary tunnel, Fkh opens a temporary, tightly-scoped door for <strong>WinRM</strong> from your IP address to the container.</p>

<p>Just like there is an <strong>Allow Sql Access</strong> command, there is an <strong>Allow WinRm Access</strong> command. When you invoke it, Fkh will:</p>

<ul>
  <li>Verify who you are using GitHub authentication and check your group membership for authorization.</li>
  <li>Open a <strong>temporary door for WinRM</strong> scoped to <strong>your public IP address</strong> only.</li>
  <li>Grant the access for a <strong>limited period of time</strong>, after which the door is closed again automatically.</li>
</ul>

<p>Once the command has run, the output tells you the IP address to connect to. From there you create a PowerShell remoting session to the container and work with it as if it were local:</p>

<pre><code class="language-pwsh">$option = New-PSSessionOption -SkipCACheck -SkipCNCheck
$session = New-PSSession -ConnectionUri https://172.160.98.242:5986/wsman -Credential (Get-Credential) -Authentication Basic -SessionOption $option
Enter-PSSession $session
</code></pre>

<p>Inside that session you have a full PowerShell prompt in the container, and you can invoke code, run scripts or copy files across the session using <code class="language-plaintext highlighter-rouge">Copy-Item -ToSession</code>. When you are done - or when the time window expires - the door is closed and the access is gone.</p>

<blockquote>
  <p><strong>Note:</strong> just like with SQL access, you can run the corresponding <strong>Revoke WinRm Access</strong> command if you want to close the door before the timeout.</p>
</blockquote>

<h2 id="option-3-open-a-winrm-session-with-allowwinrmaccess-using-vs-code">Option 3: Open a WinRM session with allowwinrmaccess using VS Code</h2>

<p>The third way is actually the same as #2 - just from VS Code. If you right click the container on which you want a terminal, you can select Allow WinRm Access and Fkh will open the tunnel for you.</p>

<p><img src="/assets/images/2026-08-06-accessing-fkh-containers-using-a-terminal/2026-08-10-07-27-36.png" alt="" /></p>

<p>In the output window, you will see something like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[AllowWinRmAccess] User: &lt;github-user&gt;
Container: &lt;container-name&gt;
Allowed IP: 203.0.113.19/32
WinRM Endpoint: 203.0.113.97:5986
Auto Revoke: 2026-08-10 07:23 UTC (2h)
Connect: Enter-PSSession -ConnectionUri https://4.166.125.97:5986/wsman -Credential (Get-Credential) -Authentication Basic -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck)
</code></pre></div></div>

<blockquote>
  <p><strong>Note:</strong> that the IP number displayed here is NOT the IP number of the container, it is the IP number of the load balancer in front of the container, which has a tunnel open to the container.</p>
</blockquote>

<p>When you now grab the line after connect and run that in the terminal, you will see something like:</p>

<p><img src="/assets/images/2026-08-06-accessing-fkh-containers-using-a-terminal/2026-08-10-07-29-48.png" alt="" /></p>

<p>Now, your VS Code terminal is inside the container.</p>

<h2 id="which-one-should-you-use">Which one should you use?</h2>

<p>All options get you a terminal in the container, so it really comes down to how you like to work:</p>

<ul>
  <li><strong>kubectl</strong> is great if you already live in Kubernetes tooling and have <code class="language-plaintext highlighter-rouge">kubectl</code> configured. It is direct and needs nothing really from Fkh - but it also assumes you have cluster access set up.</li>
  <li><strong>Allow WinRm Access</strong> is the Fkh-native option. It needs no cluster tooling on your machine, it is authenticated and authorized through GitHub, scoped to your IP, and time-limited - exactly like just-in-time database access.</li>
</ul>

<p>For most people I would recommend the WinRM approach, simply because it gives you the same secure, just-in-time, no-standing-credentials experience as the rest of Fkh, without having to install and configure <code class="language-plaintext highlighter-rouge">kubectl</code> and secondly because it also runs in VS Code and can be performed without the fkh CLI installed.</p>

<h2 id="why-this-is-a-good-thing">Why this is a good thing</h2>

<p>The terminal access model gives you the best of both worlds, just like the database access did:</p>

<ul>
  <li><strong>Security</strong> - the containers are never exposed to the public internet, there are no standing credentials, and every access is authenticated, authorized, scoped to your IP and time-limited.</li>
  <li><strong>Convenience</strong> - when you genuinely need a shell in a container, you can have one, using tools you already have, without anyone handing out permanent access.</li>
</ul>

<p>This is the same philosophy that runs through all of Fkh: no humanly created secrets, managed identities and federated credentials for machine-to-machine access, and GitHub authentication plus group membership for people.</p>

<h2 id="whats-next">What’s next?</h2>

<p>In the coming posts I will continue going into more detail about how to set up Fkh and the functionality that is implemented or planned. One of my ideas is to support test runs directly from fkh, which could even take a snapshot of the database and restore the database afterwords if need be. This would cause the test run to happen in fkh, and your pipelines can do other things while running and only at the very end, wait until all tests are completed. Let me know whether you like that idea?</p>

<p>As always, take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><category term="WinRM" /><category term="kubectl" /><summary type="html"><![CDATA[In my previous post I broke down what it costs to run Fkh - Freddy’s Kubernetes Helper. This post is about something more hands-on: how you open a terminal to one of your containers.]]></summary></entry><entry><title type="html">The price of running Fkh (Freddy’s Kubernetes Helper)</title><link href="https://freddysblog.com/2026/08/05/the-pricing-of-fkh/" rel="alternate" type="text/html" title="The price of running Fkh (Freddy’s Kubernetes Helper)" /><published>2026-08-05T12:00:00+02:00</published><updated>2026-08-05T12:00:00+02:00</updated><id>https://freddysblog.com/2026/08/05/the-pricing-of-fkh</id><content type="html" xml:base="https://freddysblog.com/2026/08/05/the-pricing-of-fkh/"><![CDATA[<p>In my <a href="/2026/08/04/the-fkh-cli/">previous post</a> I described the Fkh CLI. One question I get almost every time I show <a href="https://github.com/Freddy-DK/Fkh"><strong>Fkh - Freddy’s Kubernetes Helper</strong></a> is: <em>what does it cost to run?</em></p>

<p>Since Fkh runs entirely in <strong>your own Azure subscription</strong>, the honest answer is “it depends on your usage” - but that isn’t very helpful, so this post breaks down exactly what has a cost attached, how each part scales as usage grows, and what a realistic monthly bill looks like for 5, 10 and 20 concurrently running containers.</p>

<blockquote>
  <p><strong>Disclaimer:</strong> all numbers in this post are <strong>approximate</strong>, based on pay-as-you-go pricing and will vary by region, currency, discounts and actual usage. Always use the <a href="https://azure.microsoft.com/pricing/calculator/">Azure Pricing Calculator</a> for your own scenario. Prices are in USD.</p>
</blockquote>

<p><img src="/assets/images/2026-08-05-the-pricing-of-fkh/2026-08-05-07-52-36.png" alt="" /></p>

<h2 id="what-has-a-cost-attached">What has a cost attached?</h2>

<p>Fkh is deployed with Terraform, and the resources fall into four buckets: things that are <strong>always billed</strong>, things that are <strong>usage-based</strong> (near zero at idle), things that are only billed when you <strong>enable them</strong>, and things that are <strong>free</strong>.</p>

<h3 id="always-billed-components">Always-billed components</h3>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Resource</th>
      <th>Default SKU/size</th>
      <th>Cost driver</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>AKS Linux node pool</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_kubernetes_cluster</code> default_node_pool</td>
      <td>1× <code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code> (always on)</td>
      <td>VM compute — runs SQL Server + system pods 24/7</td>
    </tr>
    <tr>
      <td><strong>AKS Windows node pool</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_kubernetes_cluster_node_pool.win</code></td>
      <td><code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code>, autoscale 0–10</td>
      <td>VM compute per BC container node (scales to zero by default)</td>
    </tr>
    <tr>
      <td><strong>Managed disk (SQL PVC)</strong></td>
      <td><code class="language-plaintext highlighter-rouge">mssql_data_pvc</code></td>
      <td>128Gi Premium SSD (<code class="language-plaintext highlighter-rouge">managed-csi-premium</code>)</td>
      <td>Provisioned disk storage</td>
    </tr>
    <tr>
      <td><strong>Container Registry (ACR)</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_container_registry</code></td>
      <td><code class="language-plaintext highlighter-rouge">Basic</code></td>
      <td>Registry storage + throughput</td>
    </tr>
    <tr>
      <td><strong>Function App storage</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_storage_account.function</code></td>
      <td>Standard LRS</td>
      <td>Required by Functions runtime</td>
    </tr>
    <tr>
      <td><strong>DB backup storage</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_storage_account.dbs</code></td>
      <td>Standard LRS</td>
      <td>Blob storage for DB backups + settings</td>
    </tr>
    <tr>
      <td><strong>Log Analytics Workspace</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_log_analytics_workspace</code></td>
      <td>PerGB2018, 30-day retention</td>
      <td>Log ingestion + retention (App Insights + Container Insights)</td>
    </tr>
    <tr>
      <td><strong>Application Insights</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_application_insights</code></td>
      <td>workspace-based</td>
      <td>Telemetry ingestion</td>
    </tr>
    <tr>
      <td><strong>AKS Load Balancer + egress IP</strong></td>
      <td>implicit (Standard LB from AKS)</td>
      <td>Standard</td>
      <td>Base LB + outbound data transfer, <strong>plus a per-container cost</strong> - each container’s <code class="language-plaintext highlighter-rouge">LoadBalancer</code> Service adds 5 rules + a public IP (see below)</td>
    </tr>
  </tbody>
</table>

<h3 id="usage-based--near-zero-at-idle">Usage-based / near-zero at idle</h3>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Resource</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Function App (backend)</strong></td>
      <td><code class="language-plaintext highlighter-rouge">azurerm_windows_function_app</code> + <code class="language-plaintext highlighter-rouge">azurerm_service_plan</code> (<code class="language-plaintext highlighter-rouge">Y1</code> Consumption)</td>
      <td>Pay-per-execution, no idle cost</td>
    </tr>
  </tbody>
</table>

<h3 id="conditional-only-billed-when-enabled">Conditional (only billed when enabled)</h3>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Flag</th>
      <th>Cost note</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Windows Spot node pool</strong></td>
      <td><code class="language-plaintext highlighter-rouge">windows_spot_enabled</code> (default <code class="language-plaintext highlighter-rouge">false</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">Standard_D8s_v5</code>, spot pricing (cheaper, evictable)</td>
    </tr>
    <tr>
      <td><strong>Staging Function App</strong></td>
      <td><code class="language-plaintext highlighter-rouge">enable_staging_backend</code></td>
      <td>Shares the <code class="language-plaintext highlighter-rouge">Y1</code> plan — consumption only</td>
    </tr>
    <tr>
      <td><strong>Static Web App (Web UI)</strong></td>
      <td><code class="language-plaintext highlighter-rouge">enable_web_app</code></td>
      <td>SKU <code class="language-plaintext highlighter-rouge">Free</code> (no cost)</td>
    </tr>
    <tr>
      <td><strong>AKS Standard/Premium tier</strong></td>
      <td><code class="language-plaintext highlighter-rouge">aks_sku_tier</code> (default <code class="language-plaintext highlighter-rouge">Free</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">Standard</code> adds control-plane SLA charge (~$73/mo)</td>
    </tr>
    <tr>
      <td><strong>Kubecost</strong></td>
      <td><code class="language-plaintext highlighter-rouge">kubecost_enabled</code></td>
      <td>Helm chart — free tier, but consumes node CPU/memory</td>
    </tr>
  </tbody>
</table>

<h3 id="free--no-direct-charge">Free / no direct charge</h3>

<p>Managed identities, the resource group, role assignments, Kubernetes secrets/services/network policies, random passwords, and the image pre-pull DaemonSet (it consumes node resources you already pay for, but there is no separate charge).</p>

<h3 id="github-repository-builds-the-docker-images">GitHub repository (builds the Docker images)</h3>

<p>Fkh integrates with <strong>AL-Go for GitHub</strong>, and the generic and specific Business Central Docker images are built by <strong>GitHub Actions</strong> in your repository. That means your only “cost” here is <strong>GitHub Actions minutes</strong> - free for public repositories and included (up to your plan’s quota) for private ones. There is no Azure charge for the build itself; the resulting images simply land in your ACR.</p>

<blockquote>
  <p><strong>Biggest cost drivers:</strong> the AKS node pool VMs (Linux always-on + Windows on demand), the 128Gi Premium SSD disk, the per-container Load Balancer rules/public IPs, and Log Analytics ingestion.</p>
</blockquote>

<h2 id="how-each-part-scales">How each part scales</h2>

<p>The whole point of the design is that a large chunk of the cost is a <strong>fixed baseline</strong> that is shared by everyone, while the part that grows with usage is mostly <strong>Windows compute</strong> - and that part scales to zero when nobody is working.</p>

<h3 id="windows-node-pool--the-big-one">Windows node pool — the big one</h3>

<p>This is where your money goes when developers are actually working. Each Business Central container needs CPU and memory, and the Windows node pool autoscales (0–10 by default) to fit the containers that are currently running.</p>

<p>The important characteristics:</p>

<ul>
  <li><strong>It scales to zero.</strong> When no containers are running - nights, weekends, holidays - the Windows node pool has no nodes and therefore no compute cost. You only pay while containers are up.</li>
  <li><strong>It scales with the number of running containers, not headcount.</strong> A single developer might have zero, one, or several containers up at once. What matters is how many containers are running <em>at the same time</em>, and for how long - not how many people are on the team. You can define the max. number of containers allowed for each developer.</li>
  <li><strong>Packing matters.</strong> A Business Central container needs roughly <strong>3 GB</strong> (depending on the size of your apps - you can monitor this and define the requirement on a per project basis), so a <code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code> (4 vCPU / 16 GB) comfortably hosts about <strong>3 containers</strong>. More concurrent containers means more nodes - or bigger nodes (see the next section).</li>
  <li><strong>Spot can cut it dramatically.</strong> Enabling the Windows Spot node pool (<code class="language-plaintext highlighter-rouge">windows_spot_enabled</code>) uses evictable spot capacity at a large discount - great for short-lived, throw-away containers that can tolerate an occasional eviction.</li>
</ul>

<h3 id="choosing-the-right-windows-node-size">Choosing the right Windows node size</h3>

<p>The default <code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code> is a good starting point, but it is not your only option - and the size you choose affects both cost efficiency and how smoothly the pool scales. Because a container needs ~3 GB, the practical density per node looks roughly like this:</p>

<table>
  <thead>
    <tr>
      <th>Node SKU</th>
      <th>vCPU / RAM</th>
      <th>Approx. containers</th>
      <th>Relative cost/hr</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code></td>
      <td>4 / 16 GB</td>
      <td>~3</td>
      <td>1×</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_D8s_v5</code></td>
      <td>8 / 32 GB</td>
      <td>~8</td>
      <td>~2×</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_D16s_v5</code></td>
      <td>16 / 64 GB</td>
      <td>~20</td>
      <td>~4×</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_D32s_v5</code></td>
      <td>32 / 128 GB</td>
      <td>~40</td>
      <td>~8×</td>
    </tr>
  </tbody>
</table>

<p>Notice that the bigger SKUs pack <em>proportionally more</em> containers - a <code class="language-plaintext highlighter-rouge">D16s_v5</code> costs roughly 4× a <code class="language-plaintext highlighter-rouge">D4s_v5</code> but hosts nearly 7× the containers, because the fixed OS/system overhead per node is amortized over more workloads. So the <strong>cost per running container actually drops as the node gets bigger</strong>, as long as you keep the node reasonably full.</p>

<p>The trade-off is scaling granularity:</p>

<ul>
  <li><strong>Small nodes (<code class="language-plaintext highlighter-rouge">D4s_v5</code>)</strong> scale in fine steps - great for spiky, low-concurrency usage where you want to add and remove capacity three containers at a time and scale to zero quickly. The downside is more wasted headroom when a node is only partly full.</li>
  <li><strong>Large nodes (<code class="language-plaintext highlighter-rouge">D8s_v5</code> / <code class="language-plaintext highlighter-rouge">D16s_v5</code>)</strong> give the best density and the lowest per-container cost for <strong>steady, higher-concurrency</strong> workloads, but each scale-up adds a big chunk of capacity at once, so a nearly-idle large node wastes more money than a small one.</li>
</ul>

<p>Rule of thumb: pick the smallest node that your <em>typical concurrent container count</em> keeps comfortably full. A team that usually runs ~8 containers at a time is better served by one <code class="language-plaintext highlighter-rouge">D8s_v5</code> than three <code class="language-plaintext highlighter-rouge">D4s_v5</code> nodes; a team spiking to ~20 containers is a natural fit for a <code class="language-plaintext highlighter-rouge">D16s_v5</code>.</p>

<h3 id="linux-node--sql-disk--mostly-fixed">Linux node + SQL disk — mostly fixed</h3>

<p>The Linux node runs SQL Server and the system pods 24/7, so it is a fixed cost regardless of how many containers you run. The 128Gi Premium SSD is likewise a fixed, provisioned cost - but it <strong>grows if you store more or larger databases</strong>. If you upload many database backups or host large databases, you may bump the disk size (or add disks), which increases this line.</p>

<h3 id="log-analytics--grows-gently-with-usage">Log Analytics — grows gently with usage</h3>

<p>Log Analytics is billed per GB ingested. More running containers and more activity means more telemetry, so this line grows slowly as usage increases. It rarely dominates the bill, but it is worth keeping an eye on retention (30 days by default) and what you choose to collect. Container Insights in particular can be tuned down if you don’t need the full firehose.</p>

<h3 id="acr--flat-until-you-outgrow-basic">ACR — flat until you outgrow Basic</h3>

<p>On the <code class="language-plaintext highlighter-rouge">Basic</code> SKU, ACR is a small, near-flat cost. It grows only if you store a lot of images or need more throughput/geo-replication, at which point you’d move to <code class="language-plaintext highlighter-rouge">Standard</code> or <code class="language-plaintext highlighter-rouge">Premium</code>. For most teams, Basic is plenty.</p>

<h3 id="backup-storage--grows-with-what-you-keep">Backup storage — grows with what you keep</h3>

<p>The DB backup storage is standard blob storage, so it grows linearly with the number and size of the database backups you upload and keep. Pruning old versions (see the file/database storage commands) keeps this in check.</p>

<h3 id="function-app--effectively-free">Function App — effectively free</h3>

<p>The backend runs on a <code class="language-plaintext highlighter-rouge">Y1</code> Consumption plan, so it is pay-per-execution with no idle cost. Even with a lot of activity and commands, the number of executions is tiny in Azure Functions terms, so this stays negligible.</p>

<h3 id="load-balancer--a-per-container-cost">Load balancer — a per-container cost</h3>

<p>Each Business Central container gets its own Kubernetes <code class="language-plaintext highlighter-rouge">Service</code> of type <code class="language-plaintext highlighter-rouge">LoadBalancer</code>, exposing five ports (80, 443, 7047, 7048, 7049) and its own <strong>public IP</strong>. On a Standard Load Balancer that is <strong>five load-balancing rules and one public IP per container</strong>.</p>

<p>Standard Load Balancer pricing (West Europe, Aug 2026) is roughly:</p>

<ul>
  <li>The <strong>first 5 rules</strong> are included for free (per hour, across the whole load balancer).</li>
  <li>Each <strong>additional rule</strong> is ~$0.01 / rule-hour.</li>
  <li>Each <strong>public IP</strong> is ~$0.006 / IP-hour.</li>
</ul>

<p>Because the very first container already uses up the 5 free rules, <strong>every container after the first is billed for its 5 rules plus its public IP</strong> - about <strong>5 × $0.01 + $0.006 ≈ $0.056/hour ≈ ~$41/month per container</strong> if it runs 24/7. Unlike compute, this cost is <strong>identical whatever node SKU you use</strong> - it scales with the number of running containers, not node size. The estimates below <strong>include</strong> this load balancer cost.</p>

<blockquote>
  <p><strong>Idea:</strong> I an considering adding a feature to containers, where I can disable endpoints to save a buck or two on the load balancer. Let me know if you think that is a good idea. I could also create a traefik-like frontend to get rid of the load balancer all-tigether.</p>
</blockquote>

<h2 id="approximate-monthly-cost-for-5-10-20-and-40-running-containers">Approximate monthly cost for 5, 10, 20 and 40 running containers</h2>

<p>Putting it together, the bill is essentially a <strong>shared fixed baseline</strong> plus <strong>Windows compute</strong> that scales with the number of concurrently running containers plus a little extra telemetry.</p>

<p>The numbers below assume a <strong>realistic working pattern</strong>: each container runs during a working day (~8 hours × ~22 workdays ≈ <strong>176 hours/month</strong>) and the <strong>Windows node pool scales to zero</strong> the rest of the time, ~3 containers per <code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code> node, <code class="language-plaintext highlighter-rouge">aks_sku_tier = Free</code>, and <strong>no</strong> spot node pool. “Running containers” means the <em>peak concurrent</em> number of containers. The totals include the per-container load balancer cost described above. Your mileage will vary.</p>

<p><strong>Shared baseline (independent of container count): ≈ $210/mo</strong></p>

<table>
  <thead>
    <tr>
      <th>Baseline item</th>
      <th>Approx. /mo</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Linux node (<code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code>, 24/7)</td>
      <td>~$140</td>
    </tr>
    <tr>
      <td>Premium SSD 128Gi</td>
      <td>~$20</td>
    </tr>
    <tr>
      <td>ACR Basic</td>
      <td>~$5</td>
    </tr>
    <tr>
      <td>Function + DB backup storage</td>
      <td>~$10</td>
    </tr>
    <tr>
      <td>Log Analytics + App Insights (light)</td>
      <td>~$13</td>
    </tr>
    <tr>
      <td>Load Balancer base + egress IP</td>
      <td>~$22</td>
    </tr>
    <tr>
      <td>Function App (<code class="language-plaintext highlighter-rouge">Y1</code> consumption)</td>
      <td>~$1</td>
    </tr>
  </tbody>
</table>

<p><strong>Total estimate (8-hour working day)</strong> - approximate total /mo depending on which Windows node SKU you pick (baseline + telemetry + Windows compute + per-container load balancer), with the node pool <strong>scaling to zero outside working hours</strong>. Each cell shows <strong>total /mo ($ per-container /mo) ×node count</strong>:</p>

<table>
  <thead>
    <tr>
      <th>Running containers (peak)</th>
      <th><code class="language-plaintext highlighter-rouge">D4s_v5</code> (3/node)</th>
      <th><code class="language-plaintext highlighter-rouge">D8s_v5</code> (8/node)</th>
      <th><code class="language-plaintext highlighter-rouge">D16s_v5</code> (20/node)</th>
      <th><code class="language-plaintext highlighter-rouge">D32s_v5</code> (40/node)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>1</strong></td>
      <td>~$285 ($285/c) ×1</td>
      <td>~$350 ($350/c) ×1</td>
      <td>~$485 ($485/c) ×1</td>
      <td>~$750 ($750/c) ×1</td>
    </tr>
    <tr>
      <td><strong>5</strong></td>
      <td>~$395 ($79/c) ×2</td>
      <td>~$395 ($79/c) ×1</td>
      <td>~$530 ($106/c) ×1</td>
      <td>~$795 ($159/c) ×1</td>
    </tr>
    <tr>
      <td><strong>10</strong></td>
      <td>~$590 ($59/c) ×4</td>
      <td>~$590 ($59/c) ×2</td>
      <td>~$590 ($59/c) ×1</td>
      <td>~$855 ($86/c) ×1</td>
    </tr>
    <tr>
      <td><strong>20</strong></td>
      <td>~$905 ($45/c) ×7</td>
      <td>~$835 ($42/c) ×3</td>
      <td>~$705 ($35/c) ×1</td>
      <td>~$970 ($49/c) ×1</td>
    </tr>
    <tr>
      <td><strong>40</strong></td>
      <td>~$1,590 ($40/c) ×14</td>
      <td>~$1,325 ($33/c) ×5</td>
      <td>~$1,195 ($30/c) ×2</td>
      <td>~$1,195 ($30/c) ×1</td>
    </tr>
  </tbody>
</table>

<p>The pattern is clear: <strong>for low concurrency the smaller SKUs are cheapest</strong> (a <code class="language-plaintext highlighter-rouge">D4s_v5</code> or two), while <strong>for high concurrency the bigger SKUs win</strong> because they pack containers more densely and carry less per-node overhead. At 40 containers a single <code class="language-plaintext highlighter-rouge">D32s_v5</code> costs about a third less than fourteen <code class="language-plaintext highlighter-rouge">D4s_v5</code> nodes.</p>

<blockquote>
  <p><strong>Note:</strong> the default Windows node pool autoscales <code class="language-plaintext highlighter-rouge">0–10</code>. The 14-node <code class="language-plaintext highlighter-rouge">D4s_v5</code> case for 40 containers would need <code class="language-plaintext highlighter-rouge">max_count</code> raised - another reason to reach for a bigger SKU at that scale.</p>
</blockquote>

<blockquote>
  <p><strong>Note also:</strong> you can define default (or forced) shutdown time for containers and allow individual developers to extend or restart containers when working overtime (happens sometimes).</p>
</blockquote>

<h3 id="but-what-if-you-never-shut-down-247">But what if you never shut down? (24/7)</h3>

<p>Here is the exact same scenario, but assuming you <strong>never shut anything down</strong> - the containers and their Windows nodes run <strong>24/7</strong> (~730 hours/month), and the per-container load balancers stay allocated around the clock. The baseline is unchanged (the Linux node and the Premium SSD are 24/7 either way), but the Windows compute and the load balancer - and therefore the per-container cost - go up substantially:</p>

<table>
  <thead>
    <tr>
      <th>Running containers (peak)</th>
      <th><code class="language-plaintext highlighter-rouge">D4s_v5</code> (3/node)</th>
      <th><code class="language-plaintext highlighter-rouge">D8s_v5</code> (8/node)</th>
      <th><code class="language-plaintext highlighter-rouge">D16s_v5</code> (20/node)</th>
      <th><code class="language-plaintext highlighter-rouge">D32s_v5</code> (40/node)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>1</strong></td>
      <td>~$495 ($495/c) ×1</td>
      <td>~$770 ($770/c) ×1</td>
      <td>~$1,320 ($1,320/c) ×1</td>
      <td>~$2,420 ($2,420/c) ×1</td>
    </tr>
    <tr>
      <td><strong>5</strong></td>
      <td>~$960 ($192/c) ×2</td>
      <td>~$960 ($192/c) ×1</td>
      <td>~$1,510 ($302/c) ×1</td>
      <td>~$2,605 ($521/c) ×1</td>
    </tr>
    <tr>
      <td><strong>10</strong></td>
      <td>~$1,730 ($173/c) ×4</td>
      <td>~$1,735 ($174/c) ×2</td>
      <td>~$1,735 ($174/c) ×1</td>
      <td>~$2,830 ($283/c) ×1</td>
    </tr>
    <tr>
      <td><strong>20</strong></td>
      <td>~$3,010 ($151/c) ×7</td>
      <td>~$2,735 ($137/c) ×3</td>
      <td>~$2,190 ($110/c) ×1</td>
      <td>~$3,285 ($164/c) ×1</td>
    </tr>
    <tr>
      <td><strong>40</strong></td>
      <td>~$5,825 ($146/c) ×14</td>
      <td>~$4,735 ($118/c) ×5</td>
      <td>~$4,185 ($105/c) ×2</td>
      <td>~$4,185 ($105/c) ×1</td>
    </tr>
  </tbody>
</table>

<p>Comparing the two tables shows just how much of the bill is under your control: at 40 containers on a <code class="language-plaintext highlighter-rouge">D16s_v5</code>, leaving everything running 24/7 instead of an 8-hour working day pushes the total from ~$1,195 up to ~$4,185 - you are now paying for nodes and public IPs around the clock, even when nobody is using them.</p>

<blockquote>
  <p><strong>Note:</strong> there is a new feature coming out, where you can force the entire cluster to shut down during nights and public holidays to save cost.</p>
</blockquote>

<p>Two things stand out:</p>

<ul>
  <li><strong>The per-container cost drops as you grow</strong>, because the fixed baseline is shared across all running containers, and larger nodes pack more densely. At 40 containers on a <code class="language-plaintext highlighter-rouge">D32s_v5</code> you are down to roughly $105 per container, versus ~$192 at 5 containers.</li>
  <li><strong>Windows compute dominates the variable cost</strong>, which is exactly the part you control - running fewer nodes, good packing, right-sizing the node SKU, scaling to zero out of hours, and enabling <strong>spot</strong> for evictable workloads can all bring the Windows compute line down substantially.</li>
</ul>

<p>This makes scaling to zero and <code class="language-plaintext highlighter-rouge">StopFkh</code> even more valuable: they don’t just save compute, they release the per-container load balancer rules and public IPs too.</p>

<h3 id="what-about-a-bigger-linux-node-for-sql">What about a bigger Linux node for SQL?</h3>

<p>The baseline above assumes the default single <code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code> Linux node running SQL Server. That is fine for light-to-moderate database usage, but SQL is the one part of the baseline you may genuinely need to scale up - not because of the <em>number</em> of containers, but because of <strong>database size, query load, and how many databases are active at once</strong>.</p>

<p>If SQL becomes the bottleneck, you would move the Linux node to a larger SKU, and this changes the <strong>fixed</strong> part of the bill (it is on 24/7):</p>

<table>
  <thead>
    <tr>
      <th>Linux SQL node</th>
      <th>vCPU / RAM</th>
      <th>~Cost /mo (Linux, 24/7)</th>
      <th>Delta vs. baseline</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_D4s_v5</code> (default)</td>
      <td>4 / 16 GB</td>
      <td>~$140</td>
      <td>—</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_D8s_v5</code></td>
      <td>8 / 32 GB</td>
      <td>~$280</td>
      <td>+$140</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_D16s_v5</code></td>
      <td>16 / 64 GB</td>
      <td>~$560</td>
      <td>+$420</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Standard_E8s_v5</code> (memory-optimized)</td>
      <td>8 / 64 GB</td>
      <td>~$365</td>
      <td>+$225</td>
    </tr>
  </tbody>
</table>

<p>A few things to keep in mind:</p>

<ul>
  <li><strong>This is a 24/7 cost</strong>, unlike the Windows nodes it does not scale to zero - so upsizing SQL raises your floor every month.</li>
  <li><strong>SQL loves memory.</strong> If you are hosting many or large databases, a memory-optimized <code class="language-plaintext highlighter-rouge">E</code>-series node (e.g. <code class="language-plaintext highlighter-rouge">E8s_v5</code> with 64 GB) often gives more headroom per dollar than a same-priced general-purpose node.</li>
  <li><strong>The Premium SSD grows separately.</strong> More/larger databases usually means bumping the 128Gi Premium SSD too, which adds to the baseline independently of the node size.</li>
</ul>

<p>For most teams the default <code class="language-plaintext highlighter-rouge">D4s_v5</code> Linux node is plenty and you should only upsize it when you actually observe SQL pressure.</p>

<h2 id="keeping-the-bill-down">Keeping the bill down</h2>

<p>A few levers you have:</p>

<ul>
  <li><strong>Scale the Windows node pool to zero out of hours.</strong> The Windows node pool can drop to <strong>zero nodes</strong> when no containers are running - nights, weekends, holidays - so you only pay for Windows compute while it is actually up. Compared to the <strong>24/7</strong> table above, scaling to zero outside working hours can easily cut the Windows compute line by <strong>more than half</strong> - and releases the per-container load balancer rules and public IPs too.</li>
  <li><strong>Shut down the whole cluster with <code class="language-plaintext highlighter-rouge">StopFkh</code>.</strong> During non-work hours you can stop the entire cluster with <code class="language-plaintext highlighter-rouge">StopFkh</code> - both the Linux and Windows node pools - removing almost all the compute cost and the load balancer cost. The one thing that keeps costing money is the <strong>persisted Premium SSD disk</strong>, which stays allocated (and billed) whether the cluster is running or not.</li>
  <li><strong>Right-size the Windows node SKU.</strong> Match the node size to your typical concurrent container count so nodes stay full (see <a href="#choosing-the-right-windows-node-size">Choosing the right Windows node size</a>).</li>
  <li><strong>Use spot nodes for throw-away containers.</strong> Enable <code class="language-plaintext highlighter-rouge">windows_spot_enabled</code> for ephemeral, evictable workloads and let them ride the discount.</li>
  <li><strong>Stay on the <code class="language-plaintext highlighter-rouge">Free</code> AKS tier</strong> unless you need the control-plane SLA (<code class="language-plaintext highlighter-rouge">Standard</code> adds ~$73/mo).</li>
  <li><strong>Prune old database backups and file versions</strong> to keep blob storage lean.</li>
  <li><strong>Tune Log Analytics</strong> retention and collection if telemetry ingestion grows.</li>
  <li><strong>Turn on Kubecost</strong> (<code class="language-plaintext highlighter-rouge">kubecost_enabled</code>) if you want per-namespace/per-container cost visibility - just remember it consumes a bit of node CPU/memory itself.</li>
</ul>

<h2 id="what-if-business-central-would-run-on-linux">What if Business Central would run on Linux?</h2>

<p>Everything above assumes Business Central containers run on <strong>Windows</strong> nodes - and today they do. But a big part of the Windows compute cost is simply the <strong>Windows licensing</strong> baked into the VM price. A Linux VM of the same size is roughly <strong>half</strong> the hourly cost of its Windows counterpart, the Linux OS also uses less CPU and memory and Linux is more performant, meaning that we can pack more containers on the same compute. The day Business Central containers can run on <strong>Linux</strong>, the compute side of this whole post changes fundamentally.</p>

<p>There is a catch during the transition, though. If you have <strong>older (Windows) containers running at the same time as new (Linux) containers</strong>, you will temporarily be paying for <strong>two</strong> container node pools instead of one - the existing Windows pool <em>and</em> a new Linux pool for the BC-on-Linux containers. During that overlap your bill actually goes <strong>up</strong> a little, because you are carrying both.</p>

<p>But that is a temporary situation. As you retire the older Windows-based containers and everything moves onto Linux, you drop the expensive Windows node pool entirely, and the container compute lands on Linux nodes at roughly half the price. <strong>The end result is that pricing drops pretty dramatically</strong> - the single biggest variable cost in Fkh gets cut roughly in half, on top of all the levers (scale-to-zero, <code class="language-plaintext highlighter-rouge">StopFkh</code>, right-sizing) you already have.</p>

<p>So the honest picture is: a short-term bump while you run old and new side by side, followed by a significant, lasting reduction once you are fully on Linux.</p>

<h2 id="comparing-with-other-services">Comparing with other services</h2>

<h3 id="azure-container-instances">Azure Container Instances</h3>

<p>When running Azure Container Instances with the standard Microsoft Business Central images, you cannot really stop and start containers - stopping a container removes the database as well, and starting a new ACI takes a very long time. In terms of pricing, an ACI with 2 vCPUs and 12 GB of memory is around $160 per month, and there is no real infrastructure around it - no integration with AL-Go or anything - so it wastes a lot of hours where your developers could be doing real work. With the current functionality in ACIs, I do not see them as relevant for Business Central work.</p>

<h3 id="azure-vms">Azure VMs</h3>

<p>When running Azure VMs, you could use Traefik and pack more containers onto one VM, but this also requires serious infrastructure - or it wastes time for your developers and requires them to have permissions and budget in the Azure Portal. Pricing for an Azure VM is in the same ballpark as ACIs, but with more overhead on installation and maintenance.</p>

<blockquote>
  <p><strong>Idea:</strong> I could build support in Fkh for using Azure VMs as an alternative to a K8s cluster, and then allowing Fkh to set up VMs and pack containers on these instead. This might make Fkh more useful for smaller partners as well, and it would be seamless for the developer.</p>
</blockquote>

<h3 id="cosmo-alpaca">Cosmo Alpaca</h3>

<p>For Cosmo Alpaca, you pay per container per minute, meaning that you have a much easier price calculation. Cheapest price I saw on their website is around $1 per hour (I think you can get a cheaper price when using a lot of compute), which would sum up to around $176 per month per container, but here you have a fully integrated VS Code extension, which helps you work with containers, saving time for your developers to do real work.</p>

<h3 id="locally-running-containers">Locally running containers</h3>

<p>Locally running containers obviously doesn’t incur any Azure costs, but they do take time away from your developers and this can be a significant amount of hours wasted.</p>

<h2 id="why-i-dont-host-fkh-for-partners-and-customers">Why I don’t host Fkh for partners and customers</h2>

<p>There are many reasons, but the primary two are these:</p>

<p>If I were to host Fkh, I would essentially be running the Linux SQL Server in Production and would have to pay license costs to Microsoft for hosting a SQL Server for my customers (not cheap) - even though they use it for Development and Test.</p>

<p>Partners and customers would be tied to me as a vendor with nowhere else to go (without a moving cost). I don’t want to put partners and customers in this situation and I don’t think they would want to put themselves in that situation either.</p>

<h2 id="wrapping-up">Wrapping up</h2>

<p>Fkh is not free to run - it uses real Azure resources in your own subscription - but the design keeps the fixed cost small and pushes the variable cost onto Windows compute that you control. The tables above assume the node pool runs 24/7, but scaling the Windows node pool to zero out of hours - or stopping the whole cluster with <code class="language-plaintext highlighter-rouge">StopFkh</code> and paying only for the persisted disk - can remove more than half of that compute. The result is a bill that scales gracefully and gets <em>cheaper per running container</em> as your usage grows.</p>

<p>Realistically, in a company with 20 developers, you will be running containers at ~$42 per month (≈ $0.004 per minute) when set up correctly. But pricing isn’t really the biggest advantage of Fkh. The biggest advantage is that it is all yours: you are in control, the data never leaves your subscription, the product is open source, the pricing is the Azure cost, it is secure, and your developers don’t need any elevated privileges or permissions, nor do they need access to Azure resources or budget.</p>

<p>As always, take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="SQL" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><category term="Azure" /><category term="Pricing" /><summary type="html"><![CDATA[In my previous post I described the Fkh CLI. One question I get almost every time I show Fkh - Freddy’s Kubernetes Helper is: what does it cost to run?]]></summary></entry><entry><title type="html">File storage in Fkh (Freddy’s Kubernetes Helper)</title><link href="https://freddysblog.com/2026/08/04/file-storage-in-fkh/" rel="alternate" type="text/html" title="File storage in Fkh (Freddy’s Kubernetes Helper)" /><published>2026-08-04T12:00:00+02:00</published><updated>2026-08-04T12:00:00+02:00</updated><id>https://freddysblog.com/2026/08/04/file-storage-in-fkh</id><content type="html" xml:base="https://freddysblog.com/2026/08/04/file-storage-in-fkh/"><![CDATA[<p>In my <a href="/2026/08/02/just-in-time-database-access-in-fkh/">previous post</a> I explained just-in-time database access in <a href="https://github.com/Freddy-DK/Fkh"><strong>Fkh - Freddy’s Kubernetes Helper</strong></a> and promised to go into more detail about the file storage. This post does exactly that.</p>

<p>The Fkh file storage basically gets rid of SAS URLs or secret URLs — you have a trusted file storage that you can access wherever you need it.</p>

<p><img src="/assets/images/2026-08-04-file-storage-in-fkh/2026-08-06-08-35-37.png" alt="" /></p>

<h2 id="why-file-storage">Why file storage?</h2>

<p>When you work with Business Central containers, there are always files you need to have around - license files, apps, configuration files, scripts, and much more. Rather than passing these around by hand or storing them in some ad-hoc location, Fkh gives you a proper, versioned file storage that lives in blob storage in <strong>your own Azure subscription</strong>.</p>

<p>Just like everything else in Fkh, you reach the file storage through the same authenticated and authorized channels - there are no standing credentials or public endpoints. Uploading and removing files are admin-only operations, while downloading and listing are available to the users who need them.</p>

<p>The storage is <strong>versioned</strong>. Every file is stored under a name, and each upload creates a new version under that name. A version manifest (<code class="language-plaintext highlighter-rouge">all.json</code>) keeps track of all versions and which one is the latest, so you can always grab the latest version or pin to a specific one.</p>

<h2 id="the-fkh-cli">The Fkh CLI</h2>

<p>While most of Fkh is available directly from VS Code, the file storage commands are a great example of where the <strong>Fkh CLI</strong> shines - especially when you want to script things or use them from a pipeline.</p>

<h2 id="uploading-a-file">Uploading a file</h2>

<p><code class="language-plaintext highlighter-rouge">fkh uploadfile</code> uploads a local file to blob storage and updates the version manifest (<code class="language-plaintext highlighter-rouge">all.json</code>) with all versions and the latest. This is an admin-only operation.</p>

<p>The key parameters are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--localPath &lt;file&gt;</code> (required) - path to the local file to upload.</li>
  <li><code class="language-plaintext highlighter-rouge">--fileName &lt;string&gt;</code> (required) - the file name, used as the folder name in blob storage.</li>
  <li><code class="language-plaintext highlighter-rouge">--fileVersion &lt;string&gt;</code> (optional) - a version label for this file, used as the blob name. If you don’t specify it, it defaults to the current UTC time as <code class="language-plaintext highlighter-rouge">yyyyMMddHHmm</code>.</li>
</ul>

<pre><code class="language-pwsh">fkh uploadfile --localPath ".\my.bclicense" --fileName "mylicense" --fileVersion "20260806"
</code></pre>

<p>If you leave out <code class="language-plaintext highlighter-rouge">--fileVersion</code>, Fkh will simply stamp it with the current UTC time, which is a nice way to keep a rolling history without having to invent version labels yourself.</p>

<blockquote>
  <p><strong>Note:</strong> Files are not sorted by <code class="language-plaintext highlighter-rouge">fileVersion</code> — it’s simply a tag, and the most recently uploaded file becomes the latest.</p>
</blockquote>

<h2 id="downloading-a-file">Downloading a file</h2>

<p><code class="language-plaintext highlighter-rouge">fkh downloadfile</code> downloads a versioned file from blob storage. You specify the file as <code class="language-plaintext highlighter-rouge">blob/version</code>, or just <code class="language-plaintext highlighter-rouge">blob</code> to use the latest version.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--file &lt;string&gt;</code> (required) - the file to download, as <code class="language-plaintext highlighter-rouge">blob/version</code> or just <code class="language-plaintext highlighter-rouge">blob</code> for the latest.</li>
  <li><code class="language-plaintext highlighter-rouge">--output &lt;string&gt;</code> (optional) - the file path to save the downloaded file to. Defaults to <code class="language-plaintext highlighter-rouge">blob-version</code> in the current directory.</li>
</ul>

<pre><code class="language-pwsh"># Download the latest version of "mylicense"
fkh downloadfile --file "mylicense" --output ".\my.bclicense"

# Download a specific version
fkh downloadfile --file "mylicense/20260806" --output ".\my.bclicense"
</code></pre>

<p>Because you can always ask for just the name and get the latest version, scripts and pipelines can stay blissfully unaware of version labels while still getting the most recent file.</p>

<h2 id="listing-files">Listing files</h2>

<p><code class="language-plaintext highlighter-rouge">fkh listfiles</code> lists the uploaded files in blob storage. You filter with a <code class="language-plaintext highlighter-rouge">name/version</code> pattern where <strong>both</strong> sides support wildcards - <code class="language-plaintext highlighter-rouge">*</code> matches any characters and <code class="language-plaintext highlighter-rouge">?</code> matches a single character. The <code class="language-plaintext highlighter-rouge">version</code> side also accepts the keyword <code class="language-plaintext highlighter-rouge">latest</code>.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--file &lt;string&gt;</code> (optional, default <code class="language-plaintext highlighter-rouge">*/latest</code>) - the filter as <code class="language-plaintext highlighter-rouge">name/version</code>.</li>
</ul>

<p>A few examples of what the filter can do:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">*/latest</code> (the default) - the latest version of every file.</li>
  <li><code class="language-plaintext highlighter-rouge">*/*</code> - all versions of all files.</li>
  <li><code class="language-plaintext highlighter-rouge">name/*</code> - all versions of a single file.</li>
  <li><code class="language-plaintext highlighter-rouge">my*/latest</code> or <code class="language-plaintext highlighter-rouge">log??/*</code> - pattern matching on both the name and the version.</li>
</ul>

<pre><code class="language-pwsh"># List the latest version of every file (default)
fkh listfiles

# List every version of every file
fkh listfiles --file "*/*"

# List all versions of the "license" file
fkh listfiles --file "mylicense/*"
</code></pre>

<p>Combine <code class="language-plaintext highlighter-rouge">--asJson</code> with these filters and you have a very scriptable way of inspecting exactly what is in your storage.</p>

<h2 id="removing-a-file">Removing a file</h2>

<p><code class="language-plaintext highlighter-rouge">fkh removefile</code> removes an uploaded file version from blob storage and updates the version manifest (<code class="language-plaintext highlighter-rouge">all.json</code>). Like uploading, this is an admin-only operation.</p>

<p>You specify the file as <code class="language-plaintext highlighter-rouge">name/version</code>, or just <code class="language-plaintext highlighter-rouge">name</code> to remove the latest version - in which case <em>latest</em> is repointed to the previous version.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--file &lt;string&gt;</code> (required) - the file to remove, as <code class="language-plaintext highlighter-rouge">name/version</code> or just <code class="language-plaintext highlighter-rouge">name</code> for the latest.</li>
  <li><code class="language-plaintext highlighter-rouge">--confirm</code> - skip the interactive confirmation prompt (useful in scripts).</li>
</ul>

<pre><code class="language-pwsh"># Remove a specific version
fkh removefile --file "mylicense/20260806"

# Remove the latest version (latest repoints to the previous one)
fkh removefile --file "mylicense"

# Skip the confirmation prompt, e.g. from a pipeline
fkh removefile --file "mylicense/20260806" --confirm
</code></pre>

<p>The fact that removing the latest version automatically repoints <em>latest</em> to the previous version means you can safely roll back a bad upload without breaking anyone who is asking for the latest version.</p>

<h2 id="and-the-same-for-databases">And the same for databases</h2>

<p>The file storage is only half the story. A very similar set of commands exists for <strong>databases</strong> - you can upload a database and then use it when creating containers, so that new containers come up with exactly the data you want.</p>

<p>This is a powerful building block: imagine restoring your online Business Central database once, uploading it to Fkh, and then spinning up containers based on that database whenever you need them. But more about this later.</p>

<p>As always, take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="SQL" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><summary type="html"><![CDATA[In my previous post I explained just-in-time database access in Fkh - Freddy’s Kubernetes Helper and promised to go into more detail about the file storage. This post does exactly that.]]></summary></entry><entry><title type="html">The Fkh CLI (Freddy’s Kubernetes Helper)</title><link href="https://freddysblog.com/2026/08/04/the-fkh-cli/" rel="alternate" type="text/html" title="The Fkh CLI (Freddy’s Kubernetes Helper)" /><published>2026-08-04T11:00:00+02:00</published><updated>2026-08-04T11:00:00+02:00</updated><id>https://freddysblog.com/2026/08/04/the-fkh-cli</id><content type="html" xml:base="https://freddysblog.com/2026/08/04/the-fkh-cli/"><![CDATA[<p>In my <a href="/2026/08/02/just-in-time-database-access-in-fkh/">previous post</a> I showed how to run backend commands from VS Code using <a href="https://github.com/Freddy-DK/Fkh"><strong>Fkh - Freddy’s Kubernetes Helper</strong></a>. Obviously, VS Code is not the only frontend for Fkh, there is also a Web App and a CLI. Who knows, in the future, there might be other front-ends (MCP is one I have on my mind - for troubleshooting purposes).</p>

<p><img src="/assets/images/2026-08-04-the-fkh-cli/2026-08-04-11-07-36.png" alt="" /></p>
<h2 id="one-backend-many-interfaces">One backend, many interfaces</h2>

<p>The most important thing to understand about Fkh is that <strong>all the functionality lives in the backend</strong>. The backend is an Azure Function running in your own Azure subscription - is the single place that authenticates you, authorizes you against your GitHub team memberships, and actually performs the work.</p>

<p>Everything else is just an interface to that backend:</p>

<ul>
  <li>The <strong>VS Code extension</strong> is an interface.</li>
  <li>The <strong>CLI</strong> is an interface.</li>
  <li>The <strong>Web app</strong> is an interface.</li>
</ul>

<p><strong>GitHub Actions</strong> and <strong>Azure DevOps</strong> talks to the same interface typically using the CLI authenticating using federated credentials (OIDC).</p>

<pre><code class="language-mermaid">graph LR
    subgraph Interfaces
        VSIX["VS Code Extension"]
        CLI["Fkh CLI"]
        WEB["Web app"]
        GA["GitHub Actions"]
        ADO["Azure DevOps"]
    end

    FUNC["Fkh Backend&lt;br/&gt;(Azure Function)"]
    FKH["Kubernetes Cluster,&lt;br/&gt;Storage, Logs,..."]
    GH["GitHub&lt;br/&gt;(auth + teams)"]

    VSIX --&gt; FUNC
    CLI --&gt; FUNC
    WEB --&gt; FUNC
    GA --&gt; CLI
    ADO --&gt; CLI
    FUNC --&gt; GH
    FUNC --&gt; FKH
</code></pre>

<p>This is a deliberate design choice. Because the backend is the bearer of permissions, the interface you happen to use doesn’t matter for security - whether you create a container from VS Code, from the CLI, or from a pipeline, the exact same authentication, authorization and validation happens on the backend. The CLI cannot do anything the backend won’t allow.</p>

<h2 id="installing-the-cli">Installing the CLI</h2>

<p>The Fkh CLI is available on NuGet as a dotnet tool and is installed by using:</p>

<pre><code class="language-pwsh">dotnet tool install|update --global fkh [--prerelease]
</code></pre>

<p>After installing the CLI, you need to specify backend and GitHub user account, and then you can get the function catalog by writing:</p>

<p><img src="/assets/images/2026-08-04-the-fkh-cli/2026-08-04-10-49-30.png" alt="" /></p>

<blockquote>
  <p><strong>Note:</strong> many more functions are available and more functions will be added as needed by partners and customers using Fkh.</p>
</blockquote>

<p>Over the coming weeks/months, I will desribe the functionality in Fkh in larger detail.</p>

<h2 id="but-who-needs-a-cli">But, who needs a CLI?</h2>

<p>If most people work in VS Code, why do we need a CLI at all?</p>

<p>As the matter of fact, most people probably won’t need to install the Fkh CLI. Admins might want to, but it isn’t a requirement for most things.</p>

<p>But, <strong>not everything is interactive</strong>. Pipelines, scheduled jobs, build agents and automation scripts don’t open VS Code and click buttons - they run commands. The CLI is built exactly for those scenarios:</p>

<ul>
  <li><strong>DevOps pipelines</strong> - spin up a container to run your tests, publish your apps, then tear it down again.</li>
  <li><strong>Scheduled maintenance</strong> - clean up old containers, refresh databases, prune file versions.</li>
  <li><strong>Scripting and glue</strong> - combine Fkh operations with everything else you do from a shell.</li>
</ul>

<p>Because the CLI speaks the same backend as everything else, a container you create from a pipeline is a first-class citizen - you can see it and manage it from VS Code afterwards, and vice versa.</p>

<h2 id="authenticating-from-a-pipeline">Authenticating from a pipeline</h2>

<p>The one thing a pipeline needs is a way to prove who it is - and Fkh uses GitHub for that, just like it does for humans.</p>

<p>The pipeline needs the <strong>ID_TOKEN: write</strong> permission, which enables OIDC and in GitHub Actions you use <strong>OIDC</strong> with the <code class="language-plaintext highlighter-rouge">--useOIDC</code> option, which fetches a short-lived token from GitHub Actions and auto-refreshes it. There are <strong>no secrets to store</strong> - no personal access tokens, no passwords, nothing to rotate or leak.</p>

<pre><code class="language-pwsh"># Inside a GitHub Actions job
fkh listcontainers --useOIDC --asJson
</code></pre>

<p>Outside of GitHub Actions - for example on a local machine or a self-hosted agent using the GitHub CLI - you can authenticate with your GitHub account using <code class="language-plaintext highlighter-rouge">--ghUser &lt;user&gt;</code> (or the <code class="language-plaintext highlighter-rouge">GH_USER</code> environment variable).</p>

<p>The common options you will reach for most often are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--useOIDC</code> - authenticate using the GitHub Actions OIDC token (pipelines).</li>
  <li><code class="language-plaintext highlighter-rouge">--ghUser &lt;user&gt;</code> - GitHub user account for the <code class="language-plaintext highlighter-rouge">gh</code> auth token (or the <code class="language-plaintext highlighter-rouge">GH_USER</code> environment variable).</li>
  <li><code class="language-plaintext highlighter-rouge">--backendUrl &lt;url&gt;</code> - override the backend URL.</li>
  <li><code class="language-plaintext highlighter-rouge">--asJson</code> - output the result as JSON so you can parse it in later steps.</li>
  <li><code class="language-plaintext highlighter-rouge">--nowait</code> - don’t wait for completion, fire and continue.</li>
  <li><code class="language-plaintext highlighter-rouge">-h</code> / <code class="language-plaintext highlighter-rouge">--help</code> - show help for any command.</li>
</ul>

<h2 id="the-same-commands-everywhere">The same commands, everywhere</h2>

<p>Most of what you can do in the CLI is also available in VS Code through the <strong>Fkh: Run Command</strong> command. That gives you a menu of the available operations, and it invokes exactly the same backend operations the CLI does.</p>

<p>So the mental model is simple: there is one set of commands, and you pick the interface that fits the moment.</p>

<ul>
  <li>Working interactively? Use the VS Code extension, or <strong>Fkh: Run Command</strong> for the less common operations.</li>
  <li>Automating? Use the CLI with <code class="language-plaintext highlighter-rouge">--useOIDC</code> in your pipeline.</li>
</ul>

<p>You don’t have to learn two different tools with two different capabilities - you learn the commands once, and you use them from wherever makes sense.</p>

<h2 id="wrapping-up">Wrapping up</h2>

<p>The CLI is a small tool with a big idea behind it: <strong>Fkh is one backend, and the CLI is just one of its faces</strong>. Everything the CLI can do, the backend does, under the same security model as the VS Code extension - and most of it is available in VS Code too through <strong>Fkh: Run Command</strong>.</p>

<p>As always, take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="SQL" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><category term="DevOps" /><summary type="html"><![CDATA[In my previous post I showed how to run backend commands from VS Code using Fkh - Freddy’s Kubernetes Helper. Obviously, VS Code is not the only frontend for Fkh, there is also a Web App and a CLI. Who knows, in the future, there might be other front-ends (MCP is one I have on my mind - for troubleshooting purposes).]]></summary></entry><entry><title type="html">Just-in-time database access in Fkh (Freddy’s Kubernetes Helper)</title><link href="https://freddysblog.com/2026/08/02/just-in-time-database-access-in-fkh/" rel="alternate" type="text/html" title="Just-in-time database access in Fkh (Freddy’s Kubernetes Helper)" /><published>2026-08-02T12:00:00+02:00</published><updated>2026-08-02T12:00:00+02:00</updated><id>https://freddysblog.com/2026/08/02/just-in-time-database-access-in-fkh</id><content type="html" xml:base="https://freddysblog.com/2026/08/02/just-in-time-database-access-in-fkh/"><![CDATA[<p>In my <a href="/2026/08/01/the-security-model-of-fkh/">previous post</a> I explained the security model of <a href="https://github.com/Freddy-DK/Fkh"><strong>Fkh - Freddy’s Kubernetes Helper</strong></a> and why you can trust it for your development processes. In that post I briefly mentioned just-in-time database access. This post explains what that means and how it works.</p>

<p><img src="/assets/images/2026-08-02-just-in-time-database-access-in-fkh/2026-08-03-05-55-43.png" alt="" /></p>

<h2 id="why-you-dont-have-direct-database-access">Why you don’t have direct database access</h2>

<p>As explained in the <a href="/2026/08/01/the-security-model-of-fkh/">security model post</a>, the databases for your containers live on a SQL Server (Developer edition) running inside the Kubernetes cluster in your own Azure subscription. The containers connect to that SQL Server over TCP 1433 from within the cluster.</p>

<p>This is by design. The SQL Server is <strong>not</strong> exposed to the public internet, and you do not have a permanent connection string, username or password on your local machine. There are no standing credentials to leak, rotate, or lose.</p>

<p>But sometimes you genuinely need to get into the database - to inspect data, run a query, troubleshoot an issue, or point SQL Server Management Studio (SSMS) or Azure Data Studio at it. That is where just-in-time database access comes in.</p>

<h2 id="what-is-just-in-time-database-access">What is just-in-time database access?</h2>

<p>Just-in-time (JIT) database access means that instead of having a permanent, always-open door to the database, Fkh opens a temporary, tightly-scoped door only when you need it, and closes it again automatically after a limited period of time.</p>

<p>When you request access, Fkh will:</p>

<ul>
  <li>Verify who you are using GitHub authentication and check your group membership for authorization.</li>
  <li>If requested, create a <strong>temporary database user</strong> with your GitHub username and the password you specify.</li>
  <li>Open a <strong>temporary network tunnel</strong> into the cluster so your local tools can reach the SQL Server.</li>
  <li>Grant the access for a <strong>limited period of time</strong>, after which the user and the tunnel are removed automatically.</li>
</ul>

<p>No standing users, no shared passwords, no secrets stored on your machine.</p>

<h2 id="how-to-get-access-from-your-computer">How to get access from your computer</h2>

<p>In VS Code you will need the Microsoft SQL Server extension (or another tool) as well as the Fkh extension. To open a tunnel from your machine to the SQL Server, you need to invoke one of the many commands from Fkh:</p>

<p><img src="/assets/images/2026-08-02-just-in-time-database-access-in-fkh/2026-08-02-23-33-04.png" alt="" /></p>

<p>Locate the <strong>Allow Sql Access</strong> command and invoke it:</p>

<p><img src="/assets/images/2026-08-02-just-in-time-database-access-in-fkh/2026-08-02-23-32-28.png" alt="" /></p>

<p>Specify the password you want to use for your temporary user in SQL:</p>

<p><img src="/assets/images/2026-08-02-just-in-time-database-access-in-fkh/2026-08-02-23-32-12.png" alt="" /></p>

<blockquote>
  <p><strong>Note:</strong> you can also use the Fkh CLI to get access; it works exactly the same way. More about this later.</p>
</blockquote>

<p>After successfully running the command, the output window displays the IP address of your SQL Connection. Use this info to create a new connection in the SQL Server extension:</p>

<p><img src="/assets/images/2026-08-02-just-in-time-database-access-in-fkh/2026-08-02-23-40-48.png" alt="" /></p>

<p>Select the database, create a new query and enjoy access to the database until it is automatically revoked.</p>

<p><img src="/assets/images/2026-08-02-just-in-time-database-access-in-fkh/2026-08-02-23-43-15.png" alt="" /></p>

<blockquote>
  <p><strong>Note:</strong> you can always run the <strong>Revoke Sql Access</strong> command if you want to close the network tunnel before timeout.</p>
</blockquote>

<h2 id="request-flow-for-database-access">Request flow for database access</h2>

<p>The flow for requesting just-in-time database access follows the same pattern as the rest of Fkh - the backend function is the bearer of permissions, and the user just proves who they are with GitHub:</p>

<pre><code class="language-mermaid">graph LR
    A["User"] --&gt;|"Request DB access&lt;br/&gt;Bearer: github_token"| B["Backend function&lt;br/&gt;(auth + validate)"]
    B --&gt;|"Check membership"| C["GitHub Teams"]
    B --&gt; D["Create temporary&lt;br/&gt;SQL user + password"]
    B --&gt; E["Open temporary&lt;br/&gt;network tunnel"]
    D --&gt; F["SQL Server&lt;br/&gt;(in AKS)"]
    E --&gt; F
    F --&gt;|"time-limited access"| A
    B --&gt;|"after expiry"| G["Revoke user&lt;br/&gt;+ close tunnel"]
</code></pre>

<h2 id="authorization-for-database-access">Authorization for database access</h2>

<p>As with everything else in Fkh, authorization is based on your GitHub team membership and the highest privilege wins:</p>

<ul>
  <li><strong>Supporters</strong> can see logs and statuses, but do not get database access.</li>
  <li><strong>Members</strong> can get just-in-time access to the databases of their own containers.</li>
  <li><strong>Admins</strong> can get just-in-time access to any container’s database.</li>
</ul>

<p>If an authenticated user is not a member of any of the configured groups, they get no access at all.</p>

<h2 id="connecting-with-your-tools">Connecting with your tools</h2>

<p>Once the temporary user, password and tunnel are in place, you can connect with the tools you already know - SQL Server Management Studio (SSMS), Azure Data Studio, <code class="language-plaintext highlighter-rouge">sqlcmd</code>, or anything else that speaks TDS - using the connection details Fkh gives you.</p>

<p>Because the access is time-limited, you don’t have to remember to clean anything up. When the window expires, the temporary user is dropped and the tunnel is closed automatically. If you need access again later, you simply request it again.</p>

<h2 id="why-this-is-a-good-thing">Why this is a good thing</h2>

<p>The just-in-time model gives you the best of both worlds:</p>

<ul>
  <li><strong>Security</strong> - there are no permanent credentials, no public database endpoint, and every access is authenticated, authorized, scoped and time-limited.</li>
  <li><strong>Convenience</strong> - when you need to get into the database, you can, using the tools you already have, without begging anyone for a connection string or a password.</li>
</ul>

<p>This is the same philosophy that runs through all of Fkh: no humanly created secrets, managed identities and federated credentials for machine-to-machine access, and GitHub authentication plus group membership for people.</p>

<h2 id="whats-next">What’s next?</h2>

<p>One of my ideas is to create a one-click restore of my online Business Central into Fkh, having SQL Server Developer edition in the midst removes the restrictions from SQL Express inside the containers, wouldn’t that be cool.</p>

<p>In the coming posts I will go into more detail about the file storage, how to set up Fkh, and the functionality that is implemented or planned.</p>

<p>As always, take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="SQL" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><summary type="html"><![CDATA[In my previous post I explained the security model of Fkh - Freddy’s Kubernetes Helper and why you can trust it for your development processes. In that post I briefly mentioned just-in-time database access. This post explains what that means and how it works.]]></summary></entry><entry><title type="html">The security model of Fkh (Freddy’s Kubernetes Helper)</title><link href="https://freddysblog.com/2026/08/01/the-security-model-of-fkh/" rel="alternate" type="text/html" title="The security model of Fkh (Freddy’s Kubernetes Helper)" /><published>2026-08-01T12:00:00+02:00</published><updated>2026-08-01T12:00:00+02:00</updated><id>https://freddysblog.com/2026/08/01/the-security-model-of-fkh</id><content type="html" xml:base="https://freddysblog.com/2026/08/01/the-security-model-of-fkh/"><![CDATA[<p>In my <a href="/2026/07/31/creating-containers-in-vs-code-using-fkh/">previous post</a> I explained how it looks when working with containers in VS Code using <a href="https://github.com/Freddy-DK/Fkh"><strong>Fkh - Freddy’s Kubernetes Helper</strong></a>. This post will explain about the security model of Fkh and why you can trust it for your development processes.</p>

<p><img src="/assets/images/2026-08-01-the-security-model-of-fkh/2026-08-02-08-00-05.png" alt="Fkh security overview" /></p>

<h2 id="its-all-yours">It’s all yours</h2>

<p>First of all, Fkh is not hosted by me or any other organization. When using Fkh, it is hosted in your own Azure subscription and your data never leaves your subscriptions.</p>

<p>Secondly, Fkh is Open Source, which prevents you from vendor lock-in and if the product doesn’t take the direction you want or you are waiting for functionality you need, you can add this to your own version of Fkh and create a PR.</p>

<p>Lastly, Fkh doesn’t store or require any passwords or any humanly created secrets or personal access tokens. Everything in Fkh uses managed identities, federated credentials, just-in-time access. GitHub authentication is used for you to prove who you are and GitHub group memberships are used to define what you have access to.</p>

<h2 id="github-authentication">GitHub authentication</h2>

<p>When configured properly, GitHub authentication is considered secure, and being the leading source code repository for enterprises, I am confident that it will stay like that. Utilizing GitHub authentication for Fkh seemed like the right approach to avoid having to create my own. Also, with the tight integration to AL-Go for GitHub, it was just a no-brainer.</p>

<p><img src="/assets/images/2026-08-01-the-security-model-of-fkh/2026-08-02-09-23-50.png" alt="Fkh GitHub authentication" /></p>

<h2 id="github-authorization">GitHub authorization</h2>

<p>With GitHub handling the authentication securely, how do we then handle authorization in a secure way? The solution implemented in Fkh is that the configuration holds the name of 0+ GitHub teams for supporters, members and admins.</p>

<p>Highest privilege wins - if an authenticated user is not a member of any of the configured groups, they have no access. If an authenticated user is a member of one or multiple groups, they will get the highest privilege.</p>

<p>Supporters will be able to see logs and statuses, Members will be able to see and manage own containers, databases etc., Admins will be able to see and manage all containers, databases, storage etc.</p>

<h2 id="request-flow-for-createcontainer">Request flow for CreateContainer</h2>

<p>When a user is creating a container, whether it being from VS Code, the CLI or any other tool, the flow goes like this:</p>

<pre><code class="language-mermaid">graph LR
    A["User"] --&gt;|"POST /api/CreateContainer&lt;br/&gt;Bearer: github_token"| B["Backend function&lt;br/&gt;(auth + validate)"]
    B --&gt;|"Check membership"| C["GitHub Teams"]
    B --&gt; D["FkhCreateContainer"]
    D --&gt; E["Check ACR image"]
    D --&gt; F["Restore DB"]
    D --&gt; G["Create K8s resources"]
    E --&gt;|"missing?"| H["Trigger GitHub Actions&lt;br/&gt;(build image)"]
</code></pre>

<h2 id="the-security-model">The security model</h2>

<p>As shown in the above diagram, the individual users of Fkh doesn’t need access to any Azure resources, nor do they need to be an admin on the computer they are working on. The backend function is the bearer of permissions for handling resources and containers, and users just need to authenticate and provide proof-of-identity using GitHub.</p>

<p>A high level overview of the architecture is here:</p>

<pre><code class="language-mermaid">graph LR
    subgraph Clients
        VSIX["VS Code Extension"]
        CLI["CLI Tool"]
    end

    GA["GitHub Actions&lt;br/&gt;(CreateImages)"]
    GA2["GitHub Actions"]
    USER["User"]
    FUNC["Fkh Backend&lt;br/&gt;(Azure Function)"]
    GH["GitHub API"]
    GHTEAMS["GitHub&lt;br/&gt;Org Teams"]

    subgraph AKS["AKS Cluster"]
        direction TB
        SQL["SQL Server&lt;br/&gt;(Linux)"]
        BC["BC Containers&lt;br/&gt;(Windows)"]
        SPOT["BC Containers&lt;br/&gt;(Windows Spot)"]
    end

    ACR["Container&lt;br/&gt;Registry"]
    BLOB["Blob Storage&lt;br/&gt;(.bak files)"]
    AAD["Azure AD&lt;br/&gt;Deploy Identity"]

    USER --&gt; VSIX
    USER --&gt; CLI
    USER --&gt;|"team membership"| GHTEAMS
    VSIX --&gt;|"GitHub token"| FUNC
    CLI --&gt;|"GitHub token&lt;br/&gt;or OIDC token"| FUNC
    FUNC --&gt;|"validate user"| GH
    FUNC &lt;--&gt;|"check membership"| GHTEAMS
    FUNC --&gt;|"manage containers"| AKS
    FUNC --&gt;|"check/pull images"| ACR
    FUNC --&gt;|"database backups"| BLOB
    FUNC --&gt;|"dispatch workflow&lt;br/&gt;(GitHub App)"| GA
    BC &amp; SPOT --&gt;|"TCP 1433"| SQL
    GA --&gt;|"OIDC"| AAD
    GA2 --&gt;|"OIDC token"| CLI
    AAD --&gt;|"federated credential"| ACR
    AAD --&gt;|"federated credential"| BLOB
</code></pre>

<p>As shown in the diagram, access is possible from users or from GitHub actions using federated credentials. In both cases, you specify who and what have access.</p>

<p>All access from code (GitHub actions) are considered admin access and can manage everything.</p>

<blockquote>
  <p><strong>Note:</strong> the diagram shows that all containers use a SQL Server on Linux instead of the built-in SQL Express inside the container. The reason for this is that containers on Kubernetes cannot be stopped. They scale to 0 and are removed, but the database persists.</p>
</blockquote>

<h2 id="sql-server-developer-edition">SQL Server Developer edition</h2>

<p>The SQL Server in the kubernetes cluster is a SQL Server Developer edition, which includes the full feature set (over the SQL Express). Since Fkh is hosted in your own Azure subscription, and only used for development and testing, you are allowed to use this at no license cost.</p>

<blockquote>
  <p><strong>Note:</strong> if I were to host Fkh for other partners and allow them to share a cluster, I would have to pay license costs for the SQL Server as for me, it would be a production setup.</p>
</blockquote>

<h2 id="just-in-time-database-access">Just-in-time database access</h2>

<p>As the databases resides inside the Kubernetes cluster, you do not have database access from your local PC. Fkh does make it possible for users to get just-in-time access to the database for a limited period of time with a temporary network tunnel and user. More about this later.</p>

<h2 id="my-containers-vs-all-containers">My containers vs. all containers</h2>

<p>In VS Code, you will see a list of your containers in the Fkh window. Admins have the ability to click the small see-all icon, to see and manage all containers. More about this later.</p>

<h2 id="a-secure-storage-account-for-your-pipelines">A secure storage account for your pipelines</h2>

<p>Fkh also contains a storage account for databases and other files, which can be used for secure file storage with no need for SAS URLs, tokens and secrets - authenticated workflows have access to get what they need. More about this later.</p>

<h2 id="whats-next">What’s next?</h2>

<p>Next posts will be going in more details about the database, the file storage, how to setup Fkh and new functionality implemented or planned.</p>

<p>The current functionality of Fkh is very much the result of one of the customers using Fkh, Bunker Holding Group. My vision and the requirements from Bunker, is what made Fkh what it is today. New functionality in Fkh will very much come from my ideas or from requirements from sponsors and customers.</p>

<p>Stay tuned and feel free to take a look at the project on GitHub: <a href="https://github.com/Freddy-DK/Fkh">https://github.com/Freddy-DK/Fkh</a> and please consider <a href="https://github.com/sponsors/Freddy-DK">sponsoring me</a> or setting up a <a href="https://github.com/Freddy-DK/Fkh/blob/main/Support%20Service%20Agreement.md">support service agreement</a> to keep this project alive and thriving.</p>

<p>Enjoy</p>

<p><em><strong>Freddy</strong></em></p>]]></content><author><name>Freddy Kristiansen</name></author><category term="Fkh" /><category term="Fkh" /><category term="Open Source" /><category term="Kubernetes" /><category term="Docker" /><category term="GitHub" /><category term="AL-Go for GitHub" /><summary type="html"><![CDATA[In my previous post I explained how it looks when working with containers in VS Code using Fkh - Freddy’s Kubernetes Helper. This post will explain about the security model of Fkh and why you can trust it for your development processes.]]></summary></entry></feed>