<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>work &amp;mdash; heyimusa</title>
    <link>https://heyimusa.blog/tag:work</link>
    <description></description>
    <pubDate>Thu, 03 Sep 2026 18:44:43 +0000</pubDate>
    <item>
      <title>I built a security-impact diff for GitOps</title>
      <link>https://heyimusa.blog/i-built-a-security-impact-diff-for-gitops</link>
      <description>&lt;![CDATA[Before and after rendered manifests flow into a focused security-impact review&#xA;&#xA;Original diagram for this build note. It shows the tool’s review model, not a production cluster topology.&#xA;&#xA;A Kubernetes pull request can look harmless when the visible change is short: a new RoleBinding, one securityContext field, a route rule, or a service type. The difficult part is that these changes rarely read like the security event they create.&#xA;&#xA;A Role can gain access to Secrets. A workload can begin sharing a host network namespace. A route can make a new hostname reachable. None of that has to look dramatic in a line-by-line diff.&#xA;&#xA;I wanted a small tool that starts from the artifact GitOps teams already review: rendered manifests before and after a change. That became kube-blast-radius, an offline CLI that explains the security-relevant capability or surface added by that change.&#xA;&#xA;!--more--&#xA;&#xA;The input is the delivery artifact, not another policy file&#xA;&#xA;The command compares two already-rendered states. It can also render Kustomize directories or Helm charts first.&#xA;&#xA;kube-blast-radius diff \&#xA;  --before rendered/main.yaml \&#xA;  --after rendered/pr.yaml&#xA;&#xA;For a simple fixture, a Role gained Secret access while a Deployment enabled hostNetwork and added a privileged container. The CLI reported:&#xA;&#xA;HIGH  Role/payments/checkout: RBAC rule newly grants access to secrets&#xA;      (RBACSECRETACCESSADDED)&#xA;&#xA;HIGH  Deployment/payments/checkout: workload newly enables hostNetwork&#xA;      (WORKLOADHOSTNETWORKADDED)&#xA;&#xA;HIGH  Deployment/payments/checkout: workload newly adds a privileged container&#xA;      (WORKLOADPRIVILEGEDADDED)&#xA;&#xA;The intent is not to decide whether every finding is wrong. A privileged DaemonSet or a public route can be intentional. The point is to turn a subtle manifest change into a question a reviewer can answer deliberately.&#xA;&#xA;I treated false assurance as the main bug&#xA;&#xA;The first version found the obvious cases quickly. The more important work was finding ways it could say too little.&#xA;&#xA;An independent review caught a few examples. A ClusterRole referenced by a namespace-scoped RoleBinding was initially easy to miss because ClusterRoles do not have a namespace. A kind: List wrapper could hide objects from a naive document parser. And an omitted ServiceAccount and an explicit default ServiceAccount should mean the same thing, not produce a scary but meaningless finding.&#xA;&#xA;Those became regression tests, not release notes:&#xA;&#xA;ClusterRole + RoleBinding -  effective Secret access&#xA;kind: List              -  expanded and analyzed&#xA;missing identity        -  analysis error, not silent skip&#xA;implicit default        -  normalized before comparison&#xA;&#xA;The tool also reports unsupported resource kinds as information instead of quietly declaring the change clean. Its text output always carries the same caveat: enabled checks are not a complete security assessment.&#xA;&#xA;Render first, then compare&#xA;&#xA;Raw YAML is useful, but many GitOps changes live in overlays and values files. The tool supports three modes:&#xA;&#xA;raw        YAML manifest stream&#xA;kustomize  kubectl kustomize directory&#xA;helm       helm template chart with before/after values&#xA;&#xA;The render commands use argument vectors rather than a shell, have a fixed timeout, and do not load kubeconfig or talk to a Kubernetes cluster. That is a deliberate boundary. Rendering still means trusting the chart or overlay source, so untrusted inputs belong in an isolated runner.&#xA;&#xA;I tested the renderers inside a Docker container with a read-only filesystem, a non-root user, all Linux capabilities dropped, no-new-privileges, and read-only mounted fixtures. Kustomize caught a newly enabled hostNetwork; Helm caught a values change that made a container privileged.&#xA;&#xA;A real GitOps artifact changed the scope&#xA;&#xA;The useful test was a read-only render of a detached Traefik route-review artifact from a GitOps repository. I exported only that tracked review directory at the commit that introduced it, rendered it with Kustomize, and compared it with an empty prior state. No cluster, DNS provider, Secret values, or live route was accessed.&#xA;&#xA;The first run did not understand Traefik IngressRoute resources. That was the right result to take seriously: an “unsupported kind” message is better than pretending a route addition has no security meaning.&#xA;&#xA;I added focused support for Traefik route matches. The next run marked the added desktop and mobile route rules as high-severity exposure changes. It did not call them malicious. It made their external-surface effect visible in the review output.&#xA;&#xA;What the first release covers&#xA;&#xA;v0.1.0 looks for changes including:&#xA;&#xA;bound RBAC access to Secrets, wildcards, escalation verbs, and broader resource-name scope;&#xA;new bindings to roles that already grant Secret access;&#xA;privileged containers, host namespaces, hostPath volumes, added Linux capabilities, UID 0, and weakened container hardening;&#xA;Service external exposure, Kubernetes Ingress hosts and paths, removed NetworkPolicies, and Traefik IngressRoute rules.&#xA;&#xA;It returns text for a reviewer and JSON for CI. High findings exit with code 1; malformed manifests and renderer failures exit with 2.&#xA;&#xA;What it does not claim&#xA;&#xA;This is not a cluster security platform. It does not calculate full NetworkPolicy reachability, inspect cloud IAM, query a live cluster, read Secret values, or certify compliance.&#xA;&#xA;That limitation is part of the product. A GitOps diff tool should be trusted for the specific questions it can answer, not for imaginary coverage.&#xA;&#xA;The source, release binary, and checksum are public:&#xA;&#xA;kube-blast-radius source&#xA;v0.1.0 release&#xA;&#xA;---&#xA;&#xA;Test notes: I built and tested the CLI locally and in disposable Docker containers. The hardened renderer test used a read-only filesystem, non-root UID 65532, dropped Linux capabilities, no-new-privileges, and read-only mounted synthetic fixtures. The GitOps validation was a read-only render of a detached review artifact; no cluster, Secret value, DNS provider, or live route was accessed. The examples are sanitized.&#xA;&#xA;Related work: Security and regulated operations case study · A rollout needs a return address&#xA;&#xA;!-- taxonomy: #topiccloudsecurity #topicgitopsdelivery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="/img/articles/kube-blast-radius-security-impact-diff.svg" alt="Before and after rendered manifests flow into a focused security-impact review"></p>

<p><em>Original diagram for this build note. It shows the tool’s review model, not a production cluster topology.</em></p>

<p>A Kubernetes pull request can look harmless when the visible change is short: a new RoleBinding, one <code>securityContext</code> field, a route rule, or a service type. The difficult part is that these changes rarely read like the security event they create.</p>

<p>A Role can gain access to Secrets. A workload can begin sharing a host network namespace. A route can make a new hostname reachable. None of that has to look dramatic in a line-by-line diff.</p>

<p>I wanted a small tool that starts from the artifact GitOps teams already review: rendered manifests before and after a change. That became <a href="https://github.com/heyimusa/kube-blast-radius">kube-blast-radius</a>, an offline CLI that explains the security-relevant capability or surface added by that change.</p>



<h2 id="the-input-is-the-delivery-artifact-not-another-policy-file">The input is the delivery artifact, not another policy file</h2>

<p>The command compares two already-rendered states. It can also render Kustomize directories or Helm charts first.</p>

<pre><code class="language-bash">kube-blast-radius diff \
  --before rendered/main.yaml \
  --after rendered/pr.yaml
</code></pre>

<p>For a simple fixture, a Role gained Secret access while a Deployment enabled <code>hostNetwork</code> and added a privileged container. The CLI reported:</p>

<pre><code class="language-text">HIGH  Role/payments/checkout: RBAC rule newly grants access to secrets
      (RBAC_SECRET_ACCESS_ADDED)

HIGH  Deployment/payments/checkout: workload newly enables hostNetwork
      (WORKLOAD_HOST_NETWORK_ADDED)

HIGH  Deployment/payments/checkout: workload newly adds a privileged container
      (WORKLOAD_PRIVILEGED_ADDED)
</code></pre>

<p>The intent is not to decide whether every finding is wrong. A privileged DaemonSet or a public route can be intentional. The point is to turn a subtle manifest change into a question a reviewer can answer deliberately.</p>

<h2 id="i-treated-false-assurance-as-the-main-bug">I treated false assurance as the main bug</h2>

<p>The first version found the obvious cases quickly. The more important work was finding ways it could say too little.</p>

<p>An independent review caught a few examples. A ClusterRole referenced by a namespace-scoped RoleBinding was initially easy to miss because ClusterRoles do not have a namespace. A <code>kind: List</code> wrapper could hide objects from a naive document parser. And an omitted ServiceAccount and an explicit <code>default</code> ServiceAccount should mean the same thing, not produce a scary but meaningless finding.</p>

<p>Those became regression tests, not release notes:</p>

<pre><code class="language-text">ClusterRole + RoleBinding -&gt; effective Secret access
kind: List              -&gt; expanded and analyzed
missing identity        -&gt; analysis error, not silent skip
implicit default        -&gt; normalized before comparison
</code></pre>

<p>The tool also reports unsupported resource kinds as information instead of quietly declaring the change clean. Its text output always carries the same caveat: enabled checks are not a complete security assessment.</p>

<h2 id="render-first-then-compare">Render first, then compare</h2>

<p>Raw YAML is useful, but many GitOps changes live in overlays and values files. The tool supports three modes:</p>

<pre><code class="language-text">raw        YAML manifest stream
kustomize  kubectl kustomize &lt;directory&gt;
helm       helm template &lt;chart&gt; with before/after values
</code></pre>

<p>The render commands use argument vectors rather than a shell, have a fixed timeout, and do not load kubeconfig or talk to a Kubernetes cluster. That is a deliberate boundary. Rendering still means trusting the chart or overlay source, so untrusted inputs belong in an isolated runner.</p>

<p>I tested the renderers inside a Docker container with a read-only filesystem, a non-root user, all Linux capabilities dropped, <code>no-new-privileges</code>, and read-only mounted fixtures. Kustomize caught a newly enabled <code>hostNetwork</code>; Helm caught a values change that made a container privileged.</p>

<h2 id="a-real-gitops-artifact-changed-the-scope">A real GitOps artifact changed the scope</h2>

<p>The useful test was a read-only render of a detached Traefik route-review artifact from a GitOps repository. I exported only that tracked review directory at the commit that introduced it, rendered it with Kustomize, and compared it with an empty prior state. No cluster, DNS provider, Secret values, or live route was accessed.</p>

<p>The first run did not understand Traefik <code>IngressRoute</code> resources. That was the right result to take seriously: an “unsupported kind” message is better than pretending a route addition has no security meaning.</p>

<p>I added focused support for Traefik route matches. The next run marked the added desktop and mobile route rules as high-severity exposure changes. It did not call them malicious. It made their external-surface effect visible in the review output.</p>

<h2 id="what-the-first-release-covers">What the first release covers</h2>

<p><code>v0.1.0</code> looks for changes including:</p>
<ul><li>bound RBAC access to Secrets, wildcards, escalation verbs, and broader resource-name scope;</li>
<li>new bindings to roles that already grant Secret access;</li>
<li>privileged containers, host namespaces, hostPath volumes, added Linux capabilities, UID 0, and weakened container hardening;</li>
<li>Service external exposure, Kubernetes Ingress hosts and paths, removed NetworkPolicies, and Traefik <code>IngressRoute</code> rules.</li></ul>

<p>It returns text for a reviewer and JSON for CI. High findings exit with code <code>1</code>; malformed manifests and renderer failures exit with <code>2</code>.</p>

<h2 id="what-it-does-not-claim">What it does not claim</h2>

<p>This is not a cluster security platform. It does not calculate full NetworkPolicy reachability, inspect cloud IAM, query a live cluster, read Secret values, or certify compliance.</p>

<p>That limitation is part of the product. A GitOps diff tool should be trusted for the specific questions it can answer, not for imaginary coverage.</p>

<p>The source, release binary, and checksum are public:</p>
<ul><li><a href="https://github.com/heyimusa/kube-blast-radius">kube-blast-radius source</a></li>
<li><a href="https://github.com/heyimusa/kube-blast-radius/releases/tag/v0.1.0">v0.1.0 release</a></li></ul>

<hr>

<p><strong>Test notes:</strong> I built and tested the CLI locally and in disposable Docker containers. The hardened renderer test used a read-only filesystem, non-root UID <code>65532</code>, dropped Linux capabilities, <code>no-new-privileges</code>, and read-only mounted synthetic fixtures. The GitOps validation was a read-only render of a detached review artifact; no cluster, Secret value, DNS provider, or live route was accessed. The examples are sanitized.</p>

<p><strong>Related work:</strong> <a href="/portfolio/work/#work">Security and regulated operations case study</a> · <a href="/a-rollout-needs-a-return-address">A rollout needs a return address</a></p>


]]></content:encoded>
      <guid>https://heyimusa.blog/i-built-a-security-impact-diff-for-gitops</guid>
      <pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>A rollout needs a return address</title>
      <link>https://heyimusa.blog/a-rollout-needs-a-return-address</link>
      <description>&lt;![CDATA[A small declarative rollout and rollback experiment&#xA;&#xA;Original diagram for this note. It describes a Docker-only simulated release flow, not a production deployment or benchmark.&#xA;&#xA;I do not trust a deployment plan until I can explain what happens when the new version is the problem.&#xA;&#xA;!--more--&#xA;&#xA;That sounds obvious. It is still easy to build a delivery process around the happy path: create an image, update a manifest, watch the rollout, call it done. The awkward part starts when a release looks healthy enough to leave the pipeline but is not healthy enough to keep.&#xA;&#xA;To keep this small, I ran a toy release flow in a disposable Docker container. There was no host change, cluster, registry, or real service. The container held a desired-state file for a fictional checkout service, plus a saved copy of the known-good release.&#xA;&#xA;apply: checkout:1.4.3&#xA;healthcheck: /readyz -  FAIL (simulated)&#xA;rollback: checkout:1.4.2&#xA;healthcheck: /readyz -  OK (simulated)&#xA;finaldesiredstate: image=checkout:1.4.2 replicas=3&#xA;&#xA;The experiment is intentionally boring. That is the point. A rollback should not require somebody to reconstruct the previous state from memory while a production graph turns red.&#xA;&#xA;The release needs a return address&#xA;&#xA;A deployment is a state transition. The candidate release is only one half of that transition; the other half is the state you can return to when the candidate fails.&#xA;&#xA;In the tiny experiment, that state was just a saved file:&#xA;&#xA;image=checkout:1.4.2&#xA;replicas=3&#xA;&#xA;Real systems are less neat. There may be configuration changes, schema compatibility, feature flags, asynchronous workers, or traffic shifts. But the basic question does not change: what exact state are we restoring, and can the deployment system express it?&#xA;&#xA;If the answer is &#34;we will figure it out,&#34; the rollback plan is not really a plan.&#xA;&#xA;Rollback is not an apology button&#xA;&#xA;People often talk about rollback as if it is the opposite of deployment. It is not. It is another deployment, with the same need for identity, evidence, and verification.&#xA;&#xA;A useful rollback path has at least three properties:&#xA;&#xA;The previous artifact or desired state is identifiable.&#xA;The path to apply it is known before the incident.&#xA;The system has a signal that says the restored version is actually healthy.&#xA;&#xA;The third item gets skipped surprisingly often. Reverting an image tag is not proof that the service recovered. It only proves that the deployment controller accepted another instruction.&#xA;&#xA;Why declarative state helps&#xA;&#xA;This is where GitOps and other declarative delivery patterns earn their keep. They make the intended state visible. They also make a reversal more concrete: restore a reviewed revision, reconcile it, and watch the same health signals that justified the rollout.&#xA;&#xA;That does not make every rollback safe. Database migrations can make a simple reversal impossible. A downstream dependency may have changed underneath you. A feature flag may be the safer first lever. Declarative state is not magic; it just removes one common source of panic: having to guess which version and configuration were running before the change.&#xA;&#xA;The part I would test next&#xA;&#xA;The toy flow did not cover the hard cases. It did not include a database, traffic management, or an actual Kubernetes controller. It only checked the shape of the idea: a failed health check should lead to a named previous state, then to a second health check.&#xA;&#xA;The next useful step is to run the same exercise against a non-critical service in an isolated environment:&#xA;&#xA;deploy a known candidate;&#xA;deliberately fail a readiness condition;&#xA;reconcile the previous revision;&#xA;verify the restored service through the same route and alert signal users depend on.&#xA;&#xA;If that feels cumbersome in a test environment, it will feel worse during an incident.&#xA;&#xA;---&#xA;&#xA;Test notes: This note is based on a disposable Docker container limited to 0.25 CPU and 128 MiB memory. It simulated a declarative checkout release changing from 1.4.2 to 1.4.3, a failed readiness check, and restoration to 1.4.2. No production infrastructure, repository, cluster, registry, or database was accessed.&#xA;&#xA;Related work: Turning deployments into a repeatable platform capability&#xA;&#xA;!-- taxonomy: #topicgitopsdelivery #topicincidentrecovery --&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="/img/articles/rollbacks-need-a-place-to-land.svg" alt="A small declarative rollout and rollback experiment"></p>

<p><em>Original diagram for this note. It describes a Docker-only simulated release flow, not a production deployment or benchmark.</em></p>

<p>I do not trust a deployment plan until I can explain what happens when the new version is the problem.</p>



<p>That sounds obvious. It is still easy to build a delivery process around the happy path: create an image, update a manifest, watch the rollout, call it done. The awkward part starts when a release looks healthy enough to leave the pipeline but is not healthy enough to keep.</p>

<p>To keep this small, I ran a toy release flow in a disposable Docker container. There was no host change, cluster, registry, or real service. The container held a desired-state file for a fictional <code>checkout</code> service, plus a saved copy of the known-good release.</p>

<pre><code class="language-text">apply: checkout:1.4.3
healthcheck: /readyz -&gt; FAIL (simulated)
rollback: checkout:1.4.2
healthcheck: /readyz -&gt; OK (simulated)
final_desired_state: image=checkout:1.4.2 replicas=3
</code></pre>

<p>The experiment is intentionally boring. That is the point. A rollback should not require somebody to reconstruct the previous state from memory while a production graph turns red.</p>

<h2 id="the-release-needs-a-return-address">The release needs a return address</h2>

<p>A deployment is a state transition. The candidate release is only one half of that transition; the other half is the state you can return to when the candidate fails.</p>

<p>In the tiny experiment, that state was just a saved file:</p>

<pre><code class="language-text">image=checkout:1.4.2
replicas=3
</code></pre>

<p>Real systems are less neat. There may be configuration changes, schema compatibility, feature flags, asynchronous workers, or traffic shifts. But the basic question does not change: <strong>what exact state are we restoring, and can the deployment system express it?</strong></p>

<p>If the answer is “we will figure it out,” the rollback plan is not really a plan.</p>

<h2 id="rollback-is-not-an-apology-button">Rollback is not an apology button</h2>

<p>People often talk about rollback as if it is the opposite of deployment. It is not. It is another deployment, with the same need for identity, evidence, and verification.</p>

<p>A useful rollback path has at least three properties:</p>
<ul><li>The previous artifact or desired state is identifiable.</li>
<li>The path to apply it is known before the incident.</li>
<li>The system has a signal that says the restored version is actually healthy.</li></ul>

<p>The third item gets skipped surprisingly often. Reverting an image tag is not proof that the service recovered. It only proves that the deployment controller accepted another instruction.</p>

<h2 id="why-declarative-state-helps">Why declarative state helps</h2>

<p>This is where GitOps and other declarative delivery patterns earn their keep. They make the intended state visible. They also make a reversal more concrete: restore a reviewed revision, reconcile it, and watch the same health signals that justified the rollout.</p>

<p>That does not make every rollback safe. Database migrations can make a simple reversal impossible. A downstream dependency may have changed underneath you. A feature flag may be the safer first lever. Declarative state is not magic; it just removes one common source of panic: having to guess which version and configuration were running before the change.</p>

<h2 id="the-part-i-would-test-next">The part I would test next</h2>

<p>The toy flow did not cover the hard cases. It did not include a database, traffic management, or an actual Kubernetes controller. It only checked the shape of the idea: a failed health check should lead to a named previous state, then to a second health check.</p>

<p>The next useful step is to run the same exercise against a non-critical service in an isolated environment:</p>
<ol><li>deploy a known candidate;</li>
<li>deliberately fail a readiness condition;</li>
<li>reconcile the previous revision;</li>
<li>verify the restored service through the same route and alert signal users depend on.</li></ol>

<p>If that feels cumbersome in a test environment, it will feel worse during an incident.</p>

<hr>

<p><strong>Test notes:</strong> This note is based on a disposable Docker container limited to 0.25 CPU and 128 MiB memory. It simulated a declarative <code>checkout</code> release changing from <code>1.4.2</code> to <code>1.4.3</code>, a failed readiness check, and restoration to <code>1.4.2</code>. No production infrastructure, repository, cluster, registry, or database was accessed.</p>

<p><strong>Related work:</strong> <a href="/portfolio/work/#work">Turning deployments into a repeatable platform capability</a></p>


]]></content:encoded>
      <guid>https://heyimusa.blog/a-rollout-needs-a-return-address</guid>
      <pubDate>Sun, 19 Jul 2026 17:31:09 +0000</pubDate>
    </item>
  </channel>
</rss>