Entra ID Security Cheatsheet
The identity plane is the perimeter. How attackers hit Entra ID, credentials, tokens, conditional access, privileged roles, app and workload identities, how to detect each in KQL, and how to harden against it. No account needed.
These run in Microsoft Sentinel (Logs) and the Entra portal's log views. Identity is where most modern intrusions begin and persist, so the detections here target the identity stack directly: the sign-in, the token, the role, the app. Tune thresholds and org values to your tenant before alerting.
The identity attack surface
An attacker in Entra ID is after one of a few things: valid credentials, a live token, a privileged role, or a foothold in an application or workload identity that outlives the user. Each maps to a detection surface. The sections below follow that surface, the highest-value identity detections, each as a real query you can adapt.
| Attacker goal | Where it shows |
|---|---|
| Valid credentials | SigninLogs: spray, stuffing, anomalous success, risk detections. |
| A live session token | SigninLogs: AiTM proxy indicators, concurrent sessions, token replay. |
| A privileged role | AuditLogs: direct role adds, PIM bypass, standing privilege. |
| App / workload foothold | AuditLogs / AADServicePrincipalSignInLogs: client secrets, federated creds, OAuth grants. |
| A trusted outside identity | External / B2B guest activity, redemption, risk. |
Sign-in log forensics
SigninLogs is the primary identity evidence source, but it has two streams: interactive (a human at a prompt) and non-interactive (tokens, refresh, background). Attackers using a stolen token surface in the non-interactive stream, so reading both, and comparing them, is where token misuse first shows.
// Detect potential token replay: different IPs in interactive
// vs non-interactive for the same user within 1 hour
let timeRange = 1h;
let interactiveIPs = SigninLogs
| where TimeGenerated > ago(timeRange)
| where ResultType == 0
| distinct UserPrincipalName, IPAddress;
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(timeRange)
| where ResultType == 0
| join kind=inner interactiveIPs on UserPrincipalName
| where IPAddress != IPAddress1
| summarize
NonInteractiveIP = any(IPAddress),
InteractiveIP = any(IPAddress1),
EventCount = count()
by UserPrincipalName
| where EventCount > 5
// Results: users with concurrent sessions from different IPs
// Validate: is the non-interactive IP a VPN, proxy, or Microsoft IP?
The tell above: the same user authenticated from two different IPs in a way consistent with a token used somewhere the original sign-in did not originate. Pair it with Identity Protection's own risk scoring, which flags many of these on its own.
// EI5.2: Detailed sign-in risk detection analysis
SigninLogs
| where TimeGenerated > ago(7d)
| where RiskLevelDuringSignIn != "none" and isnotempty(RiskLevelDuringSignIn)
| extend RiskDetections = parse_json(RiskEventTypes_V2)
| mv-expand RiskDetection = RiskDetections
| extend DetectionType = tostring(RiskDetection)
| summarize
Count = count(),
Users = dcount(UserPrincipalName),
SuccessCount = countif(ResultType == 0),
BlockedCount = countif(ResultType == 53003)
by DetectionType, RiskLevelDuringSignIn
| order by Count desc
// Shows which specific detections are most common in your tenant
// SuccessCount > 0 for high-risk detections = risk policies may not be enforcing
// BlockedCount > 0 = CA policies are responding to risk signals (good)
Confirms TP: a risk detection that Identity Protection raised on a sign-in that nonetheless succeeded, the attacker got in despite the risk signal, and conditional access did not block it.
Conditional access: attacks and validation
Conditional access is the control attackers most want to evade. Two moves dominate: authenticating through legacy protocols that do not support modern auth (and therefore skip MFA), and weakening or excluding a policy once privileged. Hunt both.
// EI4.2: Detect password spray through legacy protocols
SigninLogs
| where TimeGenerated > ago(24h)
| where ClientAppUsed in ("Exchange ActiveSync", "IMAP4", "POP3",
"Authenticated SMTP", "Other clients")
| summarize
Attempts = count(),
SuccessCount = countif(ResultType == 0),
FailCount = countif(ResultType != 0),
DistinctUsers = dcount(UserPrincipalName),
DistinctIPs = dcount(IPAddress)
by ClientAppUsed, bin(TimeGenerated, 1h)
| where DistinctUsers > 5 and FailCount > 20
| order by Attempts desc
// Pattern: many users targeted, many failures, few successes
// High DistinctUsers with high FailCount = spray campaign in progress
// ANY SuccessCount > 0 = compromised accounts: investigate immediately
Confirms TP: password-spray failures arriving over legacy authentication protocols, which bypass conditional-access MFA entirely. If legacy auth is not blocked tenant-wide, this is your exposure.
Token security
Token theft is the highest-value identity attack because it sidesteps the password and MFA both. Adversary-in-the-middle phishing proxies the real login, captures the issued token, and replays it. The detections target the proxy infrastructure and the replay pattern.
// EI7.3: Detect potential AiTM proxy indicators in sign-in logs
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| where RiskLevelDuringSignIn in ("medium", "high")
| extend RiskDetections = tostring(RiskEventTypes_V2)
| where RiskDetections has_any ("anomalousToken", "tokenIssuerAnomaly",
"unfamiliarFeatures", "impossibleTravel")
| extend DeviceOS = tostring(DeviceDetail.operatingSystem)
| extend IsCompliant = tostring(DeviceDetail.isCompliant)
| extend Browser = tostring(DeviceDetail.browser)
| project TimeGenerated, UserPrincipalName, IPAddress,
Country = tostring(LocationDetails.countryOrRegion),
RiskDetections, DeviceOS, IsCompliant, Browser, AppDisplayName
| order by TimeGenerated desc
// Indicators of proxy-based theft:
// - anomalousToken: token properties inconsistent with legitimate authentication
// - Non-compliant device or empty device details (proxy does not report a real device)
// - IP from hosting provider (proxy infrastructure, not user's ISP)
// - Followed by non-interactive sign-ins from a DIFFERENT IP (token replay)
Confirms TP: sign-in telemetry consistent with AiTM proxy infrastructure, an MFA-satisfied sign-in routed through a cloud-hosting ASN that is not a normal egress for the user.
// EI7.4: Detect concurrent sessions from different IPs (possible cookie theft)
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| where IsInteractive == false // Non-interactive = token renewal
| summarize
DistinctIPs = dcount(IPAddress),
IPs = make_set(IPAddress, 10),
Apps = make_set(AppDisplayName, 10)
by UserPrincipalName, bin(TimeGenerated, 1h)
| where DistinctIPs > 2 // Same user, same hour, 3+ different IPs
| order by DistinctIPs desc
// Multiple IPs for the same user in the same hour from non-interactive sign-ins
// May indicate: VPN switching (legitimate), mobile IP hopping (legitimate),
// or cookie replay from attacker infrastructure (investigate)
// Check: are any of the IPs hosting providers? (attacker indicator)
Confirms TP: concurrent sessions for one identity from different IPs in a window too tight for legitimate movement, the original session and the attacker's replayed one, live at once.
PIM and privileged identity
Standing privilege is the prize. Privileged Identity Management exists to make privilege just-in-time and audited, so the attacks are: find permanent privileged roles to ride, or add privilege by a direct assignment that skips PIM. Audit the standing roles and watch the activation log.
// EI6.1: Audit all permanent (active) privileged role assignments
AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName == "Add member to role"
| extend Target = tostring(TargetResources[0].userPrincipalName)
| extend Role = tostring(TargetResources[0].modifiedProperties[1].newValue)
// This only shows recent additions. For the complete picture, use Graph API
// or the Entra admin center: Identity → Roles and administrators
The audit above lists permanent (always-on) privileged assignments, every one is standing attack surface that should arguably be PIM-eligible instead of active.
// EI6.5: Failed PIM activation attempts
AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName has "PIM"
| where Result == "failure" or ResultReason has "denied" or ResultReason has "failed"
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend Role = tostring(TargetResources[0].modifiedProperties[1].newValue)
| extend FailureReason = tostring(ResultReason)
| project TimeGenerated, Actor, Role, FailureReason
| order by TimeGenerated desc
// Failed activations: the user attempted to activate but could not complete
// Common legitimate failures: FIDO2 key not recognized, approval timeout
// Suspicious failures: multiple failures from a user who normally activates successfully
// Multiple failures across different roles = possible attacker exploring available privileges
Confirms TP: a burst of failed PIM activation attempts can indicate an attacker probing for a role they cannot quite reach, worth correlating with the account's other recent activity.
Applications and workload identity
Apps and workload identities authenticate as themselves, not as a user, so a credential added to an app outlives any password reset on the human who owns it. The two highest-value detections: a new client secret or certificate on an app registration, and federated-credential abuse on a workload identity.
// EI9.3: Detect client secret additions to application registrations
AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName in ("Add service principal credentials",
"Update application – Certificates and secrets management")
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend AppName = tostring(TargetResources[0].displayName)
| extend CredentialDetails = tostring(TargetResources[0].modifiedProperties)
| project TimeGenerated, Actor, AppName, OperationName, CredentialDetails
| order by TimeGenerated desc
// Every credential addition or modification in the last 7 days
// EVERY result should be authorized through change management
// An unexpected credential addition may indicate:
// - An attacker establishing persistence (adding their own secret to an existing app)
// - A developer creating a new secret without following the credential policy
Confirms TP: a client secret or certificate added to an application or service principal outside a known change, a classic durable-persistence move, the app now authenticates without the user.
// EI10.5: Monitor federated credential usage across environments
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| where ServicePrincipalName has_any ("app-dev", "app-staging", "app-production",
"deploy", "pipeline", "cicd")
| extend Environment = case(
ServicePrincipalName has "dev", "Development",
ServicePrincipalName has "staging" or ServicePrincipalName has "qa", "Staging",
ServicePrincipalName has "prod" or ServicePrincipalName has "production", "Production",
"Unknown")
| summarize
SignIns = count(),
IPs = make_set(IPAddress, 5),
Resources = make_set(ResourceDisplayName, 5)
by ServicePrincipalName, Environment
| order by Environment, SignIns desc
// CI/CD pipeline identity activity grouped by environment
// Production identities should only sign in from known CI/CD IP ranges
// Development identities signing in to production resources = environment leak (investigate)
Confirms TP: federated-credential usage on a workload identity from an unexpected environment, or a stale federated credential suddenly active, points at workload-identity abuse.
External identities
Guest and B2B identities are a trusted door from outside the tenant. A compromised guest, or an over-permissioned one, is access an attacker did not have to phish for. Inventory and risk-score them.
// EI11.1: Per-guest risk scoring
SigninLogs
| where TimeGenerated > ago(90d)
| where UserType == "Guest" and ResultType == 0
| extend Country = tostring(LocationDetails.countryOrRegion)
| extend AuthMethod = tostring(parse_json(AuthenticationDetails)[0].authenticationMethod)
| summarize
SignIns = count(),
DistinctApps = dcount(AppDisplayName),
DistinctCountries = dcount(Country),
DistinctIPs = dcount(IPAddress),
UsesWeakAuth = countif(AuthMethod has "email" or AuthMethod has "oneTimePasscode"),
LastSignIn = max(TimeGenerated)
by UserPrincipalName, HomeTenantId
| extend RiskScore =
DistinctApps * 3 // Broad access = higher risk
+ DistinctCountries * 5 // Multiple countries = higher risk
+ iff(UsesWeakAuth > 0, 20, 0) // Weak auth = significant risk bump
+ iff(DistinctIPs > 10, 10, 0) // Many IPs = unusual
| order by RiskScore desc
// Per-guest risk score: prioritize governance attention
// High RiskScore: broad access + multiple countries + weak auth = priority review
// Low RiskScore: single app + single country + strong auth = standard governance
Confirms TP: a guest identity with elevated risk, broad access, or activity inconsistent with its expected use is a trust-boundary exposure worth scoping like an internal compromise.
Hardening checklist
Detection finds the attack; configuration prevents it. The defensive baseline for Entra ID, each item closes one of the surfaces above.
| Control | Closes |
|---|---|
| Block legacy authentication | The MFA-bypass path for spray and stuffing. The single highest-value control. |
| Phishing-resistant MFA | Raises the bar against AiTM token theft (FIDO2/passkeys resist proxying). |
| Conditional access baseline | Require compliant device / managed app; block risky sign-ins; no broad exclusions. |
| PIM for all privileged roles | Removes standing privilege; makes elevation just-in-time and audited. |
| Token / session lifetime | Shortens the window a stolen token stays valid; continuous access evaluation. |
| App & workload restrictions | Limit who can consent to apps; review app credentials; restrict workload-identity federation. |
| External-identity governance | Restrict B2B, review guest access, expire stale guests. |
A user satisfies MFA, but the sign-in routes through a cloud-hosting ASN the user never uses (AiTM proxy indicator). Minutes later, SigninLogs shows a non-interactive session for the same user from a second IP, concurrent with the first. The attacker proxied the login, captured the token, and is now replaying it. The password held; the token did not.
Respond: revoke sessions (not just reset the password, the token survives a reset), then check for the persistence that survives a revoke: a new app client secret, a federated credential, an added CA exclusion. Phishing-resistant MFA is the control that would have prevented the proxy.
Quick lookup
| Identity attack | Table + signal |
|---|---|
| Password spray / stuffing | SigninLogs, error 50126 across many accounts; check legacy-auth path |
| Token theft (AiTM) | SigninLogs, proxy-ASN sign-in + concurrent/replayed session |
| Risky sign-in that succeeded | SigninLogs + AADUserRiskEvents, risk raised but ResultType 0 |
| Privilege escalation | AuditLogs, direct role add outside PIM; standing privileged roles |
| App persistence | AuditLogs, client-secret/cert added to app or service principal |
| Workload-identity abuse | AADServicePrincipalSignInLogs, federated-cred usage from new env |
| Guest / B2B abuse | External identity activity, guest risk scoring |
| CA evasion | SigninLogs, legacy-auth success; AuditLogs, CA policy change |
From detecting identity attacks to designing the defense
This cheatsheet is the detection set. Entra ID Security teaches the whole identity stack: conditional-access architecture, token and PIM security, application and workload identity, and the complete hardened design that closes these surfaces.
Explore the course