<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>SSPR on IAMDevBox</title><link>https://www.iamdevbox.com/tags/sspr/</link><description>Recent content in SSPR on IAMDevBox</description><image><title>IAMDevBox</title><url>https://www.iamdevbox.com/IAMDevBox.com.jpg</url><link>https://www.iamdevbox.com/IAMDevBox.com.jpg</link></image><generator>Hugo -- 0.146.0</generator><language>en-us</language><lastBuildDate>Thu, 01 Oct 2026 20:21:55 -0400</lastBuildDate><atom:link href="https://www.iamdevbox.com/tags/sspr/index.xml" rel="self" type="application/rss+xml"/><item><title>Storm-3068: How SSPR Abuse Turns One Azure AD Account Into Kubernetes Credential Theft</title><link>https://www.iamdevbox.com/posts/storm-3068-sspr-account-takeover-azure-devops-kubernetes-credential-theft/</link><pubDate>Thu, 01 Oct 2026 18:00:00 +0000</pubDate><guid>https://www.iamdevbox.com/posts/storm-3068-sspr-account-takeover-azure-devops-kubernetes-credential-theft/</guid><description>Microsoft&amp;#39;s Storm-3068 campaign chains self-service password reset abuse, MFA method hijacking, and Azure DevOps pipeline manipulation to steal Kubernetes kubeconfig files. Here&amp;#39;s the exact attack chain and the SSPR, Conditional Access, and RBAC controls that stop it.</description><content:encoded><![CDATA[<p>Microsoft disclosed in September 2026 that a threat actor tracked as <strong>Storm-3068</strong> 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.</p>
<h2 id="the-attack-chain-step-by-step">The Attack Chain, Step by Step</h2>
<p>Storm-3068&rsquo;s campaign worked because each individual step abuses a legitimate feature, not a vulnerability:</p>
<ol>
<li><strong>SSPR takeover</strong> — The attacker completed a self-service password reset on a target user account, most likely after obtaining the account&rsquo;s recovery information through a prior phishing or credential-stuffing attempt elsewhere.</li>
<li><strong>Device registration</strong> — 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.</li>
<li><strong>MFA hijack</strong> — The attacker registered their own MFA method on the account and removed the legitimate user&rsquo;s MFA method, converting a one-time credential theft into persistent, re-authenticatable account control.</li>
<li><strong>Azure DevOps reconnaissance</strong> — 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.</li>
<li><strong>Pipeline weaponization</strong> — The attacker created or modified Azure DevOps pipelines to run a Kubernetes agent step, using the pipeline&rsquo;s own service connection permissions to pull <code>kubectl config view --raw</code> output and commit the resulting kubeconfig files into a repository. One malicious pipeline alone was authorized to touch more than 50 resources.</li>
<li><strong>Tooling for persistence</strong> — The attacker modified pipelines further to deploy the <strong>Atera</strong> remote monitoring agent (legitimate RMM software, abused for remote access) and <strong>Chisel</strong> (a TCP/UDP tunneling tool), using Chisel to proxy Kubernetes API traffic back to attacker infrastructure.</li>
</ol>
<blockquote>
<p><strong>Why this matters more than a typical phishing writeup</strong>: every step after the initial SSPR abuse used only legitimate product features — Intune enrollment, pipeline authoring, service connections. There&rsquo;s no malware signature to catch. The defense has to be permission boundaries and anomaly detection, not endpoint protection.</p></blockquote>
<h2 id="fix-1-lock-down-self-service-password-reset">Fix 1: Lock Down Self-Service Password Reset</h2>
<p>The entry point is SSPR completed without device or location context. Require a trusted signal before SSPR can succeed:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Entra Admin Center → Protection → Conditional Access → New Policy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Name: Require Compliant Device for SSPR
</span></span><span style="display:flex;"><span>Assignments:
</span></span><span style="display:flex;"><span>  Users: All users
</span></span><span style="display:flex;"><span>  Target resources: User flows → Self-service password reset
</span></span><span style="display:flex;"><span>Conditions:
</span></span><span style="display:flex;"><span>  Device platforms: Any (do not exclude)
</span></span><span style="display:flex;"><span>Grant:
</span></span><span style="display:flex;"><span>  Require device to be marked as compliant
</span></span><span style="display:flex;"><span>  OR Require Microsoft Entra hybrid joined device
</span></span><span style="display:flex;"><span>Enable policy: On
</span></span></code></pre></div><p>If your organization can&rsquo;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.</p>
<h2 id="fix-2-alert-on-mfa-method-changes-not-just-sign-ins">Fix 2: Alert on MFA Method Changes, Not Just Sign-Ins</h2>
<p>Storm-3068&rsquo;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&rsquo;t alert on it.</p>
<pre tabindex="0"><code class="language-kql" data-lang="kql">// Microsoft Sentinel / Log Analytics
AuditLogs
| where TimeGenerated &gt; ago(1d)
| where OperationName in (&#34;User registered security info&#34;, &#34;User deleted security info&#34;, &#34;Admin deleted security info&#34;)
| 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)
</code></pre><p>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&rsquo;s historical baseline — that sequence is exactly what converts a one-time SSPR compromise into standing access.</p>
<h2 id="fix-3-audit-and-scope-azure-devops-service-connections">Fix 3: Audit and Scope Azure DevOps Service Connections</h2>
<p>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:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># List all service connections in a project</span>
</span></span><span style="display:flex;"><span>az devops service-endpoint list <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --org https://dev.azure.com/&lt;your-org&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --project &lt;project-name&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --query <span style="color:#e6db74">&#34;[].{name:name, type:type, authScheme:authorization.scheme, allPipelines:isShared}&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -o table
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check which pipelines are authorized to use a given connection</span>
</span></span><span style="display:flex;"><span>az devops service-endpoint show <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --org https://dev.azure.com/&lt;your-org&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --project &lt;project-name&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --id &lt;service-connection-id&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --query <span style="color:#e6db74">&#34;serviceEndointProjectReferences&#34;</span>
</span></span></code></pre></div><p>Harden the settings that matter most:</p>
<ul>
<li><strong>Disable &ldquo;Grant access permission to all pipelines&rdquo;</strong> on every Kubernetes/cluster service connection — require explicit per-pipeline authorization instead.</li>
<li><strong>Scope each connection to one cluster/namespace</strong>, not a subscription-wide or multi-cluster service principal. A pipeline that deploys to <code>staging</code> should not hold credentials that can read <code>production</code> kubeconfig.</li>
<li><strong>Require pipeline YAML to come from a protected branch</strong> (<code>Approvals and checks → Branch control</code>) so an attacker with repo write access can&rsquo;t redefine what a trusted pipeline does.</li>
</ul>
<h2 id="fix-4-rotate-and-restrict-kubernetes-credentials-at-the-source">Fix 4: Rotate and Restrict Kubernetes Credentials at the Source</h2>
<p>Even with pipeline scoping, assume any kubeconfig that ever transited a CI/CD system should be treated as short-lived, not standing:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Prefer workload identity federation over long-lived kubeconfig secrets</span>
</span></span><span style="display:flex;"><span>az aks update <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --resource-group &lt;rg&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --name &lt;cluster-name&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --enable-oidc-issuer <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --enable-workload-identity
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Audit existing service account tokens for long expiry</span>
</span></span><span style="display:flex;"><span>kubectl get secrets --all-namespaces <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -o json | jq -r <span style="color:#e6db74">&#39;.items[] | select(.type==&#34;kubernetes.io/service-account-token&#34;) | .metadata.namespace + &#34;/&#34; + .metadata.name&#39;</span>
</span></span></code></pre></div><p>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 <a href="/posts/mtls-certificate-authentication-microservices-kubernetes/">mTLS Certificate Authentication for Kubernetes Microservices</a> guide. If your cluster still issues long-lived service account secrets for CI/CD, migrating off them is the first priority.</p>
<h2 id="fix-5-hunt-for-the-persistence-tooling">Fix 5: Hunt for the Persistence Tooling</h2>
<p>Atera and Chisel are both legitimate tools being abused here, which means signature-based AV won&rsquo;t flag them. Hunt for their network and process fingerprints directly:</p>
<pre tabindex="0"><code class="language-kql" data-lang="kql">// Flag non-browser outbound connections from build agents
DeviceNetworkEvents
| where TimeGenerated &gt; ago(7d)
| where DeviceName has_any (&#34;build&#34;, &#34;agent&#34;, &#34;runner&#34;, &#34;devops&#34;)
| where RemoteUrl has &#34;atera.com&#34;
   or (InitiatingProcessFileName !in~ (&#34;chrome.exe&#34;,&#34;msedge.exe&#34;,&#34;svchost.exe&#34;)
       and RemotePort !in (443, 80)
       and Protocol == &#34;Tcp&#34;)
| project TimeGenerated, DeviceName, InitiatingProcessFileName, RemoteUrl, RemotePort
</code></pre><p>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.</p>
<h2 id="incident-response-checklist">Incident Response Checklist</h2>
<p>If you find evidence of this attack chain in your tenant, Microsoft&rsquo;s containment guidance comes down to five actions, in order:</p>
<ol>
<li><strong>Disable the compromised identity</strong> and force a credential reset through an out-of-band channel (not SSPR).</li>
<li><strong>Remove all attacker-registered MFA methods and devices</strong> — don&rsquo;t just reset the password; the attacker&rsquo;s second factor is the real persistence mechanism.</li>
<li><strong>Revoke and reissue every credential that touched an affected pipeline</strong> — kubeconfig files, service connection secrets, and any PATs (personal access tokens) the compromised identity could have generated.</li>
<li><strong>Audit and delete unauthorized pipelines, pipeline modifications, and service connections</strong> created or altered during the compromise window.</li>
<li><strong>Rotate cluster-level credentials</strong> (not just the stolen kubeconfig) if there&rsquo;s evidence the attacker reached the Kubernetes API directly — assume the kubeconfig&rsquo;s embedded certificate or token was used, not just copied.</li>
</ol>
<p>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 <a href="/posts/oauth-device-code-flow-security-prevent-device-code-phishing/">OAuth Device Code Phishing defense guide</a>; for broader non-human credential hygiene across CI/CD systems, see <a href="/posts/nhi-secrets-sprawl-fixing-the-non-human-identity-credential-crisis/">NHI Secrets Sprawl</a>.</p>
]]></content:encoded></item></channel></rss>