Start your free trial — no credit card required.
Seven Minutes, 100 Storage Accounts: Storm-3168 Found the Secret in a GitHub Edit History
Neha11 min read
SAAS SECURITY-CLOUD-BREACHES#azure#service-principals#storm-3168

Seven Minutes, 100 Storage Accounts: Storm-3168 Found the Secret in a GitHub Edit History

A service principal secret deleted from a public GitHub issue stayed live in its edit history. Storm-3168 used it to wipe most of a tenant's Azure Storage in seven minutes.

Share:

Someone at the victim organization did the right thing, just too late and in the wrong way. They noticed they'd pasted a service principal's client ID, client secret and tenant ID into a public GitHub issue, and they edited the issue to remove it.

GitHub keeps edit history. The secret stayed one click away, and nobody rotated it.

That's the opening of the intrusion Microsoft Threat Intelligence published on September 25, 2026, attributed to a cluster it tracks as Storm-3168. The activity happened in early June. By the time it was over, the attacker had attempted more than 100 storage account deletions, removed most of them, deleted a Key Vault, a Function App and its App Service plan, and then come back to collect keys from what was left. Microsoft links Storm-3168 to JADEPUFFER, the operation Sysdig's threat research team described in July as the first documented agentic ransomware campaign. Security Affairs and BleepingComputer both reported the same timeline from Microsoft's write-up.

I work on cloud identity, and the timeline is what stays with me. Not because it's exotic. Because almost every step is something your logs already record, and almost nothing in a normal Azure setup would have paged anyone before the deletions started.

So let's walk through it in order.

Hour 0: a credential nobody thought was live

Microsoft is blunt in its recommendations: "Treat credentials that have been publicly exposed as compromised, even if the original location has subsequently been edited or deleted." That sentence exists because people don't.

Here's the edge case I see over and over in reviews. An engineer pastes a config blob into an issue, a Slack channel or a support ticket while debugging. Someone points it out. The engineer edits the message, feels relieved, and moves on. The incident is now "handled" in their head, so there's no ticket to rotate anything. GitHub issue edits, Slack edits and most wiki platforms keep prior revisions. Secret scanners that run on commits don't always look at issue bodies, and almost none look at revision history.

The client secret on an Entra ID app registration can be valid for up to two years. If you don't rotate it, an edit buys you nothing.

This is the same class of problem as the API key METR's agent handed to an attacker: a non-human credential with real permissions, sitting somewhere the owner had stopped thinking about.

Hours 0 to 15.5: reading everything

The first service principal spent roughly 15 hours and 30 minutes doing discovery. Microsoft counts more than 300 successful read operations across virtual machines, subscriptions, resource groups and resources.

No writes. No deletes. Just reading.

If you've ever tried to build a detection for this phase, you know why it slips through. A service principal that lists resources is doing what service principals do. Infrastructure-as-code pipelines, cost tools, backup tools and CSPM scanners all enumerate. Three hundred reads over fifteen hours is a quiet day for a Terraform identity. Unless you baseline each principal individually, this blends in.

Hour 1.5: a second identity joins

About 90 minutes after the first principal started, a second service principal from the same tenant began its own enumeration. This was the one that mattered. According to Microsoft, it held Storage Account Contributor through a group assignment, Contributor directly, and SQL DB Contributor.

Read that list again. Contributor at that scope means it could delete almost anything it could see. The group-granted role is the detail I'd flag in a review: group-assigned roles on service principals are the ones that never show up when someone asks "what can this app do?" because people look at direct assignments first.

Microsoft also recorded five distinct tokens issued for this one service principal, with work divided between them. Two of the tokens used for deletion were active in the same 70-second window. That's not a person clicking in the portal. The user agent on the malicious ARM requests was python-requests/2.34.2, and Microsoft describes the pattern as coordinated, scripted or automated execution.

Hour 16: one failed call, then 70 seconds

Around the 16-hour mark, the second principal checked App Service configuration stores (successfully) and tried to discover Azure OpenSearch resources (unsuccessfully). Then it ran a ListKeys operation against a storage account that didn't exist. That call failed.

Seventy seconds later, the destruction began.

I'd love to know exactly what that failed call meant to the operator's logic. A stale inventory entry? A validation step? Microsoft doesn't say, so I won't guess. What I will point out is that this is the one moment in the whole timeline where the attacker touched something that wasn't real, and it produced an error in the logs. Nobody was watching for it. Hold that thought.

Minutes 0 to 7: the burst

In about seven minutes, the attacker made more than 100 storage account deletion attempts. Most succeeded. The Key Vault, Function App and App Service plan in one resource group went too.

In parallel, it tried to delete several Azure SQL databases. Every one of those failed, and the reason is almost funny: the requests used an API version that the Azure SQL database resource type doesn't support. The tooling had a bug. The databases survived because the attacker's code was wrong, not because a control stopped it.

It also went after recovery. Microsoft records repeated attempts to remove Azure Site Recovery locks and Azure Backup protection locks, all unsuccessful. Resource locks also blocked deletion of a few storage accounts.

Across the whole destructive window, Microsoft counts more than 150 destructive or credential-collection operations in about 35 minutes.

Thirty minutes later: back for the keys

About 30 minutes after the last deletion, the same identity returned and issued more than 30 successful ListKeys requests against the storage accounts that remained.

That ordering is worth thinking about. Destroy first, then harvest. Storage account keys are full-access, long-lived credentials that bypass Entra ID entirely. Anyone holding them can read and write data without ever generating a sign-in event. If you're responding to this incident and you rotate the service principal secret but not the storage keys, you've closed the front door and left a stack of master keys outside.

Why "agentic" matters here, and why it doesn't

Microsoft's headline calls these agentic-driven cloud attacks, and the JADEPUFFER link explains why. Sysdig's July report described an operation that exploited CVE-2025-3248, an unauthenticated code execution bug in Langflow, then let an LLM-based agent run the intrusion. Sysdig says it executed more than 600 distinct payloads, encrypted 1,342 configuration items without saving the decryption key, and recovered from a failed login with a corrected approach in 31 seconds.

The Storm-3168 timeline has the same fingerprints: parallel tokens, tight timing, quick moves after a failure. It also has the same weakness. An agent with no human in the loop will happily ship a request with a wrong API version and keep going.

Here's my opinion, and I'll own it. The "AI attacker" framing mostly matters for one number: the gap between first destructive action and done. Seven minutes. No on-call rotation acknowledges a page, logs in, understands the blast radius and revokes a service principal in seven minutes. If your detection for a compromised cloud identity fires on the deletions themselves, you've already lost the data that wasn't locked.

So the useful question isn't "can we detect deletion faster?" It's "what fires during the fifteen and a half hours of reading?"

The phase you can actually catch

Go back to the timeline. Fifteen-plus hours of enumeration across two identities. A config-store check. An OpenSearch probe. A ListKeys call against something that didn't exist.

Behavioral baselines can catch some of that, if you've built one per principal and you have the staff to triage it. Most teams I talk to haven't. They have a Defender for Cloud alert or two and a lot of service principals nobody owns.

There's a cheaper signal: plant things in the inventory that no legitimate process ever touches.

  • A decoy storage account with an inviting name like prodbackupsarchive that no app, pipeline or person uses. Any ListKeys, any read, any delete attempt on it is malicious by definition. An attacker that enumerates everything and then pulls keys from everything will hit it.
  • A decoy service principal secret placed exactly where Storm-3168 found the real one: a repo, an issue template, a wiki page, a .env in a dev container. The decoy authenticates to nothing useful. The moment someone tries it against Entra ID, you know your exposed-secret problem is being worked by an adversary, with their source IP.
  • Decoy Key Vault entries or App Service settings with fake connection strings. Storm-3168 read App Service configuration stores. Any credential sitting in that config is something an agent will try.

The edge case that makes this work against agents specifically: a human attacker might skip an oddly named resource. An automated operator that inventories 300+ objects and then iterates ListKeys across all storage accounts doesn't make judgment calls about which ones look fake. It touches all of them. That failed ListKeys 70 seconds before the burst shows this operator was already calling key operations on resources it hadn't verified.

There's an honest caveat. Last month I wrote about a phishing kit that deletes canary tokens from M365 mailboxes before using a session. Attackers do adapt to well-known canary formats. That's an argument for decoys that look like your real naming conventions and live in your real resource groups, not for skipping them. A decoy that matches your tenant's patterns is much harder to filter than a public canary service's domain.

We wrote a longer piece on honeytokens for non-human identities like API keys and service accounts if you want the design patterns. The short version: the alert has to come from use of the credential, not from someone noticing where it was stored.

What I'd do this week if I ran an Azure tenant

None of this is new advice, but the Storm-3168 timeline gives each item a concrete reason.

  1. Rotate, don't edit. Write it into your incident runbook: any secret posted anywhere public is rotated immediately, whether or not the post was edited or deleted. Then search your GitHub org's issue and PR edit histories for client_secret, AccountKey= and tenant GUIDs. GitHub's API exposes issue edit history; your scanners probably don't use it.
  2. Inventory service principal role assignments, including group-inherited ones. Any principal with Contributor at subscription scope and no named owner is your Storm-3168 candidate. Drop it to the narrowest built-in role it needs.
  3. Disable shared key access on storage accounts where you can, so ListKeys stops being a path to the data. Where you can't, rotate storage keys as part of every service principal compromise response, not just the principal's own secret.
  4. Put resource locks and immutable backup policies on recovery infrastructure. This is the control that actually held in June. Site Recovery and Backup protection locks survived repeated removal attempts. A few storage accounts survived because of resource locks. Everything without a lock was gone in seven minutes.
  5. Prefer workload identity federation or managed identities over client secrets. A federated credential has nothing to paste into an issue.
  6. Seed the inventory. Decoy storage accounts, a fake service principal secret in the places your engineers actually paste things, decoy connection strings in App Service config. Alert on any touch. That's the part that turns 15 hours of quiet reading into 15 hours of warning.

The broader identity pattern isn't Azure-specific. The LiteLLM gateways still answering to a quickstart key and Storm-3168's tenant failed for the same reason: a machine credential with too much reach, and no signal until it was used destructively. If you want to see how Mine2 approaches decoy credentials and cloud mines across identity and cloud attack paths, the solutions overview maps them to each stage.

The lesson in the SQL bug

I keep coming back to those Azure SQL databases. They survived because the attacker's tooling sent the wrong API version. That's luck, and you can't plan around an attacker's bugs.

What you can plan around is the shape of the attack. Agents read everything first, they act fast once they decide, and they don't second-guess a resource that looks a little off. Microsoft's own write-up shows the window between "first look" and "gone" was more than 15 hours wide and the window between "gone" and "too late" was seven minutes. Spend your detection budget on the first window. If you want to see what a decoy storage account or planted service principal secret looks like when it fires during that reconnaissance phase, book a Mine2 demo and we'll walk through it against an attack timeline like this one.

M2

Neha

Cloud Security Architect, Mine2

Neha works on cloud and identity security at Mine2, covering SaaS, OAuth, and the credential-theft paths attackers favour in the cloud.

Share this article

Secure Your Network Today

Ready to implement advanced cyber deception in your organization? See how MINE2 can transform your threat detection capabilities.