<?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: Tom Elias</title>
    <description>The latest articles on DEV Community by Tom Elias (@tomelias10).</description>
    <link>https://dev.to/tomelias10</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%2F4152894%2F1f08fd7b-d293-4b92-8ad1-fe76736e17f6.png</url>
      <title>DEV Community: Tom Elias</title>
      <link>https://dev.to/tomelias10</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tomelias10"/>
    <language>en</language>
    <item>
      <title>They deleted the leaked secret. The attacker used it anyway.</title>
      <dc:creator>Tom Elias</dc:creator>
      <pubDate>Wed, 30 Sep 2026 18:16:36 +0000</pubDate>
      <link>https://dev.to/tomelias10/they-deleted-the-leaked-secret-the-attacker-used-it-anyway-17an</link>
      <guid>https://dev.to/tomelias10/they-deleted-the-leaked-secret-the-attacker-used-it-anyway-17an</guid>
      <description>&lt;p&gt;&lt;em&gt;The Anatomy of Non-Human &amp;amp; Agent Access Drift in 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most access reviews still start with a list of people. In 2026 that list covers the smallest part of the problem.&lt;/p&gt;

&lt;p&gt;Every CI pipeline, service principal, API key, and AI agent is an identity with access to something. Unlike people, they don't leave the company, they don't get offboarded, and nobody notices when what they can reach quietly grows. That growth is access drift, and two incidents from the last week of September show exactly what it looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: A secret that outlived its deletion
&lt;/h2&gt;

&lt;p&gt;On 25 September Microsoft described an actor it tracks as Storm-3168 (also called JADEPUFFER). The attacker used two compromised Azure service principals. One spent about 15.5 hours on reconnaissance. The other made more than 100 attempts to delete storage accounts in about seven minutes, then pulled keys.&lt;/p&gt;

&lt;p&gt;The likely entry point wasn't an exploit. It was a secret posted in a public GitHub issue. Someone deleted the text, but it stayed in the issue's edit history, and it was never revoked.&lt;/p&gt;

&lt;p&gt;That's the first kind of drift: &lt;strong&gt;an identity whose credential nobody considers live anymore, but which still is.&lt;/strong&gt; It has delete rights, and no human owns it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: A client that trusts the wrong server
&lt;/h2&gt;

&lt;p&gt;On 28 September, GHSA-qx49-fqc8-xw99 was published for the official MCP Python SDK, based on research by Cycode. A malicious MCP server responds with a 404, the SDK falls back to a discovery path that never validates the issuer, and the client sends its client secret, authorization code, and PKCE verifier to the attacker.&lt;/p&gt;

&lt;p&gt;It's fixed in 1.30.0 and 2.2.0. For the server-to-server auth providers, where no human is in the loop, you also have to pass &lt;code&gt;issuer=&lt;/code&gt; explicitly. Upgrading alone isn't enough.&lt;/p&gt;

&lt;p&gt;That's the second kind of drift: &lt;strong&gt;an agent's tooling changes, or trusts something new, without anyone re-approving it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: The document that says none of this can happen
&lt;/h2&gt;

&lt;p&gt;The third link isn't a CVE. It's the customer security questionnaire. "Do you review machine credentials?" "Do you control third-party integrations?" Those answers are often written from memory by someone who has never seen the service principal list or the MCP config.&lt;/p&gt;

&lt;p&gt;Put the three together and you get a chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secret that should be dead
  → MCP client that hands secrets to whoever asks
  → agent with wide scopes that trusts both
  → production
  + a signed document saying it's all controlled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What drift looks like in config
&lt;/h2&gt;

&lt;p&gt;Illustrative examples of the patterns involved, not from any real org.&lt;/p&gt;

&lt;p&gt;A CI workflow with more power than it needs:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request_target&lt;/span&gt;        &lt;span class="c1"&gt;# runs with repo secrets on external PRs&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write-all&lt;/span&gt;         &lt;span class="c1"&gt;# every scope, not the one it needs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An MCP client config that grew over time:&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;"mcpServers"&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;"filesystem"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-fs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"github"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"GITHUB_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ghp_..."&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="nl"&gt;"installer"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quickstart-mcp"&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;Three problems in eight lines: filesystem access rooted at &lt;code&gt;/&lt;/code&gt;, a token pasted inline, and an unpinned package that runs whatever version is published today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking it locally
&lt;/h2&gt;

&lt;p&gt;I released three small Go CLIs that check each link. They're read-only, run offline against local files, and send no telemetry. The repo ships with testdata so you can see what they report before pointing them at your own config.&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/tomelias10/orynval-labs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;orynval-labs
make build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;nhi-ghost&lt;/strong&gt; looks for over-privileged, secret-bearing, or stale machine identities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/nhi-ghost internal/nhi/testdata
&lt;span class="c"&gt;# nhi-ghost 0.1.0&lt;/span&gt;
&lt;span class="c"&gt;# 7 findings: 4 high, 1 medium, 1 low, 1 info&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the testdata it flags a &lt;code&gt;write-all&lt;/code&gt; + &lt;code&gt;pull_request_target&lt;/code&gt; workflow, committed secrets (masked as &lt;code&gt;sk_l****&lt;/code&gt;), a wildcard IAM policy, and a disabled service account that's still present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mcp-drift&lt;/strong&gt; compares MCP client configs against an approved baseline (&lt;code&gt;.orynval/mcp-baseline.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/mcp-drift internal/mcp/testdata
&lt;span class="c"&gt;# mcp-drift 0.1.0&lt;/span&gt;
&lt;span class="c"&gt;# 7 findings: 2 high, 2 medium, 3 info&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It flags the &lt;code&gt;/&lt;/code&gt;-scoped filesystem server, the inline &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; (masked), a server whose definition hash no longer matches its approval, the unpinned &lt;code&gt;npx -y&lt;/code&gt; installer, and an unapproved remote endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;trust-proof&lt;/strong&gt; answers a questionnaire only from evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/trust-proof &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--questions&lt;/span&gt; internal/trustproof/testdata/questions.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--evidence&lt;/span&gt;  internal/trustproof/testdata/evidence
&lt;span class="c"&gt;# trust-proof — DRAFT (human review required; no claims auto-certified)&lt;/span&gt;
&lt;span class="c"&gt;# Questions: 5   Answered: 3   Gaps: 2   Coverage: 60%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every answer cites a file and line. Two questions come back &lt;code&gt;UNKNOWN&lt;/code&gt; because nothing in the evidence supports them. A token found inside an evidence file is masked as &lt;code&gt;ghp_****&lt;/code&gt; instead of being copied into the answer.&lt;/p&gt;

&lt;p&gt;All three tools support terminal, JSON, and SARIF 2.1.0 output (&lt;code&gt;--format json|sarif&lt;/code&gt;), so they drop into CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;List every service principal and API key with write or delete rights, and give each one a named human owner.&lt;/li&gt;
&lt;li&gt;Rotate any secret that has ever appeared in an issue, PR, or chat, even if it was deleted.&lt;/li&gt;
&lt;li&gt;Upgrade the MCP Python SDK to 1.30.0 or 2.2.0, and set &lt;code&gt;issuer=&lt;/code&gt; on server-to-server providers.&lt;/li&gt;
&lt;li&gt;Pin MCP server versions and re-approve them when the definition changes.&lt;/li&gt;
&lt;li&gt;Before signing the next questionnaire, make every "yes" point to a file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tools are v0.1.0 and open source under Apache-2.0: &lt;a href="https://github.com/tomelias10/orynval-labs" rel="noopener noreferrer"&gt;https://github.com/tomelias10/orynval-labs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you need this validated across a full production environment, that's what we do at &lt;a href="https://orynval.com" rel="noopener noreferrer"&gt;https://orynval.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
      <category>go</category>
    </item>
  </channel>
</rss>
