<?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: Josh van Leeuwen</title>
    <description>The latest articles on DEV Community by Josh van Leeuwen (@joshvanl).</description>
    <link>https://dev.to/joshvanl</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%2F4177132%2Fc577d880-4488-440c-a455-18a7780718ea.jpg</url>
      <title>DEV Community: Josh van Leeuwen</title>
      <link>https://dev.to/joshvanl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joshvanl"/>
    <language>en</language>
    <item>
      <title>How Do I Stop Prompt Injection From Reaching My Tools?</title>
      <dc:creator>Josh van Leeuwen</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:48:58 +0000</pubDate>
      <link>https://dev.to/diagrid/how-do-i-stop-prompt-injection-from-reaching-my-tools-2hik</link>
      <guid>https://dev.to/diagrid/how-do-i-stop-prompt-injection-from-reaching-my-tools-2hik</guid>
      <description>&lt;p&gt;You can't stop untrusted text from reaching a model. If your agent reads public GitHub issues, anyone can put instructions into the text it reads. The model might follow them, even when you've told it not to.&lt;/p&gt;

&lt;p&gt;What you can control is what happens afterwards. Which tools the agent can call, what arguments they accept, and whether the model gets to make the decision to execute them at all.&lt;/p&gt;

&lt;p&gt;Take an agent that reads GitHub issues and applies labels. It needs to understand the issue, decide on a label and update GitHub. It doesn't need permission to close issues, merge pull requests or read repository secrets.&lt;/p&gt;

&lt;p&gt;Here's how I'd build this with Dapr.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the agent inside a workflow
&lt;/h2&gt;

&lt;p&gt;I'd use a Dapr Workflow to manage the issue triage process, rather than letting the model decide which actions to take.&lt;/p&gt;

&lt;p&gt;The workflow receives a verified GitHub webhook, passes the issue text to an agent and gets back a suggested label. It then checks that label before calling GitHub.&lt;/p&gt;

&lt;p&gt;The important part is what the agent doesn't control. It doesn't choose the repository, issue number or which GitHub operation to run. Those come from the workflow and the original verified event.&lt;/p&gt;

&lt;p&gt;The model is deciding how to classify the issue, not what it's allowed to do with GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat MCP tools as workflows too
&lt;/h2&gt;

&lt;p&gt;Dapr has an &lt;code&gt;MCPServer&lt;/code&gt; resource which lets you declare a connection to an MCP server. When Dapr loads it, it discovers the available tools and registers a durable workflow for each one.&lt;/p&gt;

&lt;p&gt;So if our GitHub MCP server exposes &lt;code&gt;add_label&lt;/code&gt; and &lt;code&gt;close_issue&lt;/code&gt;, Dapr registers workflows named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dapr.internal.mcp.github.CallTool.add_label
dapr.internal.mcp.github.CallTool.close_issue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent doesn't need to manage the MCP connection itself. The application can invoke those workflows, and Dapr handles the MCP call.&lt;/p&gt;

&lt;p&gt;For example, the resource on our &lt;code&gt;github-tools&lt;/code&gt; application could look 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;dapr.io/v1alpha1&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;MCPServer&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;github&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;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;streamableHTTP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github-mcp.internal/mcp&lt;/span&gt;
&lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;github-tools&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL and tool names here are examples; you'd use those exposed by your own MCP server.&lt;/p&gt;

&lt;p&gt;Now we need to decide which applications can invoke those tool workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce permissions outside the model
&lt;/h2&gt;

&lt;p&gt;A list of permitted tools in a system prompt isn't a security control. The model can be convinced to ignore it.&lt;/p&gt;

&lt;p&gt;With Dapr, you can use a &lt;code&gt;WorkflowAccessPolicy&lt;/code&gt; to control which App IDs are allowed to schedule particular workflows on another application.&lt;/p&gt;

&lt;p&gt;For our example:&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;dapr.io/v1alpha1&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;WorkflowAccessPolicy&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;github-tools-policy&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;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;github-tools&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;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;callers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;triage-workflow&lt;/span&gt;
      &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&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;dapr.internal.mcp.github.CallTool.add_label&lt;/span&gt;
          &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows &lt;code&gt;triage-workflow&lt;/code&gt; to invoke &lt;code&gt;add_label&lt;/code&gt; on &lt;code&gt;github-tools&lt;/code&gt;. It doesn't grant access to &lt;code&gt;close_issue&lt;/code&gt;, or any of the other tools the MCP server might expose.&lt;/p&gt;

&lt;p&gt;The workflow access policy is enforced by Dapr, using the calling application's verified identity. The model doesn't get to override it.&lt;/p&gt;

&lt;p&gt;There are two details worth knowing here. You need a policy loaded for deny-by-default behaviour, and the MCP server workflows need to run under a separate App ID. Dapr always allows an application to schedule its own workflows, so you shouldn't rely on this policy to isolate tools running under the same App ID as the caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the arguments before calling GitHub
&lt;/h2&gt;

&lt;p&gt;Being allowed to call &lt;code&gt;add_label&lt;/code&gt; doesn't mean every argument should be accepted.&lt;/p&gt;

&lt;p&gt;A poisoned issue could convince the model to suggest a label like &lt;code&gt;security-approved&lt;/code&gt;, or try to make it update a different issue.&lt;/p&gt;

&lt;p&gt;I'd have the agent return a label, then check it in the workflow before calling GitHub. Something like this, using the Dapr Python Workflow SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dapr.ext.workflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;

&lt;span class="n"&gt;wfr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WorkflowRuntime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;ALLOWED_LABELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs-triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@wfr.workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triage_issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage_issue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DaprWorkflowContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# classify_issue is a registered activity that calls the model
&lt;/span&gt;    &lt;span class="n"&gt;suggestion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classify_issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_LABELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_child_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dapr.internal.mcp.github.CallTool.add_label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github-tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the core of the workflow, with the activity implementation and runtime startup left out. The tool arguments will depend on your MCP server.&lt;/p&gt;

&lt;p&gt;Notice that the model only supplies the label. The repository and issue number come from the verified webhook that started the workflow.&lt;/p&gt;

&lt;p&gt;I'd also validate arguments where the tool runs. Dapr's &lt;code&gt;MCPServer&lt;/code&gt; resource supports &lt;code&gt;beforeCallTool&lt;/code&gt; hooks, which are themselves workflows. You can use one to reject labels outside the allow-list before the call reaches the MCP server.&lt;/p&gt;

&lt;p&gt;That way the check isn't only happening in the application that called the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a separate workflow for anything destructive
&lt;/h2&gt;

&lt;p&gt;Adding a label is one thing. Closing an issue, deleting something or deploying a release is another.&lt;/p&gt;

&lt;p&gt;For those operations, I'd use a separate workflow that waits for human approval.&lt;/p&gt;

&lt;p&gt;Dapr Workflows can wait for an external event, so the workflow can request approval, stop executing and resume when an authorised user responds. There's no need to keep a process running while someone makes the decision.&lt;/p&gt;

&lt;p&gt;The approval needs to be verified by your application, not accepted as something the model says it received.&lt;/p&gt;

&lt;p&gt;You can then give the review workflow its own App ID and grant it access to &lt;code&gt;close_issue&lt;/code&gt;, without giving that permission to the triage workflow.&lt;/p&gt;

&lt;p&gt;The same applies to any other tool where a bad decision is difficult to undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't fix
&lt;/h2&gt;

&lt;p&gt;None of this stops prompt injection from influencing the model. A malicious GitHub issue might still persuade it to suggest the wrong label.&lt;/p&gt;

&lt;p&gt;But the model doesn't have permission to close the issue, the workflow won't accept a label outside the allowed set, and the repository and issue number aren't under the model's control.&lt;/p&gt;

&lt;p&gt;That's a much smaller problem than giving an agent access to every tool on the MCP server and relying on a system prompt to keep it in line.&lt;/p&gt;

&lt;p&gt;You still need to think about prompt injection. You just shouldn't be relying on the model to enforce the limits of what it's allowed to do.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Dapr documentation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.dapr.io/developing-ai/mcp/mcp-server-resource/" rel="noopener noreferrer"&gt;MCPServer resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dapr.io/operations/security/workflow-access-policy/" rel="noopener noreferrer"&gt;Workflow access policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-multi-app/" rel="noopener noreferrer"&gt;Multi-application workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-patterns/" rel="noopener noreferrer"&gt;Workflow patterns and external events&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>dapr</category>
      <category>security</category>
    </item>
    <item>
      <title>Agents Are For-Loops. That Is Good News for Security.</title>
      <dc:creator>Josh van Leeuwen</dc:creator>
      <pubDate>Tue, 13 Oct 2026 09:42:17 +0000</pubDate>
      <link>https://dev.to/diagrid/agents-are-for-loops-that-is-good-news-for-security-598</link>
      <guid>https://dev.to/diagrid/agents-are-for-loops-that-is-good-news-for-security-598</guid>
      <description>&lt;p&gt;&lt;em&gt;If agents were magic, we couldn’t secure them. They aren’t, so we can.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgz928gfegxbfpt5dtbeh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgz928gfegxbfpt5dtbeh.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI agents are not magic. They are workflows with probabilistic decision-making and real-world privileges. Strip away the framing and you find the classical operator feedback loop you already know from Kubernetes controllers and embedded thermostats: observe, decide, act, repeat. The only new element is that the deciding step is a language model.&lt;/p&gt;

&lt;p&gt;This is good news, because it means twenty years of distributed-systems security applies almost unchanged. The model is the part you cannot trust; everything around it is the part you already know how to lock down.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLMs don’t act. Tools do.
&lt;/h2&gt;

&lt;p&gt;A model emits instructions. It’s the tools in the acting step that translate the model’s intent into real-world behaviour- the database write, the refund, the deployment. Which means the security boundary is not the prompt and never was. It’s the tool layer, and tool access is an authorisation problem we have mature answers for: each agent gets its own verifiable identity, each identity gets explicitly scoped grants, and everything ungrated is denied by default.&lt;/p&gt;

&lt;p&gt;Note what this replaces. The common pattern today is one shared API key in an environment variable, used by every agent, with the real policy written in the system prompt as a polite request. A permission that exists only as an instruction to a model is not a permission. And a gateway in front of the tools helps less than it appears: a gateway secures a path, it cannot secure a workload it does not run. If the agent can reach the backing service directly, the gateway is decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a secured for-loop looks like
&lt;/h2&gt;

&lt;p&gt;Identity issued per agent, short-lived and cryptographically verifiable (SPIFFE gives you the standard). Authorisation scoped per tool, per agent, enforced in the runtime. Deny-all as the starting posture, grants as deliberate exceptions. And a signed execution history, so “what did the agent do” has an answer that survives an audit rather than a log file that anyone could have edited.&lt;/p&gt;

&lt;p&gt;There is still a weakness in this system: prompt injection against a tool the agent is legitimately granted. If the agent is allowed to send email and a poisoned document convinces the model to send the wrong one, identity and authorisation did their job and the damage still happened. No infrastructure makes a model trustworthy. What infrastructure can do is keep the set of things a misbehaving model can reach small- which turns a catastrophe into an incident.&lt;/p&gt;

&lt;p&gt;The deeper treatments: &lt;a href="https://www.diagrid.io/blog/agent-identity-the-foundational-layer-that-ai-is-still-missing" rel="noopener noreferrer"&gt;Agent Identity: The Foundational Layer AI Is Still Missing&lt;/a&gt; and &lt;a href="https://www.diagrid.io/blog/why-mcp-gateways-are-not-enough" rel="noopener noreferrer"&gt;Why MCP Gateways Are Not Enough&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>identity</category>
      <category>zerotrust</category>
    </item>
    <item>
      <title>SPIFFE in Ten Minutes, for People Securing Agents</title>
      <dc:creator>Josh van Leeuwen</dc:creator>
      <pubDate>Sun, 11 Oct 2026 14:01:59 +0000</pubDate>
      <link>https://dev.to/joshvanl/spiffe-in-ten-minutes-for-people-securing-agents-k4h</link>
      <guid>https://dev.to/joshvanl/spiffe-in-ten-minutes-for-people-securing-agents-k4h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmfiqe9ol0ie8hyrg4mf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxmfiqe9ol0ie8hyrg4mf.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every serious answer to "how should agents authenticate?" eventually lands on SPIFFE, so here is the working minimum.&lt;/p&gt;

&lt;p&gt;SPIFFE gives every workload a verifiable identity document (an SVID) tied to a URI like spiffe://trust-domain/ns/prod/agent/invoice-reviewer. The document is short-lived, automatically rotated, and cryptographically verifiable against the trust domain's bundle. No secret to leak, no key to rotate by hand, no shared credential to make agents indistinguishable from each other.&lt;/p&gt;

&lt;p&gt;Three properties matter for agents specifically. First, identity per workload, not per team or per environment; so "which agent did this" has an answer. Second, mutual verification: the tool verifies the agent and the agent verifies the tool, which kills a whole class of spoofing. Third, federation Identities issued in your cluster can be trusted by AWS or Azure without copying secrets across the boundary.&lt;/p&gt;

&lt;p&gt;What SPIFFE does not give you: authorisation. An SVID says who is calling; it says nothing about what they may do. Policy is a separate layer, and identity without authorisation is meaningless. Authorisation without identity is impossible, which is why this comes first.&lt;/p&gt;

&lt;p&gt;I spent seven years on cert-manager watching teams hand-roll worse versions of this with long-lived certificates and spreadsheet rotation schedules. The standard exists, so use it.&lt;/p&gt;

&lt;p&gt;How Dapr builds universal application identity on SPIFFE, including the cloud federation: &lt;a href="https://www.diagrid.io/blog/universal-application-identity-with-dapr" rel="noopener noreferrer"&gt;https://www.diagrid.io/blog/universal-application-identity-with-dapr&lt;/a&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>cybersecurity</category>
      <category>infrastructure</category>
      <category>security</category>
    </item>
  </channel>
</rss>
