☁️Entra IDApplicationsMonitoringRisk ProtectionConditional Access

Entra Service Principal Sign-In Anomaly Detection: Workload Identity Blind Spots

Service principals cannot perform MFA, are missed by ordinary Conditional Access, and their sign-ins live in a separate log stream. Learn to baseline and detect anomalies on the identities holding your widest Graph permissions.

Younes AZABARBy Younes AZABAR15 min read
Entra Service Principal Sign-In Anomaly Detection: Workload Identity Blind Spots

Entra Service Principal Sign-In Anomaly Detection: Workload Identity Defined

Entra service principal sign-in anomaly detection, workload identity monitoring that baselines how machine accounts authenticate, is the one control most tenants never configure. Every other identity control in a mature tenant — MFA enrollment campaigns, phishing-resistant authentication methods, risk-based Conditional Access — is built around a human being present at the keyboard. Service principals have no human, and the controls simply do not reach them.

Microsoft is explicit about why these identities are different. A workload identity is an identity that allows an application or service principal to access resources, and Microsoft's own documentation lists three properties that separate it from a user account: workload identities "Can't perform multifactor authentication", "Often have no formal lifecycle process", and "Need to store their credentials or secrets somewhere."

Read those three together and the exposure becomes obvious. The credential is a string sitting in a pipeline variable, a Key Vault, an appsettings file, or a container image layer. There is no second factor behind it, no lifecycle that expires it on a leaver's last day, and — because a service principal sign-in involves no user — nothing that looks unusual to a human-centric alerting stack. Once that string leaks, the attacker authenticates exactly as the application does, and the sign-in succeeds on the first attempt.

⚠️

⚠️ Warning: After a service principal secret leaks there is no password to spray, no MFA prompt to fatigue and no user to phish. The only signal left is the shape of the sign-ins: how many, from where, on which network, with which credential.

Why Your MFA and Conditional Access Coverage Does Not Apply

Conditional Access is the control administrators assume covers everything. It does not — workload identities are one of several places its coverage quietly stops, alongside the scope and exclusion mistakes catalogued in Entra ID Conditional Access Gaps: What Misconfigurations Leave Real Exposure. Microsoft's documentation states that Conditional Access policies "historically applied only to users when they access apps and services", and that support for service principals is a separate capability called Conditional Access for workload identities.

That separate capability carries restrictions that decide whether your policy actually does anything:

ConstraintWhat Microsoft's documentation states
Licensing"Workload Identities Premium licenses are required to create or modify Conditional Access policies scoped to service principals." Existing policies keep working in unlicensed directories but cannot be modified.
Scope"Policy can be applied to single tenant service principals that are registered in your tenant. Microsoft and third-party SaaS applications, including multitenant apps, are not covered by these policies. Managed identities aren't covered by policy."
Group assignment"While service principals can be added to groups, Conditional Access policies assigned to a group that contains a service principal are not enforced for that service principal." It must be assigned directly as a workload identity.
Available controlsUnder Grant, "Block access is the only available option." There is no "require MFA" to fall back on.

The group-assignment rule deserves emphasis, because it fails silently. An administrator who drops service principals into a security group already targeted by a Conditional Access policy will see a policy that looks correctly scoped in the portal and enforces nothing at all against those service principals. Nothing errors. Nothing warns. The policy is simply not evaluated for them.

The supported conditions are narrow by design: blocking service principals from "outside of known public IP ranges", blocking "based on risk detected by Microsoft Entra ID Protection", and authentication contexts. Location and risk — not device, not MFA, not compliance.

There is also an asymmetry between detecting risk and blocking it that is easy to get backwards. ID Protection "detects risk on single tenant, non-Microsoft SaaS, and multitenant apps", while Conditional Access enforcement reaches only single-tenant service principals. Managed identities are outside both. So for a multitenant SaaS application in your tenant, you can be told it is compromised but cannot block it with a workload identity policy — which makes detection and response, not prevention, the operative control.

Where Service Principal Sign-Ins Actually Live

The second reason these sign-ins go unwatched is that most hunting queries never touch them. Microsoft Entra records four types of sign-in logs: interactive user sign-ins, non-interactive user sign-ins, service principal sign-ins, and managed identity sign-ins. A SOC that built its detections against user sign-ins is not looking at a filtered subset of one table — it is looking at a different stream entirely.

The service principal stream is its own thing. Microsoft describes it plainly: "Unlike interactive and non-interactive user sign-ins, service principal sign-ins don't involve a user. Instead, they're sign-ins by any nonuser account, such as apps or service principals... In these sign-ins, the app or service provides its own credential, such as a certificate or app secret to authenticate or access resources."

When exported to a Log Analytics workspace through Diagnostic settings, these land in the AADServicePrincipalSignInLogs table — not SigninLogs. If you have never enabled that log category, the queries in the next section return zero rows, and zero rows will look exactly like a clean environment.

Retention is the other half of the prerequisite. Microsoft's data retention reference gives sign-in logs seven days on Microsoft Entra ID Free and 30 days on P1 or P2. A behavioural detection needs a baseline plus a recent window inside that budget — you cannot build a 90-day profile from a 30-day log. Getting the export configured is covered in Entra ID Logging: Retention, Diagnostic Settings, SIEM Export Gaps; treat it as a hard prerequisite for everything below.

These are the columns that carry the signal:

ColumnWhy it matters
AppId, ServicePrincipalNameThe identity to baseline. AppId is the stable grouping key.
IPAddress, LocationDetails, AutonomousSystemNumberWhere the workload authenticated from. A workload's network is usually far more stable than a human's.
ServicePrincipalCredentialKeyId, ClientCredentialTypeWhich credential was used, and whether it was a client secret or a client assertion.
ResourceDisplayName, ResourceIdentityWhat it reached for. New target resources are a strong pivot signal.
ResultType"0" for success. Failures are a different investigation.
ConditionalAccessStatusWhether a workload identity policy was even evaluated.
FederatedCredentialIdPopulated when a federated identity credential was used instead of a stored secret.

The Attack Chain After a Secret Leaks

Step 1 — The credential escapes

The secret leaves the environment: committed to a public repository, captured from a build log, pulled out of a compromised developer workstation, or bought. Microsoft's Leaked Credentials workload identity detection exists precisely because this is routine — it checks credentials acquired "from GitHub, the dark web, paste sites, or other sources" against valid credentials in the tenant. The same service covers user accounts, where the recurring failure is response rather than detection — see Entra ID Risk Protection: Leaked Credentials, Risky Users Not Remediated.

Step 2 — Authentication that looks entirely legitimate

The attacker performs an OAuth client credentials flow with the stolen secret. No MFA is possible, so none is missing. If no workload identity policy is directly assigned, ConditionalAccessStatus reflects a sign-in nothing evaluated. The result is a successful sign-in that is byte-for-byte the same operation the real application performs a thousand times a day. Only the surrounding context differs.

Step 3 — Persistence by adding a credential

Rather than rely on a secret that may be rotated, an attacker with sufficient privilege adds their own credential to the application or service principal object. The identity now has two valid credentials, and revoking the leaked one changes nothing. This step leaves two traces: a directory change in the audit log, and — on every subsequent sign-in — a ServicePrincipalCredentialKeyId that has never appeared for that AppId before.

Step 4 — Acting through Graph

The service principal's application permissions are now the blast radius, and application permissions are not constrained by any user's rights. Tenant-wide grants such as Directory.ReadWrite.All or RoleManagement.ReadWrite.Directory turn a leaked secret into directory-wide control; see Entra App Registration Dangerous Graph API Permissions for how these grants are inventoried and reduced. Microsoft's Suspicious API Traffic detection targets this stage, firing "when abnormal GraphAPI traffic or directory enumeration of a service principal is observed."

Detection

Behavioural detection here rests on a property that works in the defender's favour: workloads are boringly consistent. A nightly sync job runs at the same hour, from the same egress IP, against the same resource, with the same credential. Humans are erratic; automation is not. That consistency makes deviation meaningful — and it is why the equivalent detections for human accounts, such as impossible travel and AiTM token replay, have to tolerate far more noise than the queries below.

IndicatorWhere to lookKey fieldsWhat it means
Sign-in volume spikeAADServicePrincipalSignInLogsAppId, count over timeBulk collection or enumeration through a stolen token
First-seen country or ASNAADServicePrincipalSignInLogsLocationDetails, AutonomousSystemNumberThe workload authenticated from infrastructure it has never used
New credential in useAADServicePrincipalSignInLogsServicePrincipalCredentialKeyId, ClientCredentialTypeAn attacker-added credential, or an unmanaged rotation
New target resourceAADServicePrincipalSignInLogsResourceDisplayNameThe identity is reaching past its normal job
Credential added to the objectEntra audit logsApplication / service principal credential changesThe persistence step in Step 3
Leaked credential / suspicious sign-inID Protection workload identity detectionsriskyServicePrincipals, servicePrincipalRiskDetectionsMicrosoft-side correlation you do not have to build

Volume spike against a per-identity baseline

// Workload identity volume spike: last 24h vs the preceding daily baseline
let Recent = 1d;
let Lookback = 30d;
let MinRecent = 20;       // suppress low-volume identities
let MinBaselineDays = 7;  // suppress identities we have not observed long enough
let Baseline =
    AADServicePrincipalSignInLogs
    | where TimeGenerated between (ago(Lookback) .. ago(Recent))
    | where ResultType == "0"
    | summarize Total = count(), Days = dcount(startofday(TimeGenerated)) by AppId
    | where Days >= MinBaselineDays and Total > 0
    | extend BaselineDaily = todouble(Total) / todouble(Days);
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(Recent)
| where ResultType == "0"
| summarize RecentCount = count(), Name = take_any(ServicePrincipalName) by AppId
| where RecentCount >= MinRecent
| join kind=inner (Baseline) on AppId
| extend SpikeRatio = round(RecentCount / BaselineDaily, 1)
| where SpikeRatio >= 5
| project Name, AppId, RecentCount, BaselineDaily = round(BaselineDaily, 1), SpikeRatio
| order by SpikeRatio desc

First-seen country or autonomous system

// A workload identity authenticating from a country or ASN it has never used
let Recent = 1d;
let Lookback = 30d;
let Known =
    AADServicePrincipalSignInLogs
    | where TimeGenerated between (ago(Lookback) .. ago(Recent))
    | where ResultType == "0"
    | extend Country = tostring(parse_json(tostring(LocationDetails)).countryOrRegion)
    | where isnotempty(Country)
    | summarize KnownCountries = make_set(Country),
                KnownASNs = make_set(AutonomousSystemNumber) by AppId;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(Recent)
| where ResultType == "0"
| extend Country = tostring(parse_json(tostring(LocationDetails)).countryOrRegion)
| where isnotempty(Country)
| join kind=inner (Known) on AppId
| where array_length(KnownCountries) > 0
| where not(set_has_element(KnownCountries, Country))
     or not(set_has_element(KnownASNs, AutonomousSystemNumber))
| project TimeGenerated, ServicePrincipalName, AppId, Country,
          ASN = AutonomousSystemNumber, IPAddress, ResourceDisplayName,
          NewCountry = not(set_has_element(KnownCountries, Country))
| order by TimeGenerated desc

A credential that has never been used before

// Sign-in using a credential key this workload identity has never presented
let Recent = 1d;
let Lookback = 30d;
let KnownKeys =
    AADServicePrincipalSignInLogs
    | where TimeGenerated between (ago(Lookback) .. ago(Recent))
    | where ResultType == "0" and isnotempty(ServicePrincipalCredentialKeyId)
    | summarize KnownKeyIds = make_set(ServicePrincipalCredentialKeyId) by AppId;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(Recent)
| where ResultType == "0" and isnotempty(ServicePrincipalCredentialKeyId)
| join kind=inner (KnownKeys) on AppId
| where not(set_has_element(KnownKeyIds, ServicePrincipalCredentialKeyId))
| project TimeGenerated, ServicePrincipalName, AppId,
          ServicePrincipalCredentialKeyId, ClientCredentialType,
          IPAddress, ResourceDisplayName
| order by TimeGenerated desc

Pair that last query with the audit log entry recording the credential being added to the application or service principal object. A new credential key with no corresponding, approved change request is the highest-fidelity signal in this article.

💡

💡 Tip: The thresholds above (>= 5x, >= 20 recent sign-ins, >= 7 baseline days) are guardrails, not physics. Each one exists to kill a specific false positive: a brand-new integration with no history would otherwise show an infinite ratio, and one sign-in against a baseline of 0.1/day would otherwise read as a tenfold spike. Tune them against your own data before alerting.

Two limitations deserve honesty. First, a baseline shorter than the ideal is unavoidable when the log itself expires in 7 to 30 days — if you have not exported to a workspace with longer retention, your baseline is capped by your license. Second, a legitimate deployment change (a new region, a migrated build agent, a planned rotation) produces exactly the same signature as an intrusion. Microsoft acknowledges the same effect in its own detection: "Sign-ins that are initiated after an authorized configuration change might trigger this detection." These queries produce investigation queues, not verdicts.

Microsoft's own Suspicious Sign-ins detection for workload identities is worth enabling alongside them. It "learns the baselines sign-in behavior for workload identities in your tenant", takes "between 2 and 60 days", and fires on unfamiliar "IP address / ASN, target resource, user agent, hosting/non-hosting IP change, IP country, credential type" — the same feature set, correlated Microsoft-side. Customers without Workload Identities Premium "still receive all detections with limited reporting details."

Remediation

💡

💡 Quick Win: Enable the service principal sign-in log category in Diagnostic settings and route it to a Log Analytics workspace. Without it, every query above returns zero rows — and zero rows are indistinguishable from a healthy tenant.

  1. Export the stream and extend the window. Route service principal sign-in logs to a workspace whose retention exceeds the 7-or-30-day default, so a baseline is possible at all. See Entra ID Logging: Retention, Diagnostic Settings, SIEM Export Gaps.

  2. Remove the secret rather than guard it. Where the workload runs on Azure, use managed identities so the platform manages credentials. Where it runs outside Azure — GitHub Actions, Kubernetes, GCP, AWS — use workload identity federation, which exchanges a token from an external identity provider for an access token. Microsoft's rationale is direct: stored credentials "pose a security risk and have to be stored securely and rotated regularly", whereas federation "eliminates the risk of leaking secrets or having certificates expire." A credential that does not exist cannot be leaked.

  3. Scope a workload identity Conditional Access policy — correctly. Create a location-based policy under Users or workload identities → Workload identities, target All resources, include Any location and exclude the named locations your workload legitimately runs from. Remember the three traps: it requires Workload Identities Premium, it must be assigned to the service principal directly rather than through a group, and Block access is the only grant control. Save it in Report-only mode first and read the results in the Service principal sign-ins view before enforcing.

  4. Inventory and rotate credentials. Microsoft's remediation guidance for a compromised workload identity is to inventory every credential on both the service principal and application objects, add a new one (x509 certificates preferred), remove the compromised credentials — "If you believe the account is at risk, we recommend removing all existing credentials" — and rotate any Key Vault secrets that identity could reach. Rotation hygiene in general is covered in Entra App Registration Credential Rotation Secrets.

  5. Shrink the blast radius before you need to. The damage a leaked secret can do is bounded by the application permissions granted to it. Strip tenant-wide Graph grants that are not genuinely required, and re-examine service principals holding directory roles — the static posture side of this problem is covered in Entra Service Principal Admin Role Over-Privileged.

  6. Verify. Re-run the three queries. A correctly instrumented tenant returns rows for your known deployment events and nothing else; a tenant returning literally nothing has a logging problem, not a clean bill of health. Where this check sits in a full tenant review is set out in How to Audit Microsoft Entra ID Security (Azure AD): Practical Review Guide.

How EtcSec Detects This

The etc-collector audit engine ships two detectors that cover exactly this telemetry, both rated High:

  • RISK_SP_SIGNIN_SPIKE — flags a service principal that signed in far more often in the last 24 hours than over the preceding days of the collected window. Grouping is per appId over machine sign-in event types only, so an interactive user sign-in that happens to carry an appId cannot dilute an application's measured rate.
  • RISK_UNUSUAL_GEO_ADMIN — flags a privileged account that signed in successfully from a country it had not used earlier in the collected window. This is the human twin of the same telemetry: the same "first-seen geography" logic applied to admins instead of workloads.

Both are built around the guardrails described above rather than a raw ratio: an identity with too little baseline history stays silent, an identity with too few recent sign-ins stays silent, an empty known-country set stays silent, and sign-ins with no resolved country are ignored rather than counted as a new country. Success filtering differs between the two: RISK_UNUSUAL_GEO_ADMIN counts successful sign-ins only, on both sides of the split, so it does not overlap with RISK_FAILED_SIGNIN_BURST, which already covers failed-attempt patterns; RISK_SP_SIGNIN_SPIKE measures its rate over every machine sign-in in the window, successful or failed.

The recent window is anchored on the most recent collected event rather than wall-clock time, so replaying an audit hours later compares the same days.

ℹ️

ℹ️ Note: EtcSec automatically checks for this vulnerability during every AD/Azure audit. Run a free audit to verify your environment.

Explore the identity security pages that support this topic