Microsoft disclosed in September 2026 that a threat actor tracked as Storm-3068 compromised a single Azure AD identity through self-service password reset (SSPR) abuse, then pivoted through Azure DevOps pipelines to steal Kubernetes cluster credentials — ultimately exfiltrating seven kubeconfig files and gaining access to more than 50 downstream resources from one initial account takeover. No password spray, no zero-day: just SSPR, MFA re-registration, and pipeline permissions that were already too broad. This is the exact attack chain and the controls that close each step.

The Attack Chain, Step by Step

Storm-3068’s campaign worked because each individual step abuses a legitimate feature, not a vulnerability:

  1. SSPR takeover — The attacker completed a self-service password reset on a target user account, most likely after obtaining the account’s recovery information through a prior phishing or credential-stuffing attempt elsewhere.
  2. Device registration — Immediately after the password reset, the attacker registered an attacker-controlled device and onboarded it into Microsoft Intune using the now-compromised identity, giving the device a trusted posture in the tenant.
  3. MFA hijack — The attacker registered their own MFA method on the account and removed the legitimate user’s MFA method, converting a one-time credential theft into persistent, re-authenticatable account control.
  4. Azure DevOps reconnaissance — Using the now fully-controlled identity, the attacker enumerated Azure DevOps repositories, pipelines, deployment environments, and connected service resources — all through normal, logged, authenticated API calls.
  5. Pipeline weaponization — The attacker created or modified Azure DevOps pipelines to run a Kubernetes agent step, using the pipeline’s own service connection permissions to pull kubectl config view --raw output and commit the resulting kubeconfig files into a repository. One malicious pipeline alone was authorized to touch more than 50 resources.
  6. Tooling for persistence — The attacker modified pipelines further to deploy the Atera remote monitoring agent (legitimate RMM software, abused for remote access) and Chisel (a TCP/UDP tunneling tool), using Chisel to proxy Kubernetes API traffic back to attacker infrastructure.

Why this matters more than a typical phishing writeup: every step after the initial SSPR abuse used only legitimate product features — Intune enrollment, pipeline authoring, service connections. There’s no malware signature to catch. The defense has to be permission boundaries and anomaly detection, not endpoint protection.

Fix 1: Lock Down Self-Service Password Reset

The entry point is SSPR completed without device or location context. Require a trusted signal before SSPR can succeed:

Entra Admin Center → Protection → Conditional Access → New Policy

Name: Require Compliant Device for SSPR
Assignments:
  Users: All users
  Target resources: User flows → Self-service password reset
Conditions:
  Device platforms: Any (do not exclude)
Grant:
  Require device to be marked as compliant
  OR Require Microsoft Entra hybrid joined device
Enable policy: On

If your organization can’t mandate compliant devices for all users, scope this policy to admin roles and service-account owners first — those are the identities whose compromise lets an attacker reach Azure DevOps and production infrastructure.

Fix 2: Alert on MFA Method Changes, Not Just Sign-Ins

Storm-3068’s persistence step — removing the legitimate MFA method and registering a new one — is the highest-signal, lowest-noise detection point in the whole chain. Most tenants log this event but don’t alert on it.

// Microsoft Sentinel / Log Analytics
AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName in ("User registered security info", "User deleted security info", "Admin deleted security info")
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend ActorUser = tostring(InitiatedBy.user.userPrincipalName)
| where TargetUser == ActorUser  // self-service change, not admin-initiated
| project TimeGenerated, OperationName, TargetUser, Result,
          IPAddress = tostring(parse_json(tostring(AdditionalDetails))[0].value)

Pair this with a rule that fires when an MFA method deletion is followed within minutes by a new method registration from a different IP or device fingerprint than the account’s historical baseline — that sequence is exactly what converts a one-time SSPR compromise into standing access.

Fix 3: Audit and Scope Azure DevOps Service Connections

The pipeline-to-Kubernetes pivot only works because service connections are frequently granted broader scope than any single pipeline needs. List what each pipeline can actually touch:

# List all service connections in a project
az devops service-endpoint list \
  --org https://dev.azure.com/<your-org> \
  --project <project-name> \
  --query "[].{name:name, type:type, authScheme:authorization.scheme, allPipelines:isShared}" \
  -o table

# Check which pipelines are authorized to use a given connection
az devops service-endpoint show \
  --org https://dev.azure.com/<your-org> \
  --project <project-name> \
  --id <service-connection-id> \
  --query "serviceEndointProjectReferences"

Harden the settings that matter most:

  • Disable “Grant access permission to all pipelines” on every Kubernetes/cluster service connection — require explicit per-pipeline authorization instead.
  • Scope each connection to one cluster/namespace, not a subscription-wide or multi-cluster service principal. A pipeline that deploys to staging should not hold credentials that can read production kubeconfig.
  • Require pipeline YAML to come from a protected branch (Approvals and checks → Branch control) so an attacker with repo write access can’t redefine what a trusted pipeline does.

Fix 4: Rotate and Restrict Kubernetes Credentials at the Source

Even with pipeline scoping, assume any kubeconfig that ever transited a CI/CD system should be treated as short-lived, not standing:

# Prefer workload identity federation over long-lived kubeconfig secrets
az aks update \
  --resource-group <rg> \
  --name <cluster-name> \
  --enable-oidc-issuer \
  --enable-workload-identity

# Audit existing service account tokens for long expiry
kubectl get secrets --all-namespaces \
  -o json | jq -r '.items[] | select(.type=="kubernetes.io/service-account-token") | .metadata.namespace + "/" + .metadata.name'

Projected, short-lived service account tokens (the Kubernetes default since 1.24+) expire in an hour by default — a kubeconfig stolen via a malicious pipeline run is far less useful to an attacker than a long-lived static credential. For the broader pattern of replacing long-lived cluster credentials with certificate-based workload identity, see our mTLS Certificate Authentication for Kubernetes Microservices guide. If your cluster still issues long-lived service account secrets for CI/CD, migrating off them is the first priority.

Fix 5: Hunt for the Persistence Tooling

Atera and Chisel are both legitimate tools being abused here, which means signature-based AV won’t flag them. Hunt for their network and process fingerprints directly:

// Flag non-browser outbound connections from build agents
DeviceNetworkEvents
| where TimeGenerated > ago(7d)
| where DeviceName has_any ("build", "agent", "runner", "devops")
| where RemoteUrl has "atera.com"
   or (InitiatingProcessFileName !in~ ("chrome.exe","msedge.exe","svchost.exe")
       and RemotePort !in (443, 80)
       and Protocol == "Tcp")
| project TimeGenerated, DeviceName, InitiatingProcessFileName, RemoteUrl, RemotePort

Any hit on a self-hosted Azure DevOps agent or build runner warrants immediate isolation — neither Atera nor Chisel has a legitimate reason to run inside a CI/CD execution environment.

Incident Response Checklist

If you find evidence of this attack chain in your tenant, Microsoft’s containment guidance comes down to five actions, in order:

  1. Disable the compromised identity and force a credential reset through an out-of-band channel (not SSPR).
  2. Remove all attacker-registered MFA methods and devices — don’t just reset the password; the attacker’s second factor is the real persistence mechanism.
  3. Revoke and reissue every credential that touched an affected pipeline — kubeconfig files, service connection secrets, and any PATs (personal access tokens) the compromised identity could have generated.
  4. Audit and delete unauthorized pipelines, pipeline modifications, and service connections created or altered during the compromise window.
  5. Rotate cluster-level credentials (not just the stolen kubeconfig) if there’s evidence the attacker reached the Kubernetes API directly — assume the kubeconfig’s embedded certificate or token was used, not just copied.

This campaign is a reminder that identity-layer hardening — SSPR, MFA method integrity, and pipeline permission scoping — now sits upstream of Kubernetes security, not alongside it. For the OAuth-specific variant of credential theft through social engineering rather than pipeline abuse, see our OAuth Device Code Phishing defense guide; for broader non-human credential hygiene across CI/CD systems, see NHI Secrets Sprawl.