<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://naeemh.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://naeemh.github.io/" rel="alternate" type="text/html" /><updated>2026-06-22T17:24:40+00:00</updated><id>https://naeemh.github.io/feed.xml</id><title type="html">Naeem Hossain | Systems &amp;amp; Software Engineer</title><subtitle>I automate technical debt, ensure our distributed fleet is running, and write too many config files</subtitle><entry><title type="html">Why your VMSS SSH keys belong in Key Vault (and how to set it up in CDKTF)</title><link href="https://naeemh.github.io/writing/vmss-ssh-keys-belong-in-keyvault/" rel="alternate" type="text/html" title="Why your VMSS SSH keys belong in Key Vault (and how to set it up in CDKTF)" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://naeemh.github.io/writing/vmss-ssh-keys-belong-in-keyvault</id><content type="html" xml:base="https://naeemh.github.io/writing/vmss-ssh-keys-belong-in-keyvault/"><![CDATA[<blockquote>
  <p><strong>TL;DR</strong> — The right model for human SSH access to a Linux fleet on Azure is:
generate the keypair inside Terraform, store the private key in Key Vault, grant
humans <code class="language-plaintext highlighter-rouge">Key Vault Secrets User</code> via RBAC, and have them connect through Azure
Bastion. This post walks through the CDKTF wiring and shares
<a href="https://pypi.org/project/azkv-ssh-fetch/"><code class="language-plaintext highlighter-rouge">azkv-ssh-fetch</code></a> — a CLI that
turns the operator side of this pattern into a single command.</p>
</blockquote>

<h2 id="the-upload-my-public-key-antipattern">The “upload my public key” antipattern</h2>

<blockquote>
  <p><em><!-- TODO: 2-3 paragraphs on the failure mode. Story about somebody emailing
their pubkey in Slack, the operator pasting it into the wrong VMSS, the
rotation nightmare that follows. Hint: every fresh engineer asks for this.
--></em></p>
</blockquote>

<h2 id="the-right-model-in-one-diagram">The right model in one diagram</h2>

<blockquote>
  <p><em><!-- TODO: ASCII or simple mermaid diagram showing:
  Terraform → tls_private_key + azurerm_key_vault_secret
  Human → RBAC (Key Vault Secrets User) → KV secret → akf fetch
  Human → Azure Bastion → VMSS (port 22)
Key insight: the private key is never in source control, never in chat,
never on a workstation longer than needed.
--></em></p>
</blockquote>

<h2 id="minimal-cdktf-snippet">Minimal CDKTF snippet</h2>

<blockquote>
  <p>*&lt;!– TODO: ~30 lines of TypeScript showing:</p>
  <ul>
    <li>generating tls_private_key</li>
    <li>writing public key to vmss admin_ssh_key</li>
    <li>writing private key to KV as a secret with content_type ‘application/x-pem-file’</li>
    <li>role_assignment block granting a group ‘Key Vault Secrets User’
Keep it copy-paste-able. Real names, masked subs.
–&gt;*</li>
  </ul>
</blockquote>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TODO: paste production-shaped snippet here.</span>
<span class="c1">// Replace with your actual stack name + group object ID.</span>
</code></pre></div></div>

<h2 id="what-this-buys-you">What this buys you</h2>

<ul>
  <li><strong>No key sprawl</strong> — one private key per VMSS, lives in one place.</li>
  <li><strong>Auditable access</strong> — every <code class="language-plaintext highlighter-rouge">get-secret</code> call shows up in KV diagnostic logs with the user identity.</li>
  <li><strong>Rotation is <code class="language-plaintext highlighter-rouge">terraform apply</code></strong> — bump the <code class="language-plaintext highlighter-rouge">tls_private_key</code> resource, re-run, done.</li>
  <li><strong>PIM-compatible</strong> — the role assignment can be marked PIM-eligible so humans elevate to access the key just-in-time.</li>
  <li><strong>No shared keys in puppet/ansible</strong> — the only thing in config management is the public key fingerprint, if anything.</li>
</ul>

<h2 id="the-operator-side-akf">The operator side: <code class="language-plaintext highlighter-rouge">akf</code></h2>

<blockquote>
  <p><em><!-- TODO: 2-3 paragraphs introducing the tool. Note that the post +
the tool are intentionally complementary &mdash; post explains the infra
pattern, tool removes the daily-driver friction.
--></em></p>
</blockquote>

<p>The operator workflow described above used to be a six-line shell snippet that
every engineer re-typed (and got wrong) every time. I wrote
<a href="https://pypi.org/project/azkv-ssh-fetch/"><code class="language-plaintext highlighter-rouge">azkv-ssh-fetch</code></a> to remove the
boilerplate:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pipx <span class="nb">install </span>azkv-ssh-fetch

<span class="c"># Find the SSH-shaped secrets in a vault</span>
akf list <span class="nt">--vault</span> prod-myteam-kv

<span class="c"># Pull a key to ~/.ssh/&lt;name&gt; with mode 0600 (atomic)</span>
akf fetch <span class="nt">--vault</span> prod-myteam-kv my-vmss-ssh

<span class="c"># Fetch + Bastion SSH in one command (key is shredded on disconnect by default)</span>
akf connect <span class="se">\</span>
  <span class="nt">--vault</span> prod-myteam-kv <span class="se">\</span>
  <span class="nt">--secret</span> my-vmss-ssh <span class="se">\</span>
  <span class="nt">--bastion</span> my-bastion <span class="nt">--bastion-rg</span> my-bastion-rg <span class="se">\</span>
  <span class="nt">--target-id</span> <span class="s2">"/subscriptions/.../virtualMachineScaleSets/my-vmss/virtualMachines/0"</span>
</code></pre></div></div>

<p>Source: <a href="https://github.com/NaeemH/azkv-ssh-fetch">github.com/NaeemH/azkv-ssh-fetch</a> · License: MIT.</p>

<h2 id="gotchas">Gotchas</h2>

<blockquote>
  <p>*&lt;!– TODO: bulleted list. Drafts:</p>
  <ul>
    <li>KV name length cap (24 chars) bites when you template names</li>
    <li>Bastion <em>Standard</em> SKU minimum for native-client tunneling</li>
    <li>VMSS image must have password auth disabled or this whole model is theater</li>
    <li>Don’t store the private key with content_type other than ‘application/x-pem-file’ — KV will let you, but tools may not filter it</li>
    <li>SIG image generation mismatch (Gen1 vs Gen2) — separate post material
–&gt;*</li>
  </ul>
</blockquote>

<h2 id="further-reading">Further reading</h2>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/azure/bastion/connect-native-client-windows">Azure Bastion native client</a></li>
  <li><a href="https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide">Key Vault RBAC guide</a></li>
  <li><a href="https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key">Terraform <code class="language-plaintext highlighter-rouge">tls_private_key</code></a></li>
  <li><a href="https://developer.hashicorp.com/terraform/cdktf">CDKTF docs</a></li>
</ul>

<hr />

<p><em>Found a mistake or want to discuss? Open an issue at
<a href="https://github.com/NaeemH/azkv-ssh-fetch/issues"><code class="language-plaintext highlighter-rouge">NaeemH/azkv-ssh-fetch</code></a> or
ping me on <a href="https://www.linkedin.com/in/naeem-hossain">LinkedIn</a>.</em></p>]]></content><author><name></name></author><category term="azure" /><category term="terraform" /><category term="cdktf" /><category term="key-vault" /><category term="bastion" /><category term="vmss" /><summary type="html"><![CDATA[A practical CDKTF walkthrough for managing VMSS SSH access through Azure Key Vault and Bastion, plus the operator tool that uses it daily.]]></summary></entry></feed>