sbd.org.uk
Back to blog
Abstract geometric illustration of a row of four user identities, with an orange application identity above them linked to a gold crown privilege
Paul

Paul

Solution Architect

··12 min read

The Global Admin You Did Not Hire

A ConnectWise app added itself to Global Administrator. Why a people-first review misses vendor service principals and Entra agent identities too.

microsoft-365entra-idsecurityidentityzero-trustpowershellgraph-api

Which of your Global Administrators did you hire?

On 15 October 2026, from 01:58 UTC, ConnectWise Cloud Management service principals assigned themselves directory roles, mostly Exchange Administrator, in roughly 50 tenants monitored by Huntress. Between 03:44 and 03:49 UTC, in one tenant, the ConnectWise Cloud Management app for Microsoft Teams "added itself to the Global Administrator role". It then added new client secrets to two other applications in the tenant. Huntress later saw the same sequence in a second tenant.

Nobody in that tenant hired a Global Administrator. A vendor's application became one, and the sources do not show who, if anyone, authorised it. Microsoft assesses that Storm-0940, which it describes as China-state aligned, was responsible. ConnectWise says the root cause remains under investigation and reports no indication that its backup archives were compromised. It disabled seven application identities at 07:00 UTC on 17 October, and partners must now re-authorise each tenant under a new application, ConnectWise Cloud Backup for M365, which is a fresh consent worth reviewing.

Later that morning on 15 October, according to SpecterOps, the ConnectWise Cloud Backup application, formerly SkyKick, signed in to customer Microsoft 365 tenants from a hosting provider address between 06:13 and 08:34 UTC. The sign-ins used the application's own credential, so the attacker inherited whatever customers had granted the application.

In a Microsoft 365 tenant, real authority can sit with applications, sync services and agent identities that gained it through a vendor's consent request, a point release or a product's design. A review built around the people in the administrator roles does not ask about them. I take four documented cases, say what each shows and what it does not, and finish with scripts that start the inventory from the other end. Only the first case is literally a Global Administrator. The other three show the same blind spot: authority or reach that nobody appointed, or a control that does not cover the principal.

A vendor application that added itself to Global Administrator

The ConnectWise case is the sharpest, and one link in it is open. Two different applications appear, and the sources should not be merged: Cloud Backup in SpecterOps's account, Cloud Management for Teams in Huntress's. Huntress gives the same sign-in times as SpecterOps, from the first 290 or so tenants it checked, with a last confirmed successful sign-in at 08:44 UTC. The role grant came about two and a half hours before the first sign-ins from the hosting address. Huntress has no source address for the earlier activity, so it has not tied the two phases to the same operator.

SpecterOps credits The Hacker News, citing Microsoft, with the report that attackers were likely abusing permissions customers had already granted, "including Directory.ReadWrite.All". I could not read that report myself, and Microsoft's own post names no Graph permission, so treat the permission as reported. The report is about granted permissions generally, and the role grant came from a different application. Microsoft's reference does not list Directory.ReadWrite.All for assigning a role, so it would not explain the grant on its own.

Three things in it hold whatever the details turn out to be. The sign-in was an application sign-in, so in SpecterOps's words "no user, device, or MFA prompt is involved". The reach was whatever customers had granted the application, which SpecterOps lists as mailboxes, OneDrive and SharePoint files, Teams data and directory information. And disabling the application does not end the exposure at once: SpecterOps says "tokens that were already issued can stay valid for up to 90 minutes after you disable an app". Session, Not Sign-In covers how long tokens live and what revocation reaches.

None of the sources I read says how the Teams application was able to grant itself a role. Microsoft's Graph reference does say what the call needs: application-only role assignment requires RoleManagement.ReadWrite.Directory. AppRoleAssignment.ReadWrite.All lets an application grant itself that permission, and Application.ReadWrite.All lets it add a credential to an application that already holds it. I do not know which applied here. A platform flaw aside, each needs a grant already made in the tenant, and that grant is what a review should find.

Huntress confirmed unauthorised sign-ins in 162 tenants by 19:00 UTC on 16 October, and its next update counted almost 1,000 tenants with sign-in activity, over 300 of them successful. Microsoft says the actor attempted sign-ins to more than 2,250 tenants and gives no count of successes. These measure different things, and none is a total of tenants compromised.

A point release that adds permissions

The second case is smaller and more ordinary. Entra Connect Sync 2.6.91.0, released on 16 September 2026, carries this under an "Important" heading in Microsoft's version history:

Microsoft Graph permissions have been added to Microsoft Entra Connect. If you use app-scoped Conditional Access policies, review policies targeting Microsoft.Azure.SyncFabric or Microsoft 365 Reporting Service.
Microsoft, Entra Connect version history

The note does not say which permissions. It does not say what fails if a policy blocks them. The same release lists a fix for "multiple security vulnerabilities in bundled third-party dependencies", and the newest entry when I read the page, the 2.6.92.0 hotfix from 23 September, is marked "This release includes security fixes. We recommend upgrading to this version as soon as possible."

The release that carries the permissions also carries security fixes in bundled dependencies, and the hotfix that follows it is marked as a security release, so the sync team will apply it promptly. It also changes what the component can reach. The notice about that change is one bullet addressed to whoever owns Conditional Access, who is usually not the person patching the server. I covered what to check before that upgrade in the Connect Sync post; the point here is the route. Authority reached this component by upgrade.

I do not know whether the new permissions are dangerous. Microsoft's note does not name them, and it is the only advance notice I found.

An identity that can be disabled in tenants you do not administer

The third case is new to Entra. Agent identities are principals created for AI agents, and Microsoft's page on disabling them lists considerations that are easy to miss because they sit in a plain list under "Disabling agent identity considerations", not in a warning box:

If you disable a multi-tenant agent identity blueprint in all tenants, and the blueprint had principals and child identities created in other tenants, then related agents in those tenants may fail or lose functionality.

And, for Microsoft's own agents:

Microsoft product experiences that assume agent identity availability (for example, Copilot Studio agents, Security Copilot scenario agents, Microsoft Entra Conditional Access Optimization Agent) might fail or fall back to standard service principals that lack agent-specific tracking and controls.

Two things follow. The page states the first: the action "Disable in all tenants" is available to someone signed in as "at least an Agent ID Administrator", which is not Global Administrator, and it can affect agents in tenants that person does not administer. The second is my reading: the page says Microsoft product experiences "might fail or fall back to standard service principals" with less tracking, so disabling an agent can leave a less governed principal in its place.

The page footer still says 1 May 2026, while the page's own metadata shows a change on 23 October. Do not use the visible date to decide whether a Learn page is current.

A user account that is not a person

The fourth case sits furthest from the title, and it belongs here because it is the same blind spot seen from the policy side. An agent can have its own user account. Microsoft's agent user accounts page says that account can be added to groups, including dynamic groups, can be assigned licences, and "can be assigned with custom roles". The same page says the account "can't be assigned privileged administrator roles" and can't be added to role-assignable groups, so it is never a Global Administrator. It is a licensed principal with group-derived access and possibly custom roles, and it looks like a user account to any review that filters on users.

The Conditional Access page for agents then limits what policy can do:

Policies scoped through the "Users" assignment don't apply to agent user accounts. This boundary applies whether the policy targets all users, selected users, groups, directory roles, or external users. To protect agent user accounts, create a policy that targets "Agent Users."

The same page says that if an agent reaches a resource with an API key, Conditional Access "won't apply to them" at all, because the key bypasses Entra authentication.

So an "All users" policy no longer means every user. Microsoft documents the way round it, and the way round is a separate policy. A tenant that wrote its baseline before agent user accounts existed has nothing that targets them. Conditional Access for agents requires Microsoft 365 E7, or a Microsoft Agent 365 licence with at least Microsoft Entra ID P1 or Microsoft 365 E3. I have not tested this in a tenant, and the sign-in logs for an agent user account are where I would look first.

Why the review misses all four

In each case a principal gained authority, or escaped a control, by a route that a people-first review does not reliably cover:

HOW IT ESCAPES THE REVIEWWHAT THE REVIEW STARTS FROMVendor consentHeld until someone revokes itnot listedPoint releaseConnect Sync 2.6.91.0 adds permissionsnot listedAgent identity disableReaches agents in tenants you do not runnot listedAgent user accountsLook like users, sit outside Users scopenot listedPrivileged-access reviewStarts from people:the members of theadministrator roles.Lists the administrators.Lists an application onlyif you ask it to.
Four documented routes by which a principal gained authority or escaped a control. A review that starts from the members of the administrator roles lists the administrators and, unless asked, none of these.

Why each one is missed:

  • Application access consented to a vendor sits on the application, not in a role membership list, and in the first case the route to the role itself is still unexplained.
  • A release note about new permissions is addressed to the Conditional Access owner, not to whoever patches the server.
  • A disable action open to an Agent ID Administrator reaches agents in tenants that administrator does not run.
  • An agent user account looks like a user but sits outside the "Users" assignment, so a policy written for users skips it.

A privileged-access review that begins with the members of the administrator roles, as role-membership reports typically do, will list the administrators. A default PIM access review of a Microsoft Entra role scopes to users and groups, and reviewing service principals needs Microsoft Entra Workload ID Premium as well as Microsoft Entra ID P2 or Microsoft Entra ID Governance. Neither will surface an application that holds a powerful permission without holding a role at all. Directory.ReadWrite.All is a permission, not a role, and it does not let an application create a role assignment. It can still write directory objects, so whether it matters is a question about what the application can do, and the review has to be built to ask it.

Starting the inventory from the other end

Two independent questions. Which service principals hold a directory role? Which hold an application permission that can reach one, whether or not they hold a role?

The first script answers the first question. It needs RoleManagement.Read.Directory, which Microsoft's roleAssignments reference lists as the least privileged permission for this call, delegated or application. Delegated use also needs a role, such as Global Reader.

It has three limits. It lists active assignments only, so eligible assignments held through Privileged Identity Management (PIM) are not in it. It lists direct assignments to service principals only. The reference page's examples filter by roleDefinitionId or principalId, so a tenant that rejects the unfiltered request below needs a loop over the role definitions instead. Neither script here has run against a tenant; I checked both with the PowerShell parser.

Connect-MgGraph -Scopes 'RoleManagement.Read.Directory' -NoWelcome
 
$roles = @{}
$uri = 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$select=id,displayName'
do {
    $page = Invoke-MgGraphRequest -Method GET -Uri $uri
    foreach ($r in $page.value) { $roles[$r.id] = $r.displayName }
    $uri = $page.'@odata.nextLink'
} while ($uri)
 
$uri = 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?$expand=principal'
$rows = do {
    $page = Invoke-MgGraphRequest -Method GET -Uri $uri
    foreach ($a in $page.value) {
        if ($a.principal.'@odata.type' -eq '#microsoft.graph.servicePrincipal') {
            [pscustomobject]@{
                Role      = $roles[$a.roleDefinitionId]
                Principal = $a.principal.displayName
                AppId     = $a.principal.appId
                ObjectId  = $a.principal.id
                Scope     = $a.directoryScopeId
            }
        }
    }
    $uri = $page.'@odata.nextLink'
} while ($uri)
 
$rows | Sort-Object Role, Principal | Format-Table -AutoSize

The second script answers the second question. It needs Application.Read.All and covers Microsoft Graph permissions only, not Exchange or SharePoint application permissions.

Connect-MgGraph -Scopes 'Application.Read.All' -NoWelcome
 
$base = 'https://graph.microsoft.com/v1.0'
$graphAppId = '00000003-0000-0000-c000-000000000000'
$graph = (Invoke-MgGraphRequest -Method GET -Uri "$base/servicePrincipals?`$filter=appId eq '$graphAppId'&`$select=id,appRoles").value[0]
 
$names = @{}
foreach ($r in $graph.appRoles) { $names[$r.id] = $r.value }
 
$watch = 'RoleManagement.ReadWrite.Directory', 'AppRoleAssignment.ReadWrite.All',
         'Application.ReadWrite.All', 'Application.ReadWrite.OwnedBy', 'Directory.ReadWrite.All'
 
$uri = "$base/servicePrincipals/$($graph.id)/appRoleAssignedTo"
$rows = do {
    $page = Invoke-MgGraphRequest -Method GET -Uri $uri
    foreach ($a in $page.value) {
        if ($names[$a.appRoleId] -in $watch) {
            [pscustomobject]@{
                Permission = $names[$a.appRoleId]
                Principal  = $a.principalDisplayName
                ObjectId   = $a.principalId
                Type       = $a.principalType
            }
        }
    }
    $uri = $page.'@odata.nextLink'
} while ($uri)
 
$rows | Sort-Object Permission, Principal | Format-Table -AutoSize

For each principal that surprises you, this request returns every application permission granted to it. {id} is the principal's object ID, not its appId.

GET https://graph.microsoft.com/v1.0/servicePrincipals/{id}/appRoleAssignments

The response gives each permission as an appRoleId, a GUID, so you resolve the names against the resource's own app roles. The tenant assessment scripts in the Graph API post already do some of this, and I will not repeat them.

This is a starting inventory, not a complete one. It does not cover owners of applications and service principals (/servicePrincipals/{id}/owners), delegated grants (/oauth2PermissionGrants), federated credentials, permissions on resources other than Microsoft Graph, Azure RBAC or partner delegated administration. The directory enumeration post shows how far any member can already list these objects, and the tenant health post has queries for ownerless app registrations and expired credentials.

Then change the habits around the list:

  1. Treat a vendor release note that mentions permissions as a change to your privileged-access model. Connect Sync is the example; the same question applies to any component that holds a credential in your tenant.
  2. Give each vendor application a named owner in your organisation, and put its consent on the same review cycle as an administrator role. The owner I mean is a person who can say why it still needs what it holds.
  3. Count agent identities and agent user accounts now, while they are few, and write a policy that targets "Agent Users" before the first one needs it.
  4. Treat containment as a sequence, because disabling an application removes nothing it changed.
    1. Export the Entra audit and service principal sign-in logs first. ConnectWise's remediation guide gives 30 days of retention on P1 or P2 and seven on Free, and turning logging on later does not recover events.
    2. Disable every affected service principal in every tenant, including dormant connections, and do not delete it. Disabling stops new tokens for that service principal in that tenant only. SpecterOps reports that issued tokens can stay valid for up to 90 minutes, and that is not the end of the exposure, because a credential added to another application outlasts any token.
    3. Search the audit log from 13 October, with those applications as initiator and as target, for role additions, credential changes, app role assignments, owner additions and consent.
    4. Remove any role you did not grant. Check client secrets, certificates and federated credentials on all applications and service principals, not only the vendor's. In the tenant Huntress describes, the application added itself to Global Administrator and put client secrets, set to expire in 2028, on two other applications. Removing a secret does not remove a certificate or federated credential, and adding a replacement credential does not remove an attacker's.
    5. Rotate any secret the application could read, including Key Vault secrets. Microsoft reports secrets read at scale across multiple Key Vaults in this campaign.
    6. For reads, use Microsoft Graph activity logs and Purview. Graph activity logs exist only if collection was already on, so an empty result does not prove nothing was read.

What is verified, and what is not

I last fetched these pages on 5 November 2026. Each quotation matched the page on that day.

  • Verified at the primary source, Microsoft's own pages: the Connect Sync 2.6.91.0 note and the 2.6.92.0 entry dated 23 September 2026 (page metadata changed 6 October 2026); the agent identity disabling considerations (footer 1 May 2026, metadata changed 23 October 2026); the agent user account roles and group behaviour (footer 11 June 2026, metadata changed 30 September 2026); the Conditional Access limits for agent user accounts (footer 30 September 2026, metadata changed 7 October 2026); and the Graph reference pages behind the scripts.
  • Verified at the primary source, Microsoft Threat Intelligence: the Storm-0940 attribution, the attempted sign-ins and the Key Vault reads, from the 21 October post. That post names no Graph permission, gives no token lifetime and does not explain the self-assignment.
  • Verified at the primary source, Huntress: the role self-assignments, the ConnectWise Global Administrator grant, the client secrets added to two other applications, the tenant counts and the 15 October sign-in window, from Huntress's advisory, in the version updated 27 October.
  • Verified at the primary source, ConnectWise: the statements on root cause, backup archives, the disabled identities and re-authorisation, from the advisory last updated 4 November, and the log retention and credential points from the remediation guide dated 18 October.
  • Not verified at the primary source: the Directory.ReadWrite.All report, which SpecterOps attributes to The Hacker News citing Microsoft, and the 90 minute token figure, from SpecterOps, last updated 19 October. The Hacker News returned 403 to a plain fetch.
  • Not read at all: Microsoft Threat Intelligence's 25 September post on Storm-3168, a case of a leaked service principal secret. I have not used anything from it.
  • Not tested: the Conditional Access behaviour for agent user accounts, and what the Connect Sync permissions are. Neither has been run in a tenant. The scripts were parsed, not run.

Start with both scripts, and ask of each result who in the organisation can say why it holds what it holds.

Get the next one by email

Long, specific write-ups on M365 architecture and security, worked out against real tenants rather than summarised from documentation. Sent rarely, and only when it is worth your time.