<?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: Vamshidher Reddy Jannapu Reddy</title>
    <description>The latest articles on DEV Community by Vamshidher Reddy Jannapu Reddy (@moneytool).</description>
    <link>https://dev.to/moneytool</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%2F4153436%2F456562f5-94ad-4e4b-b38b-7a09e0ff7534.png</url>
      <title>DEV Community: Vamshidher Reddy Jannapu Reddy</title>
      <link>https://dev.to/moneytool</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moneytool"/>
    <language>en</language>
    <item>
      <title>"Just use RBAC" is half right. Here's the other half for AI agents.</title>
      <dc:creator>Vamshidher Reddy Jannapu Reddy</dc:creator>
      <pubDate>Fri, 09 Oct 2026 01:12:35 +0000</pubDate>
      <link>https://dev.to/moneytool/just-use-rbac-is-half-right-heres-the-other-half-for-ai-agents-548o</link>
      <guid>https://dev.to/moneytool/just-use-rbac-is-half-right-heres-the-other-half-for-ai-agents-548o</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/moneytool/aegis-devops" rel="noopener noreferrer"&gt;Aegis-DevOps&lt;/a&gt;, an open-source check that runs before an AI coding agent's shell commands and blocks the ones your policy forbids. In the last two weeks I've heard the same objection twice. A Reddit thread said RBAC already does this. A maintainer turned down a plugin submission because the agent harness "already has built-in provisions for controlling tool use."&lt;/p&gt;

&lt;p&gt;Both are half right. You should have RBAC, and you should use your agent's permission settings. But neither one answers the question that matters when an agent is about to run &lt;code&gt;kubectl delete&lt;/code&gt;: should &lt;em&gt;this&lt;/em&gt; action, from &lt;em&gt;this&lt;/em&gt; agent, happen right now?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I drafted this article with help from an AI assistant and reviewed it myself. The command results below are from running aegis-devops 0.3.2 against its example policy.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent permission settings match text
&lt;/h2&gt;

&lt;p&gt;Claude Code's permission rules are a good example, and its documentation is unusually honest about them. A Bash rule matches the command text the model writes. The docs say a deny rule "isn't a security boundary around the program", and give examples: &lt;code&gt;Bash(git push *)&lt;/code&gt; stops &lt;code&gt;git push origin main&lt;/code&gt; but not &lt;code&gt;git -C . push origin main&lt;/code&gt;. For inspecting the full command before it runs, they point you to a PreToolUse hook.&lt;/p&gt;

&lt;p&gt;That's fine for what permission settings are for: deciding what the agent may run without asking you. It isn't a policy engine, and nobody claims it is.&lt;/p&gt;

&lt;p&gt;A hook can do more because it can work out what the command &lt;em&gt;does&lt;/em&gt;. These all reach the same rule in Aegis ("no deletes in the &lt;code&gt;prod&lt;/code&gt; namespace"):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command the agent writes&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubectl delete deploy web -n prod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubectl -n prod delete deploy web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubectl delete deploy web -nprod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/usr/bin/kubectl ...&lt;/code&gt;, &lt;code&gt;sudo kubectl ...&lt;/code&gt;, &lt;code&gt;env kubectl ...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;BLOCK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bash -c 'kubectl delete deploy web -n prod'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;K=kubectl; $K delete deploy web -n prod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;refused: can't be checked statically, so the hook denies it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row matters as much as the others. A guard that can't understand a command should say no, not "no rule matched, go ahead".&lt;/p&gt;

&lt;h2&gt;
  
  
  RBAC sees the developer, not the agent
&lt;/h2&gt;

&lt;p&gt;RBAC and IAM decide what an &lt;em&gt;identity&lt;/em&gt; may do. A coding agent on a laptop usually runs with the developer's own kubeconfig and cloud credentials. As far as the API server is concerned, the agent &lt;em&gt;is&lt;/em&gt; the developer, with every permission the developer has.&lt;/p&gt;

&lt;p&gt;You can fix that by giving agents their own identities, and you should. But even then, RBAC is a static grant. It can say "this service account may delete deployments in prod". It can't say "this agent may not, unless a human approved it, and not during the release freeze", and it has no idea where a rule came from.&lt;/p&gt;

&lt;p&gt;That last part is the reason I built Aegis. In an agent's world, a "rule" can arrive inside a Jira ticket or a Slack message the agent was asked to read. So an Aegis rule only gets a vote if nobody has changed it since it was signed, and if its author was allowed to write that kind of rule. A line planted in a ticket gets no vote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use all three
&lt;/h2&gt;

&lt;p&gt;The honest answer to "why not RBAC?" is "yes, and":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RBAC / IAM&lt;/strong&gt;: the hard ceiling on what any identity can do. Keep it tight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent permission settings&lt;/strong&gt;: what the agent may run without asking you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pre-execution policy check&lt;/strong&gt;: whether this specific action should happen, by rules you can trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The policy check doesn't have to stay on the laptop either. Once agents have their own identities, Aegis compiles the same policy into the platform layer: &lt;code&gt;aegis compile aws&lt;/code&gt; writes Service Control Policies, and &lt;code&gt;aegis compile kubernetes&lt;/code&gt; writes ValidatingAdmissionPolicies. That covers calls that never go through the hook, like a boto3 script. Both are previews, and I wrote up &lt;a href="https://dev.to/moneytool/my-agent-guardrail-only-lived-on-the-laptop-so-i-compiled-it-into-aws-j1d"&gt;how the AWS part works&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do yet
&lt;/h2&gt;

&lt;p&gt;Readers keep finding real gaps, which is the point of writing these posts. This week one found that a namespace-scoped rule doesn't match a command that relies on the namespace in your current kube context, and that &lt;code&gt;kubectl apply -f -&lt;/code&gt; is allowed because the piped manifest is never read. Both are being fixed. Aegis is alpha. Don't put it in front of anything you care about without reading the open gaps in the repo first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;aegis-devops &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; aegis init .aegis

&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude plugin marketplace add moneytool/aegis-devops
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;aegis-devops@aegis-devops

&lt;span class="c"&gt;# Codex, Copilot, VS Code, Cursor, Gemini CLI, OpenCode&lt;/span&gt;
aegis &lt;span class="nb"&gt;install &lt;/span&gt;codex   &lt;span class="c"&gt;# or copilot | vscode | cursor | gemini | opencode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you think the RBAC answer is enough for your setup, I'd like to hear why. Issues are open at &lt;a href="https://github.com/moneytool/aegis-devops" rel="noopener noreferrer"&gt;github.com/moneytool/aegis-devops&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>My agent guardrail was fine. Its kubectl parser wasn't.</title>
      <dc:creator>Vamshidher Reddy Jannapu Reddy</dc:creator>
      <pubDate>Thu, 08 Oct 2026 01:40:33 +0000</pubDate>
      <link>https://dev.to/moneytool/my-agent-guardrail-was-fine-its-kubectl-parser-wasnt-c65</link>
      <guid>https://dev.to/moneytool/my-agent-guardrail-was-fine-its-kubectl-parser-wasnt-c65</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/moneytool/aegis-devops" rel="noopener noreferrer"&gt;Aegis-DevOps&lt;/a&gt;, an open-source check that sits between an AI coding agent and the shell. Before Claude Code, Codex, Copilot, Cursor or Gemini CLI runs &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;terraform&lt;/code&gt; or a cloud CLI, Aegis turns the command into an intent (action, resource, namespace) and matches it against signed policy. Recently I asked three AI models to review the design for the next version. They also found four bugs in the version that was already shipped. None of them were in the policy engine. They were all in the step that decides what a command means.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I drafted this article with help from an AI assistant and reviewed it myself. The bugs, the fixes and the command output below are from the project's own changelog and from running both versions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the parser is the security boundary
&lt;/h2&gt;

&lt;p&gt;A rule like "no rollout restarts in &lt;code&gt;prod&lt;/code&gt;" is written against a normalized intent: action &lt;code&gt;rollout-restart&lt;/code&gt;, resource &lt;code&gt;deployment/*&lt;/code&gt;, namespace &lt;code&gt;prod&lt;/code&gt;. If the parser produces a different shape for the same command, the rule doesn't match. In most guards, including this one, "no rule matched" means allow.&lt;/p&gt;

&lt;p&gt;So a parser bug isn't a crash or a wrong answer you'd notice. It's a silent allow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: a flag before the subcommand
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubectl&lt;/code&gt; accepts global flags almost anywhere, and agents put them anywhere. This is what 0.1.5 made of a perfectly ordinary command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ aegis check kubectl --json -- kubectl -n prod rollout restart deploy/web

0.1.5:  ALLOW  rollout--n  prod/*
        ALLOW  rollout--n  restart/*
        ALLOW  rollout--n  deployment/web     (namespace lost)
0.2.1:  ALLOW  rollout-restart  deployment/web  ns=prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser took &lt;code&gt;-n&lt;/code&gt; as the rollout subcommand, then read everything after it as resources. The namespace was gone, so a namespace-scoped rule about rollouts in &lt;code&gt;prod&lt;/code&gt; could never match. The fix takes the subcommand before merging global options, and consumes global options that appear between &lt;code&gt;rollout&lt;/code&gt; and &lt;code&gt;restart&lt;/code&gt; too, because kubectl accepts that placement as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: impersonation was treated as noise
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--as&lt;/code&gt; changes who the API server thinks is making the request. Rules scoped to the agent's own identity stop applying server-side, which makes the client-side hook the only place that sees the swap before it happens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ aegis check kubectl --json -- kubectl --as admin rollout restart deploy/web -n prod

0.1.5:  ALLOW  rollout---as  admin/*   (and two more intents like it)
0.2.1:  ALLOW  rollout-restart  deployment/web  ns=prod
        BLOCK  impersonate  user/admin  ns=prod   [block-kubectl-impersonation]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 0.2.1, &lt;code&gt;--as&lt;/code&gt;, &lt;code&gt;--as-group&lt;/code&gt; and &lt;code&gt;--as-uid&lt;/code&gt; each become their own &lt;code&gt;impersonate&lt;/code&gt; intent, and the example policy blocks them. &lt;code&gt;kubectl rollout --as admin restart ...&lt;/code&gt; parses the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: the same object under different names
&lt;/h2&gt;

&lt;p&gt;Rules are written against one canonical name per object. The parser didn't always produce it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl cordon node1        0.1.5: node1/&lt;span class="k"&gt;*&lt;/span&gt;        0.2.1: node/node1
kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;web-0 &lt;span class="nt"&gt;-n&lt;/span&gt; prod  0.1.5: web-0          0.2.1: pod/web-0
kubectl delete sc fast      0.1.5: sc/fast        0.2.1: storageclass/fast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule for &lt;code&gt;storageclass/*&lt;/code&gt; missed &lt;code&gt;sc/fast&lt;/code&gt;. A rule for &lt;code&gt;node/*&lt;/code&gt; missed every drain and cordon.&lt;/p&gt;

&lt;p&gt;There was a fourth case in this family that failed the other way. &lt;code&gt;kubectl drain node1 --ignore-daemonsets&lt;/code&gt; was rejected as malformed (exit 65), because the parser thought the flag needed a value. The hook fails closed, so the agent got blocked instead of waved through. That's annoying, but it's the right way to be wrong. Failing closed turned a parser bug into friction instead of a hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 4: letting untrusted rules stall the pipeline
&lt;/h2&gt;

&lt;p&gt;This one isn't a parsing bug, but it's the one that worried me most, because it's the exact attack the project exists to stop.&lt;/p&gt;

&lt;p&gt;Aegis only lets a rule vote if it passes two checks: nobody edited it after it was signed, and its author was allowed to write that kind of rule. A rule that fails either check is discarded. That stops a line planted in a ticket or a Slack thread from becoming policy.&lt;/p&gt;

&lt;p&gt;But there was a path that ran before those checks. When the environment of an action couldn't be resolved, every matching rule forced ESCALATE. So an unauthorized rule scoped to &lt;code&gt;env: prod&lt;/code&gt; could stall any action whose environment was unknown. That's a denial of service on the guardrail, from exactly the kind of rule it's meant to ignore. In 0.2.1 those rules are discarded on that path too, and reported in &lt;code&gt;discarded[]&lt;/code&gt; like everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone writing a command guard
&lt;/h2&gt;

&lt;p&gt;These apply whether you're writing hooks for Claude Code, a wrapper script, or an admission policy.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Normalize before you match.&lt;/strong&gt; Write rules against one canonical form, and test that every spelling (aliases, short kinds, plural kinds, bare names) reaches it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test flag placement, not just flags.&lt;/strong&gt; Generate the same command with global options before the verb, between the verb and its subcommand, and at the end, and assert that the intent is identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags that change identity or target are part of the intent.&lt;/strong&gt; &lt;code&gt;--as&lt;/code&gt;, &lt;code&gt;--context&lt;/code&gt;, &lt;code&gt;--kubeconfig&lt;/code&gt;, &lt;code&gt;-n&lt;/code&gt;. Never file them under "options we can ignore".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed on anything you don't fully understand.&lt;/strong&gt; An unknown option should block, not fall through to "no rule matched".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get someone else to read the design.&lt;/strong&gt; None of these came from my tests. They came from reviewers reading the code while checking a design for something else.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The fixes are in 0.2.1 and later (0.3.2 is current on PyPI). If you wrote rules against the old forms (&lt;code&gt;node1/*&lt;/code&gt;, bare pod names, short kinds), update them. &lt;a href="https://github.com/moneytool/aegis-devops/blob/main/docs/constraints.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/constraints.md&lt;/code&gt;&lt;/a&gt; has the normal forms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;aegis-devops &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; aegis init .aegis

&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude plugin marketplace add moneytool/aegis-devops
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;aegis-devops@aegis-devops

&lt;span class="c"&gt;# Gemini CLI&lt;/span&gt;
gemini extensions &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/moneytool/aegis-devops

&lt;span class="c"&gt;# Codex, Copilot, VS Code, Cursor, OpenCode&lt;/span&gt;
aegis &lt;span class="nb"&gt;install &lt;/span&gt;codex   &lt;span class="c"&gt;# or copilot | vscode | cursor | opencode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The follow-up, on enforcing the same policy in AWS itself: &lt;a href="https://dev.to/moneytool/my-agent-guardrail-only-lived-on-the-laptop-so-i-compiled-it-into-aws-j1d"&gt;My agent guardrail only lived on the laptop. So I compiled it into AWS.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've found a command shape that slips past your own agent guard, I'd like to hear it. Issues are open at &lt;a href="https://github.com/moneytool/aegis-devops" rel="noopener noreferrer"&gt;github.com/moneytool/aegis-devops&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>My agent guardrail only lived on the laptop. So I compiled it into AWS.</title>
      <dc:creator>Vamshidher Reddy Jannapu Reddy</dc:creator>
      <pubDate>Thu, 01 Oct 2026 19:08:30 +0000</pubDate>
      <link>https://dev.to/moneytool/my-agent-guardrail-only-lived-on-the-laptop-so-i-compiled-it-into-aws-j1d</link>
      <guid>https://dev.to/moneytool/my-agent-guardrail-only-lived-on-the-laptop-so-i-compiled-it-into-aws-j1d</guid>
      <description>&lt;p&gt;For the past few weeks I've been building &lt;a href="https://github.com/moneytool/aegis-devops" rel="noopener noreferrer"&gt;Aegis-DevOps&lt;/a&gt;, an open-source check that sits in front of the shell commands an AI coding agent runs. Before Claude Code, Codex, Copilot, Cursor or Gemini CLI runs &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;terraform&lt;/code&gt; or &lt;code&gt;aws&lt;/code&gt;, a hook hands the command to Aegis, and Aegis blocks it if your signed policy says no. It works. It also has a hole I knew about from the start, and version 0.3.0 is my first attempt at closing it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I drafted this article with help from an AI assistant and reviewed it myself. The commands and output below are from running aegis-devops 0.3.0 against its example policy.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hole: a hook only sees commands
&lt;/h2&gt;

&lt;p&gt;A pre-execution hook sees the command string the agent asks its shell tool to run. It doesn't see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Python script the agent writes and runs, which calls &lt;code&gt;boto3&lt;/code&gt; directly;&lt;/li&gt;
&lt;li&gt;an SDK call from code the agent is "just testing";&lt;/li&gt;
&lt;li&gt;a credential the agent found in a file and used from somewhere else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all three cases the destructive call reaches the AWS API without ever looking like &lt;code&gt;aws s3 rb&lt;/code&gt;. Blocking on the client is the right first layer, because it's fast and it can explain itself to the model. It isn't the last layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: let AWS refuse the call
&lt;/h2&gt;

&lt;p&gt;AWS already has a control that sits above everything an identity inside an account can do: a &lt;strong&gt;Service Control Policy&lt;/strong&gt;. An SCP attached to an account in an AWS Organization caps what any role in that account may do, and an agent with IAM rights inside the account can't detach it.&lt;/p&gt;

&lt;p&gt;So 0.3.0 adds a compiler. It takes the same policy the hook enforces and writes it out as SCPs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ aegis compile aws --account 111122223333 --out build/aegis-aws
aegis: wrote 1 SCP(s) for account 111122223333 to build/aegis-aws
       (exact=2, not-applicable=27, partial=1)
aegis: agents.yaml is report-only: the SCPs are under report-only/ and are not for attaching;
       review coverage.md, then set enforcement: enforce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule "never delete an S3 bucket or an RDS database" becomes an explicit &lt;code&gt;Deny&lt;/code&gt;:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&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="s2"&gt;"rds:DeleteDBInstance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rds:DeleteDBCluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rds:DeleteDBSnapshot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="s2"&gt;"rds:DeleteDBClusterSnapshot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:DeleteBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:DeleteObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="s2"&gt;"s3:DeleteObjectVersion"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&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="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&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="nl"&gt;"ArnNotLike"&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="nl"&gt;"aws:PrincipalArn"&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="s2"&gt;"arn:aws:iam::111122223333:role/BreakGlass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                                         &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::111122223333:role/Deploy"&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="nl"&gt;"StringNotEqualsIfExists"&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="nl"&gt;"aws:SourceIdentity"&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="s2"&gt;"alice@example.com"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it doesn't matter whether the agent typed &lt;code&gt;aws s3 rb&lt;/code&gt;, wrote a boto3 script, or found a key. If the call is made as an agent identity, AWS refuses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who counts as an agent?
&lt;/h2&gt;

&lt;p&gt;An API call carries an identity, not a hint about who was typing. So the compiler needs an identity model, in a signed &lt;code&gt;agents.yaml&lt;/code&gt;. The default is &lt;strong&gt;deny-by-default&lt;/strong&gt;: you list the identities you trust (people, your deploy role, CI bound to a specific workflow), plus at least one break-glass role, and everything else is treated as an agent.&lt;/p&gt;

&lt;p&gt;I went with deny-by-default because the other way round, listing the agents, fails open. Forget one, or let an agent mint a new role, and the new one isn't restricted.&lt;/p&gt;

&lt;h2&gt;
  
  
  An agent will try to become trusted
&lt;/h2&gt;

&lt;p&gt;If the only thing between an agent and &lt;code&gt;DeleteDBInstance&lt;/code&gt; is "is this call from the Deploy role?", the obvious move is to become the Deploy role. So every compile also adds statements that stop exactly that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no &lt;code&gt;sts:AssumeRole&lt;/code&gt; into a trusted or break-glass role, and no &lt;code&gt;iam:PassRole&lt;/code&gt; of one;&lt;/li&gt;
&lt;li&gt;no edits to a trusted role's trust policy or permissions, and no new credentials for a trusted user;&lt;/li&gt;
&lt;li&gt;no setting an exempt source identity on a session;&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;organizations:LeaveOrganization&lt;/code&gt;, since an account that leaves the organization sheds its SCPs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part I care about most: saying what it doesn't cover
&lt;/h2&gt;

&lt;p&gt;A guardrail that looks stronger than it is does more damage than no guardrail, because people stop checking. So every compile writes a coverage report, rule by rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### block-s3-bucket-delete: exact, mapping verified
- enforced: s3/bucket delete (s3:DeleteBucket, s3:DeleteObject, s3:DeleteObjectVersion)
- same effect, not covered: s3:PutLifecycleConfiguration: an expiration rule deletes every object
- same effect, not covered: s3:PutBucketPolicy: a deny-all bucket policy locks everyone out

### block-rds-delete: partial, mapping verified
- not enforced: 'rds/*' also matches resource types the action map does not know
- same effect, not covered: rds:ModifyDBInstance: BackupRetentionPeriod=0 deletes the automated backups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Same effect, not covered" is the list of other ways to reach the same outcome that the rule, as written, doesn't block. You can widen the rule, or accept the gap knowingly.&lt;/p&gt;

&lt;p&gt;A few more choices along the same lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Report-only first.&lt;/strong&gt; The first compile writes SCPs into &lt;code&gt;report-only/&lt;/code&gt; and tells you not to attach them. You switch &lt;code&gt;agents.yaml&lt;/code&gt; to &lt;code&gt;enforce&lt;/code&gt; (a signed change) once you've reviewed who would be restricted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aegis never holds cloud write credentials.&lt;/strong&gt; It writes JSON, and you apply it with your own infrastructure as code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unverified rules never reach a compiled policy.&lt;/strong&gt; The compiler starts from a verified snapshot, so a rule that was tampered with, forged, or written by someone not allowed to write it gets no vote, the same as on the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every action mapping was tested&lt;/strong&gt; in a sandbox AWS Organization, with live calls and the IAM policy simulator. A mapping added later starts out marked unverified.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's still missing
&lt;/h2&gt;

&lt;p&gt;It's a preview. AWS is in the 0.3.0 release; a Kubernetes target (ValidatingAdmissionPolicies scoped to agent identities) shipped as a preview in 0.3.1, and GCP and Azure are designed but not built. SCPs don't apply to an organization's management account, so agents shouldn't run there at all. Rules with a time window or a rate limit stay client-side, because an SCP can't express them. And it's a young project: I'd much rather hear "this mapping is wrong" now than after someone relies on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;aegis-devops
aegis init .aegis
aegis compile aws &lt;span class="nt"&gt;--account&lt;/span&gt; &amp;lt;your-account-id&amp;gt; &lt;span class="nt"&gt;--out&lt;/span&gt; build/aegis-aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client hook installs in one command for each agent (&lt;code&gt;aegis install claude|codex|copilot|cursor|gemini|opencode&lt;/code&gt;), and there's a &lt;a href="https://github.com/marketplace/actions/aegis-devops-plan-check" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; for Terraform and OpenTofu plans. Details on the compiler are in &lt;a href="https://github.com/moneytool/aegis-devops/blob/main/docs/server-side.md" rel="noopener noreferrer"&gt;docs/server-side.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run agents with real AWS access, I'd like to know how you scope them today. Issues are open at &lt;a href="https://github.com/moneytool/aegis-devops" rel="noopener noreferrer"&gt;github.com/moneytool/aegis-devops&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Deleting your AWS Copilot stacks can delete your database. Here's why.</title>
      <dc:creator>Vamshidher Reddy Jannapu Reddy</dc:creator>
      <pubDate>Thu, 01 Oct 2026 02:51:27 +0000</pubDate>
      <link>https://dev.to/moneytool/deleting-your-aws-copilot-stacks-can-delete-your-database-heres-why-pen</link>
      <guid>https://dev.to/moneytool/deleting-your-aws-copilot-stacks-can-delete-your-database-heres-why-pen</guid>
      <description>&lt;p&gt;AWS ended support for the Copilot CLI on June 12, 2026 and archived the repository ten days later. If you deployed ECS services with Copilot, they keep running: Copilot was only ever a generator of CloudFormation stacks.&lt;/p&gt;

&lt;p&gt;The trouble starts when you try to leave. Most teams want those services in Terraform, and the obvious plan is "recreate it in Terraform, then delete the Copilot stacks." Done naively, that plan deletes production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four traps
&lt;/h2&gt;

&lt;p&gt;I read through Copilot's source to map exactly what happens when its stacks are deleted. Four things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Custom-resource Delete handlers.&lt;/strong&gt; Copilot ships 13 Lambda-backed custom resources, and 9 of them do something destructive on Delete. When the environment stack goes, they:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delete the ACM certificate and its DNS validation records,&lt;/li&gt;
&lt;li&gt;delete every Route 53 alias record for your custom domains,&lt;/li&gt;
&lt;li&gt;delete the NS delegation record in the app's hosted zone,&lt;/li&gt;
&lt;li&gt;empty the ELB access-logs bucket, every object version included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Importing those resources into Terraform first doesn't save you: the Lambda doesn't care who manages the certificate now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The env-controller.&lt;/strong&gt; Each service stack carries an &lt;code&gt;EnvControllerAction&lt;/code&gt; custom resource. When the last service that needs a shared ALB, NAT gateways or EFS is deleted, the env-controller updates the environment stack and removes them. Delete your last service stack and your load balancer and file system go with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Unprotected addons.&lt;/strong&gt; Aurora, DynamoDB and S3 addons live in a nested stack with no &lt;code&gt;DeletionPolicy&lt;/code&gt;. Delete the parent and CloudFormation deletes your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;--retain-resources&lt;/code&gt; doesn't help.&lt;/strong&gt; The flag everyone reaches for only works on stacks already in &lt;code&gt;DELETE_FAILED&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adopt in place instead of rebuilding
&lt;/h2&gt;

&lt;p&gt;The safe sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put &lt;code&gt;DeletionPolicy: Retain&lt;/code&gt; and &lt;code&gt;UpdateReplacePolicy: Retain&lt;/code&gt; on &lt;strong&gt;every&lt;/strong&gt; resource in &lt;strong&gt;every&lt;/strong&gt; stack, nested stacks and the StackSet included. Retain on a &lt;code&gt;Custom::*&lt;/code&gt; resource also stops CloudFormation from invoking its Delete handler.&lt;/li&gt;
&lt;li&gt;Import every resource into Terraform with the exact values it runs with today, so the first plan is import-only.&lt;/li&gt;
&lt;li&gt;Delete the Copilot stacks. CloudFormation forgets the resources; nothing is deleted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Doing that by hand across dozens of resources is error-prone, so I built a tool for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  ecsodus
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/moneytool/ecsodus" rel="noopener noreferrer"&gt;ecsodus&lt;/a&gt; is an open-source CLI (Apache-2.0) that does exactly that sequence and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx ecsodus inventory &lt;span class="nt"&gt;--app&lt;/span&gt; myapp &lt;span class="nt"&gt;-o&lt;/span&gt; inventory.json   &lt;span class="c"&gt;# read-only discovery&lt;/span&gt;
uvx ecsodus report    inventory.json                  &lt;span class="c"&gt;# REPORT.md: what's safe, what's blocked, why&lt;/span&gt;
uvx ecsodus generate  inventory.json &lt;span class="nt"&gt;--out&lt;/span&gt; infra/     &lt;span class="c"&gt;# Terraform + retain patches + RUNBOOK.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmoneytool%2Fecsodus%2Fmain%2Fdocs%2Fassets%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmoneytool%2Fecsodus%2Fmain%2Fdocs%2Fassets%2Fdemo.gif" alt="ecsodus demo" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The design choices that matter for a tool like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only by construction.&lt;/strong&gt; Every AWS client is wrapped in a guard that refuses anything but &lt;code&gt;Describe&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt;, &lt;code&gt;Get&lt;/code&gt; and &lt;code&gt;Lookup&lt;/code&gt;. ecsodus never applies Terraform, deletes anything or moves traffic. You run the runbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact imports.&lt;/strong&gt; Each resource becomes a flat &lt;code&gt;aws_*&lt;/code&gt; block built from literal deployed values, and &lt;code&gt;ecsodus check --phase import&lt;/code&gt; rejects any plan with an update, create, delete or replacement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy-only change sets.&lt;/strong&gt; The retain patch edits the deployed template line by line, and &lt;code&gt;check --changeset&lt;/code&gt; accepts a change set only if it changes deletion policies and nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never split a stack.&lt;/strong&gt; If anything in a stack is unsupported, the whole stack (and everything that depends on it) stays on Copilot, and the report says why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An honest baseline.&lt;/strong&gt; Every report starts with the zero-risk option: keep the CloudFormation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Does it work?
&lt;/h2&gt;

&lt;p&gt;On September 30 I ran it end to end against a real Copilot v1.34.1 app (an environment, a Load Balanced Web Service, DynamoDB and S3 addons; 5 stacks, 49 resources) in a sandbox account:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;44 of 44 resources imported, 0 changed, 0 destroyed.&lt;/li&gt;
&lt;li&gt;Every Copilot stack and the StackSet deleted.&lt;/li&gt;
&lt;li&gt;The service kept returning HTTP 200, and the sentinel data in DynamoDB and S3 survived.&lt;/li&gt;
&lt;li&gt;The env-controller and rule-priority Lambdas were never invoked on teardown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full report is in the repo: &lt;a href="https://github.com/moneytool/ecsodus/blob/main/docs/e2e/2026-09-30-aws-e2e.md" rel="noopener noreferrer"&gt;docs/e2e/2026-09-30-aws-e2e.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do yet
&lt;/h2&gt;

&lt;p&gt;It's alpha. v0.1 supports Load Balanced Web Services and Backend Services, their environment, and Aurora/RDS, DynamoDB and S3 addons. Worker Services, Scheduled Jobs, Request-Driven Web Services, Static Sites, NLB, CloudFront, sidecars and pipelines are detected and reported as blocked. App Runner is next.&lt;/p&gt;

&lt;p&gt;If you're still running something on Copilot, I'd like to hear what it is. Run &lt;code&gt;ecsodus inventory&lt;/code&gt; and &lt;code&gt;ecsodus report&lt;/code&gt; (both read-only) and open an issue with what's blocked: that's how the roadmap gets set. And if the tool saves you a bad afternoon, a star on &lt;a href="https://github.com/moneytool/ecsodus" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other Copilot users find it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
