<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrey Lesnikov</title>
    <description>The latest articles on DEV Community by Andrey Lesnikov (@justrunme).</description>
    <link>https://dev.to/justrunme</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4187294%2F8391d73a-4dc9-4312-aea9-64ad5984dfab.png</url>
      <title>DEV Community: Andrey Lesnikov</title>
      <link>https://dev.to/justrunme</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/justrunme"/>
    <language>en</language>
    <item>
      <title>Don't Give AI Agents Root. Let Them Open Pull Requests.</title>
      <dc:creator>Andrey Lesnikov</dc:creator>
      <pubDate>Wed, 21 Oct 2026 08:16:26 +0000</pubDate>
      <link>https://dev.to/justrunme/dont-give-ai-agents-root-let-them-open-pull-requests-513f</link>
      <guid>https://dev.to/justrunme/dont-give-ai-agents-root-let-them-open-pull-requests-513f</guid>
      <description>&lt;p&gt;Would I give an AI agent root access to a production Kubernetes cluster? No. Would I let it open pull requests? Gladly.&lt;/p&gt;

&lt;p&gt;That sounds like a contradiction until you look at where the real control point in a GitOps setup sits. It isn't the cluster. It's the path a change takes before it reaches the cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea that clicked for me
&lt;/h2&gt;

&lt;p&gt;Earlier this month Mauro Morales, a CNCF Ambassador, published &lt;a href="https://www.cncf.io/blog/2026/10/08/dont-give-ai-agents-root-make-them-propose-the-next-system-state/" rel="noopener noreferrer"&gt;Don't give AI agents root. Make them propose the next system state.&lt;/a&gt; on the CNCF blog. His core argument is simple: an agent should inspect the current state of a system and propose the next one. It should not apply that state itself. The proposal goes through the same software factory every human change goes through: review, CI, tests, build, signing, release, deployment.&lt;/p&gt;

&lt;p&gt;One line from his post stuck with me: if you give an agent root and expect a well-written &lt;code&gt;AGENTS.md&lt;/code&gt; to prevent an unrecoverable mistake, you are fooling yourself. The model is nondeterministic. You can't guarantee the outcome, so you have to guarantee the path instead.&lt;/p&gt;

&lt;p&gt;His post focuses on the operating system layer and immutable hosts. I want to look at the same principle from the Kubernetes and GitOps side, because for many platform teams the boundary already exists. We just don't always enforce it properly for bots.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitOps already drew the line
&lt;/h2&gt;

&lt;p&gt;If you run Argo CD or Flux, this model is almost boring. The rule is already there: what is not in Git does not end up in the cluster. The controller reconciles toward the desired state in the repository, and drift gets corrected.&lt;/p&gt;

&lt;p&gt;So the safe shape for an agent looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read access&lt;/strong&gt; to metrics, logs, events and the manifests in Git, so it can diagnose problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write access to a branch&lt;/strong&gt;, so it can propose a change as a pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing else.&lt;/strong&gt; No &lt;code&gt;cluster-admin&lt;/code&gt;, no &lt;code&gt;kubectl apply&lt;/code&gt; against production, no direct access to the sync controller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent becomes one more contributor. Its change is a diff that a human can read, CI can test and policy can reject. If it's wrong, you close the PR. If it was merged and turns out wrong, you revert a commit, and you know exactly which commit and which identity introduced it.&lt;/p&gt;

&lt;p&gt;That traceability is the part people underestimate. It's not only about stopping bad changes. It's about knowing afterwards what changed, when and who or what proposed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap: "root with a few extra steps"
&lt;/h2&gt;

&lt;p&gt;Here is the point from Mauro's post that I think matters most: the agent's identity may propose, but it must not approve, merge, or change the pipeline. Otherwise you have just replaced root with root plus a few extra steps.&lt;/p&gt;

&lt;p&gt;This is exactly where real setups tend to fall short. A common pattern looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Someone creates a bot account or a GitHub App for the agent.&lt;/li&gt;
&lt;li&gt;Setting up fine-grained permissions takes time, so it gets admin on the repository "for now".&lt;/li&gt;
&lt;li&gt;Branch protection has an exception for that account, or the bot can approve its own PRs through a second token.&lt;/li&gt;
&lt;li&gt;The pipeline definition lives in the same repo, and the bot can edit it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At that point the PR flow is theater. The agent can propose a change, approve it, merge it and adjust the CI job that was supposed to catch it. Argo CD then faithfully syncs whatever landed on &lt;code&gt;main&lt;/code&gt;. The cluster is protected by GitOps in theory and wide open in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check in a setup like this
&lt;/h2&gt;

&lt;p&gt;None of this needs new tooling. It's mostly configuration you probably already have, applied consistently to non-human identities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate identity per agent.&lt;/strong&gt; No shared tokens with humans or other automation, so the audit trail means something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch protection without bypass for bots.&lt;/strong&gt; Required reviews from humans, required status checks, no admin override for the agent account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No self-approval.&lt;/strong&gt; The identity that opened a PR can't be the one that approves it, and there is no second token lying around that makes that possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline files are protected.&lt;/strong&gt; Changes to CI definitions, Argo CD &lt;code&gt;Application&lt;/code&gt; objects or policy files go through &lt;code&gt;CODEOWNERS&lt;/code&gt; with a human owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived, scoped credentials.&lt;/strong&gt; The agent's read access to observability data should expire and be limited to what it needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy in CI fails closed.&lt;/strong&gt; If a policy check can't run or can't decide, the result is "not approved", never a silent green.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point deserves extra emphasis. A gate that passes when it has missing data is not a gate. For agent-proposed changes that matters even more, because the volume of proposals can grow much faster than the attention of the reviewers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves us
&lt;/h2&gt;

&lt;p&gt;I don't think the answer is to keep agents out of production work. They are useful for reading signals, correlating logs and proposing a fix, especially when a tired on-call engineer would be doing it at 3 a.m. The answer is to give them the same contract every engineer on the team already has: you can suggest anything, and the system decides what gets applied.&lt;/p&gt;

&lt;p&gt;GitOps gave us that contract years ago. The job now is to make sure the bot account actually lives under it, instead of quietly sitting above it because it was quicker to set up that way.&lt;/p&gt;

&lt;p&gt;So I'll ask the same question I asked on LinkedIn: how much is an agent allowed to do in your production environment today, honestly?&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>ai</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Rehearse the Change Before You Apply It: Fail-Closed Blast Radius for Kubernetes and Argo CD</title>
      <dc:creator>Andrey Lesnikov</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:56:44 +0000</pubDate>
      <link>https://dev.to/justrunme/rehearse-the-change-before-you-apply-it-fail-closed-blast-radius-for-kubernetes-and-argo-cd-3l6l</link>
      <guid>https://dev.to/justrunme/rehearse-the-change-before-you-apply-it-fail-closed-blast-radius-for-kubernetes-and-argo-cd-3l6l</guid>
      <description>&lt;p&gt;A Helm upgrade scales two deployments in a &lt;code&gt;payments&lt;/code&gt; namespace: one goes from 6 to 18 replicas, the other from 4 to 12. Review is easy. CI is green. The diff is tiny. Argo CD syncs it, and a few minutes later the new pods are stuck. The events say &lt;code&gt;FailedCreatePodSandBox: failed to assign an IP address&lt;/code&gt;. The pod IP pool was nearly full, and nobody in the review thought about it, because nobody in the review looks at IP capacity when they read a replica count.&lt;/p&gt;

&lt;p&gt;That scenario is not hypothetical decoration. It is the main end-to-end fixture in my open-source project, &lt;a href="https://github.com/justrunme/architecture-rehearsal" rel="noopener noreferrer"&gt;Architecture Rehearsal&lt;/a&gt;, because it is the cleanest example of a failure class I care about: changes whose blast radius only shows up once production executes them. The review had opinions. Production had the actual dependency graph.&lt;/p&gt;

&lt;p&gt;I'm a DevOps platform engineer, and this post explains what the tool does, how it is built, what it has actually been tested against, and where it falls short.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: rehearse first, and treat "I don't know" as a result
&lt;/h2&gt;

&lt;p&gt;The core idea is simple. Before a change is applied, build a model of the relevant part of the system, apply the change to the model, and run a set of deterministic failure scenarios against it. Then let a policy decide whether the change is allowed to proceed.&lt;/p&gt;

&lt;p&gt;The part I spent the most time on is a rule from the project's first ADR:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Missing data never becomes false &lt;code&gt;approve&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most gates I have used are binary: pass or fail. The problem is what they do when they lack information. If the PDB inventory was never collected, a binary checker usually finds no PDB violations and passes. That is a false approve, and it is worse than having no gate, because now there is a green check to point at.&lt;/p&gt;

&lt;p&gt;So the gate has four outcomes, each with its own exit code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Exit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No matched high-risk patterns; required coverage present&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Medium-risk findings&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High/critical findings with evidence&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unknown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required data missing, not safe to approve&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;unknown&lt;/code&gt; is fail-closed by default. CI has to treat exit 4 as a failing gate, or someone has to explicitly write a policy that says otherwise. The intended consequence is that operators fix their collectors and RBAC instead of ignoring the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The pipeline, in the repo's own words, is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Collect â†’ Model â†’ Propose â†’ Rehearse â†’ Gate â†’ Observe â†’ Verify â†’ Calibrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A causal graph from a cluster baseline
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;rehearsal snapshot k8s&lt;/code&gt; turns a cluster dump (a &lt;code&gt;kubectl&lt;/code&gt; List export, or live collection) into a baseline snapshot: workloads, services, PVCs, NetworkPolicies, PDBs and the edges between them. A change is compiled from rendered manifests (&lt;code&gt;rehearsal change manifests&lt;/code&gt;), from Terraform plans, or from an Argo CD Application's desired state. The change is scoped: by default it cannot remove nodes, so a rendered chart for one namespace doesn't accidentally look like "delete everything else in GitOps".&lt;/p&gt;

&lt;p&gt;The scenarios are plain Go code, each with a name and a risk model. The current set includes &lt;code&gt;cni-ip-capacity&lt;/code&gt;, &lt;code&gt;pdb-disruption&lt;/code&gt;, &lt;code&gt;rwo-node-loss&lt;/code&gt;, &lt;code&gt;volume-az&lt;/code&gt;, &lt;code&gt;service-routing&lt;/code&gt; and &lt;code&gt;prom-zero-match&lt;/code&gt;. For the Argo CD path there are dedicated detectors for scheduling capacity (CPU/memory requests vs allocatable), PVC zone conflicts, NetworkPolicies that cut a dependency, PDB-vs-rollout conflicts, and blast radius. Each one returns PASS, WARN, BLOCK or UNKNOWN. When a scenario doesn't have the facts it needs, it emits an &lt;code&gt;unknown&lt;/code&gt; finding instead of staying silent.&lt;/p&gt;

&lt;p&gt;There is no LLM in the risk path. The capability matrix lists that explicitly as not supported. The graph and the rules decide.&lt;/p&gt;

&lt;h3&gt;
  
  
  The policy gate
&lt;/h3&gt;

&lt;p&gt;Scenarios produce facts. A small policy engine turns them into a decision. This is the built-in default, written as the YAML policy document the engine accepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rehearsal.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RehearsalPolicy&lt;/span&gt;
&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;risk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;["critical","high"]'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"block"'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rollback&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"unavailable"'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;requiredMissing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0'&lt;/span&gt;
&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;risk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"medium"'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"warn"'&lt;/span&gt;
&lt;span class="na"&gt;unknownAsBlock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than the syntax. First, if you omit &lt;code&gt;unknownAsBlock&lt;/code&gt;, it defaults to &lt;code&gt;true&lt;/code&gt;; setting it to &lt;code&gt;false&lt;/code&gt; is allowed but logged as a warning. Second, an expression the engine can't parse doesn't get skipped. It is rejected when the policy loads, and if one somehow reaches evaluation it produces &lt;code&gt;block&lt;/code&gt;. Both of these were bugs I fixed in the latest round of hardening. Before that fix, an omitted &lt;code&gt;unknownAsBlock&lt;/code&gt; meant &lt;code&gt;false&lt;/code&gt;, and an unrecognised expression simply never matched, so a typo could quietly weaken a policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  RehearsalRun: the gate as cluster state
&lt;/h3&gt;

&lt;p&gt;For teams that want this inside the cluster, there is an operator built on controller-runtime and a &lt;code&gt;RehearsalRun&lt;/code&gt; CRD (&lt;code&gt;rehearsal.io/v1beta1&lt;/code&gt;). The example in the repo looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rehearsal.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RehearsalRun&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-rollout&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;baselineRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baselines/prod.json&lt;/span&gt;
  &lt;span class="na"&gt;changeRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changes/payments-v2.json&lt;/span&gt;
  &lt;span class="c1"&gt;# observedRef: observed/prod-after.json&lt;/span&gt;
  &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status carries &lt;code&gt;phase&lt;/code&gt;, &lt;code&gt;decision&lt;/code&gt;, &lt;code&gt;risk&lt;/code&gt;, &lt;code&gt;controlPlaneRunId&lt;/code&gt;, &lt;code&gt;jobId&lt;/code&gt;, &lt;code&gt;evidenceDigest&lt;/code&gt;, a &lt;code&gt;specDigest&lt;/code&gt; to detect drift, and standard conditions (&lt;code&gt;Accepted&lt;/code&gt;, &lt;code&gt;Running&lt;/code&gt;, &lt;code&gt;Ready&lt;/code&gt;, &lt;code&gt;Failed&lt;/code&gt;). Each spec generation gets its own run ID (&lt;code&gt;{namespace}-{name}-{uid8}-g{generation}&lt;/code&gt;), so editing a run produces a new rehearsal instead of overwriting the old record.&lt;/p&gt;

&lt;p&gt;One design decision started as a real security bug: the CR can never carry the control plane URL or token. An earlier version had a &lt;code&gt;controlPlaneURL&lt;/code&gt; field. That meant anyone allowed to create a &lt;code&gt;RehearsalRun&lt;/code&gt; could point the operator's bearer token at a server they controlled. The field was removed in v1.5.1, and now the URL and token come only from the operator's Deployment and Secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the prediction against reality
&lt;/h3&gt;

&lt;p&gt;A gate that is never checked against reality slowly turns into folklore. After a deploy (or after a change was applied anyway), &lt;code&gt;rehearsal verify&lt;/code&gt; compares the rehearsal report with an observed snapshot. It checks that the report is bound to the same baseline and change digests, that the change was actually applied, and that each predicted scenario is consistent with what was observed. Operator annotations such as "we saw failure X" count only as soft checks. The independent scenario check has to pass on its own, and pods stuck in &lt;code&gt;Pending&lt;/code&gt; across the cluster don't count as confirmation. The outcome feeds a calibration step that tracks how accurate each scenario has been.&lt;/p&gt;

&lt;h3&gt;
  
  
  A content-addressed evidence chain
&lt;/h3&gt;

&lt;p&gt;Every analysis writes an evidence chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;baseline â†’ change â†’ proposed â†’ report â†’ observed â†’ verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each link is a SHA-256 digest computed from the live objects. The report digest is always recomputed from the report body, so if someone edits &lt;code&gt;decision&lt;/code&gt; or &lt;code&gt;risk&lt;/code&gt; in a stored report and leaves the old digest field alone, verification fails. The chain can be wrapped in a DSSE-style envelope signed with HMAC or Ed25519. I want to be precise here: this is not full Sigstore, and the threat model says plainly that HMAC does not give you non-repudiation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Argo CD PreSync hook
&lt;/h3&gt;

&lt;p&gt;The newest piece came from an obvious gap. A CI check only runs if the change goes through CI. A manual sync from the Argo CD UI, or a direct commit to the tracked branch, skips it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rehearsal argo-presync&lt;/code&gt; runs the same impact gate as an Argo CD &lt;strong&gt;PreSync&lt;/strong&gt; Job (&lt;code&gt;argocd.argoproj.io/hook: PreSync&lt;/code&gt;). The Job reads the desired manifests, takes a live snapshot, runs the detectors and prints a single JSON decision line. ALLOW and WARN exit 0, so the sync continues. BLOCK exits 3 and UNKNOWN exits 4, and Argo aborts the sync with the live objects untouched. Missing inputs, API errors and timeouts also fail closed.&lt;/p&gt;

&lt;p&gt;There is an audited escape hatch for incidents: annotate the Application with &lt;code&gt;rehearsal.io/break-glass: "true"&lt;/code&gt; plus a non-empty &lt;code&gt;rehearsal.io/break-glass-reason&lt;/code&gt;. The Job logs the override and includes it in the JSON line. Break-glass without a reason is itself treated as a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real run
&lt;/h2&gt;

&lt;p&gt;Everything below comes from the repo at commit &lt;code&gt;920ff4f&lt;/code&gt;, built locally (&lt;code&gt;rehearsal 1.6.0&lt;/code&gt;; the PreSync gate shown later is part of v1.7.0). These are the commands from &lt;code&gt;scripts/e2e_pipeline.sh&lt;/code&gt; (the same steps as &lt;code&gt;make e2e&lt;/code&gt;), run against the payments fixture from the opening.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/justrunme/architecture-rehearsal.git
&lt;span class="nb"&gt;cd &lt;/span&gt;architecture-rehearsal
go build &lt;span class="nt"&gt;-o&lt;/span&gt; bin/rehearsal ./cmd/rehearsal
bash scripts/e2e_pipeline.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Snapshot, scoped change, then analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==&amp;gt; 1/5 snapshot k8s (baseline from kubectl List dump)
wrote snapshot out/e2e-pipeline/baseline.json phase=baseline source=kubernetes-manifests (22 nodes)
==&amp;gt; 2/5 change manifests (scoped to payments; removals off)
wrote change out/e2e-pipeline/change.json (removals=false scope_ns=[payments])
  scope OK: 2 patch(es), 0 removals
==&amp;gt; 3/5 analyze (expect exit 3 block)
risk=critical decision=block findings=1 rollback=unknown digest=270e3b4f4ea2e25509461038cafe2e0fe804027a73abce74b77bc3fb6e55e5f9 evidence=out/e2e-pipeline/change-helm-scale-payments-e2e-20261017T133404Z
  analyze blocked as expected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The finding in &lt;code&gt;latest-report.json&lt;/code&gt; explains the block with numbers instead of a vague severity label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pod IP capacity exhaustion (CNI / VPC)"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Replica delta 20 + surge 6 = 26 IPs needed; available 10 (deficit 16). baseline=14 proposed=34"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"cascade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"baseline_replicas=14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"proposed_replicas=34"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"rollout_surge=6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"requested_ips=26 available=10 deficit=16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"IP exhaustion â†’ FailedCreatePodSandbox â†’ readiness timeout â†’ Helm pending-install"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"recommended_controls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"increase max-pods / prefix delegation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"add nodes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"reduce maxSurge"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same report includes a &lt;code&gt;coverage&lt;/code&gt; block (overall &lt;code&gt;0.45&lt;/code&gt; on this fixture) and lists its gaps, for example &lt;code&gt;no meta.metric_labels â€” prom scenarios limited&lt;/code&gt;. I want the tool to say what it didn't look at.&lt;/p&gt;

&lt;p&gt;The fixture also contains a post-deploy dump for the case where the change was applied anyway. Verification passes, with the independent scenario check succeeding apart from the operator annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"name": "scenario:cni-ip-capacity",
"passed": true,
"detail": "meta.cni_ip_available=0 (explicit CNI provider)"
...
"name": "rollout_status:workload/payments/payments-api",
"detail": "rollout_not_converged (consistent with capacity/CNI prediction risk): readyReplicas=6 want=18",
...
  verify OK outcome=verified score=1 digest=72f7dbab61a69655
  decision=block risk=critical failures=['cni-ip-capacity']
e2e pipeline OK: dumpâ†’graphâ†’scoped changeâ†’blockâ†’verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signing and verifying the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ export REHEARSAL_HMAC_SECRET="$(openssl rand -hex 32)"
$ ./bin/rehearsal evidence sign-dsse --chain out/e2e-pipeline/latest-chain.json --out out/e2e-pipeline/evidence-dsse.json
wrote out/e2e-pipeline/evidence-dsse.json verified=true
$ ./bin/rehearsal evidence verify-dsse --envelope out/e2e-pipeline/evidence-dsse.json
signature OK keyId=hmac-env changeId=change-helm-scale-payments-e2e baseline=3035a1f23205 change=e73dd8fe9fa6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Argo CD fixtures (&lt;code&gt;bash scripts/e2e_argo_impact.sh&lt;/code&gt;) show the four outcomes. This is the case I care about most, where the baseline never recorded PDB collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==&amp;gt; unknown-pdb-inventory (expect 4)
verdict=UNKNOWN decision=unknown findings=1 confidence=low ...
unknown-pdb-inventory exit=4 expect=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated markdown report says why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| `pdb-rollout` | **UNKNOWN** | low | PDB inventory status unknown (baseline did not record PDB collection) |

## Missing data

Gate cannot safely PASS while these are missing:

- `pdb-inventory`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four detectors passed. A binary gate would probably have approved this change. This one refuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What has actually been tested
&lt;/h2&gt;

&lt;p&gt;I keep a claim-by-claim &lt;a href="https://github.com/justrunme/architecture-rehearsal/blob/main/docs/EVIDENCE.md" rel="noopener noreferrer"&gt;EVIDENCE.md&lt;/a&gt; that links each claim to a CI run. In short: the policy gate, the evidence chain and DSSE path are covered by the &lt;code&gt;test&lt;/code&gt; job. The operator installs via Helm on kind in &lt;code&gt;operator-kind-e2e&lt;/code&gt;. The native Argo CD gate runs against real Argo CD Applications on kind in &lt;code&gt;argo-kind-e2e&lt;/code&gt;, where a benign Synced app passes and a risky OutOfSync Deployment is blocked.&lt;/p&gt;

&lt;p&gt;The PreSync hook runs in &lt;code&gt;argo-presync-kind-e2e&lt;/code&gt; with pinned Argo CD v2.14.9. On that fixture set (kind e2e plus an offline matrix), 5 of 5 risky changes were blocked and 0 of 3 safe changes were falsely blocked. The in-cluster hook added roughly 0.9 to 1.0 seconds per sync on a single-node kind cluster. Those are eight cases. That's a regression check. It tells you nothing about field accuracy, and I wouldn't quote it as an accuracy figure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The blast radius is simulated.&lt;/strong&gt; It comes from a graph plus a change, not from fault injection against a live cluster. It complements chaos engineering and doesn't replace it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is only as good as the inventory.&lt;/strong&gt; Fail-closed protects you from false approves caused by &lt;em&gt;missing&lt;/em&gt; data. It can't protect you from data that is present but wrong. The repo explicitly does not claim zero false approves on incomplete real-world inventories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live collection against arbitrary production clusters&lt;/strong&gt; is still at the "reference / not yet" stage in the evidence table. CI uses fixtures and kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-cluster.&lt;/strong&gt; Everything tested is single-node kind. Multi-cluster federation of evidence stores is out of scope for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scenario catalogue is small.&lt;/strong&gt; Six rehearsal scenarios and five Argo detectors cover failure classes I have seen often, not everything that can go wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PreSync hook expects desired manifests in a ConfigMap&lt;/strong&gt;, which adds wiring work. It shipped in the v1.7.0 release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No production SLOs yet.&lt;/strong&gt; There are no published time-to-gate or false-approve-rate numbers beyond the fixture metrics above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing is DSSE-style with HMAC/Ed25519&lt;/strong&gt;, not a full Sigstore/transparency-log setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Next on my list: more evidence from live collection, more scenarios and detectors (especially around IAM and observability coverage, which score 0 on the main fixture), a less manual way to feed desired state into the PreSync hook, and turning calibration data into something a team can read as "this scenario has been right N out of M times on our clusters".&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and tell me where it's wrong
&lt;/h2&gt;

&lt;p&gt;The repo is Apache-2.0: &lt;strong&gt;&lt;a href="https://github.com/justrunme/architecture-rehearsal" rel="noopener noreferrer"&gt;github.com/justrunme/architecture-rehearsal&lt;/a&gt;&lt;/strong&gt;. The fastest way in is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make e2e
make e2e-argo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a longer write-up on the &lt;a href="https://justrunme.com/cases/architecture-rehearsal/" rel="noopener noreferrer"&gt;case study page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm especially interested in two kinds of feedback: failure classes from your own incidents that a graph-plus-scenario model should catch, and places where the gate says &lt;code&gt;approve&lt;/code&gt; when you think it should have said &lt;code&gt;unknown&lt;/code&gt;. Issues and PRs are welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Andrey Lesnikov is a Senior DevOps Platform Engineer based in Halle, Germany, and a 3rd place Company Award recipient at the Cloud Native Rockstar Awards 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>gitops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
