Microsoft Entra ID Hardening Guide
Microsoft Entra ID hardening for the identity control plane — authentication methods and strengths, Conditional Access policy authoring, PIM role settings, access reviews, restricted management administrative units, and identity log export.
Overview
Microsoft Entra ID (formerly Azure Active Directory) is the cloud identity platform for over 720 million users across enterprises worldwide. As the authentication backbone for Microsoft 365, Azure, and thousands of SaaS applications, Entra ID security is foundational to Zero Trust architecture. The January 2024 Midnight Blizzard breach of Microsoft’s corporate environment demonstrated how a single misconfigured test account without MFA can cascade into widespread compromise.
This is a product guide within the Microsoft platform. Tenant-wide requirements — that MFA is required, that legacy authentication is blocked, that break-glass accounts exist and are monitored, that PIM governs admin roles, that OAuth consent is restricted, and that the unified audit log is on — live in the Microsoft 365 Common Controls hub and are referenced here rather than restated. Everything below is the identity control plane itself: the policy objects an administrator authors in the Entra admin center to make those requirements real, plus the identity-only controls the hub does not reach.
The two layers fail independently, which is why both exist. The hub decides what the tenant requires; this guide decides what the identity system actually enforces — and a tenant policy that says “require MFA” enforces nothing stronger than SMS if the authentication methods policy still has SMS enabled for everyone.
Intended Audience
- Security engineers managing identity infrastructure
- IT administrators configuring Entra ID tenants
- GRC professionals assessing IAM compliance
- Third-party risk managers evaluating SSO integrations
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers the Entra ID admin surface: the authentication methods policy and authentication strengths, Conditional Access policy authoring, Privileged Identity Management role settings, access reviews and restricted management administrative units, application registration and legacy-API governance, and identity log export. Tenant-wide policy requirements are in the Microsoft 365 Common Controls guide; Intune’s admin plane is in the Microsoft Intune guide. Azure infrastructure is covered separately.
Automation surface: every Conditional Access, PIM, and emergency-access control below ships Terraform in a single interdependent module (packs/microsoft-entra-id/terraform/) plus Microsoft Graph PowerShell. Diagnostic-settings export requires the azurerm provider rather than azuread, and PIM role settings (activation duration, approval chain) are admin-center or Graph-beta only — the controls below say so where it applies.
Table of Contents
- Authentication & Access Controls
- Conditional Access
- Privileged Identity Management
- Application Security
- Monitoring & Detection
- Third-Party Integration Security
- Compliance Quick Reference
1. Authentication & Access Controls
1.1 Configure Authentication Methods and Authentication Strengths
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.3, 6.5 |
| NIST 800-53 | IA-2(1), IA-2(6) |
Tenant requirement: that MFA is required for all users is Microsoft 365 §1.1, which also carries Microsoft’s mandatory-MFA enforcement timeline. This control is what makes that requirement mean something stronger than SMS.
Description
Enable the authentication methods your tenant will accept and disable the ones it will not, then author the reusable authentication strength that Conditional Access policies reference. The authentication methods policy is the Entra-native object that decides which factors can ever satisfy an MFA prompt; a Conditional Access policy that grants on “require multifactor authentication” accepts whatever this policy leaves enabled.
Rationale
Why This Matters:
- A tenant-wide “require MFA” policy is satisfied by SMS or voice if those methods are still enabled — the requirement and the factor strength are configured in two different places
- SMS and voice are defeated by SIM swapping and by real-time phishing proxies (Evilginx, Modlishka); FIDO2 and Windows Hello for Business are origin-bound and cannot be relayed
- Authentication strengths are reusable objects: author “Phishing-Resistant MFA” once and every admin-scoped Conditional Access policy can grant on it, instead of each policy re-specifying factors
Attack Prevented: Real-time phishing proxy relay, SIM swap, MFA fatigue, downgrade to a weak second factor
Real-World Incidents:
- Midnight Blizzard (2024): Test account without MFA led to Microsoft corporate email compromise
Prerequisites
- Microsoft Entra ID P1 or P2 license
- FIDO2 security keys provisioned for privileged users
- Authentication Policy Administrator, Security Administrator, or Global Administrator role
ClickOps Implementation
Step 1: Configure Authentication Methods
- Navigate to: Protection → Authentication methods → Policies
- Enable the methods you intend to accept:
- Passkey (FIDO2): Enable for all users. Under Configure, set Enforce attestation to Yes and Enforce key restrictions to Yes, restricting to your approved key AAGUIDs
- Microsoft Authenticator: Enable with number matching and application/location context displayed
- Temporary Access Pass: Enable for onboarding and key-replacement scenarios
- Disable the methods you do not: set SMS and Voice call to Disabled, or target them to a named recovery group only
Step 2: Create the Phishing-Resistant Authentication Strength
- Navigate to: Protection → Authentication methods → Authentication strengths
- Click + New authentication strength
- Name: “Phishing-Resistant MFA”
- Select:
- Passkey (FIDO2)
- Windows Hello for Business
- Certificate-based authentication (CBA)
- Save — Conditional Access policies grant on this object by name
Step 3: Retire Security Defaults if Still Enabled
- Navigate to: Identity → Overview → Properties → Manage security defaults
- Security Defaults and Conditional Access are mutually exclusive. Tenants with P1/P2 should run Conditional Access; leave Security Defaults enabled only where no Conditional Access policy exists yet, and turn it off in the same change that enables the tenant MFA policy
Time to Complete: ~45 minutes
Code Implementation
Code Pack: CLI Script
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Policy.ReadWrite.AuthenticationMethod"
# Get current authentication method policy
$policy = Get-MgPolicyAuthenticationMethodPolicy
# Enable FIDO2
$fido2Config = @{
id = "fido2"
state = "enabled"
includeTargets = @(
@{
targetType = "group"
id = "all_users"
}
)
}
Update-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration `
-AuthenticationMethodConfigurationId "fido2" `
-BodyParameter $fido2Config
# Configure Microsoft Authenticator with number matching
$authAppConfig = @{
id = "microsoftAuthenticator"
state = "enabled"
featureSettings = @{
displayAppInformationRequiredState = @{
state = "enabled"
}
displayLocationInformationRequiredState = @{
state = "enabled"
}
numberMatchingRequiredState = @{
state = "enabled"
}
}
}
Update-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration `
-AuthenticationMethodConfigurationId "microsoftAuthenticator" `
-BodyParameter $authAppConfig
Validation & Testing
How to verify the control is working:
- As a test user, attempt to register SMS as a method — it should be unavailable
- Sign in against a policy that grants on the “Phishing-Resistant MFA” strength using a passkey — it should succeed; using Authenticator push — it should fail
- Review Monitoring → Sign-in logs → the Authentication Details tab and confirm the satisfying method is the one you intended
- Check the Authentication methods activity report for users still registered on disabled methods
Expected result: Only phishing-resistant factors can satisfy admin sign-in; weak methods cannot be registered or used
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access security |
| NIST 800-53 | IA-2(1), IA-2(6) | Multi-factor authentication |
| ISO 27001 | A.9.4.2 | Secure log-on procedures |
| CIS M365 Foundations | v7.0.0 | MFA enforcement (see benchmark for current IDs) |
1.2 Maintain the Emergency Access Exclusion Group
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 5.1 |
| NIST 800-53 | AC-2 |
Tenant requirement: creating the break-glass accounts, registering their phishing-resistant credentials, storing the credentials offline, and alerting on their use are Microsoft 365 §1.4. This control is the Conditional Access half — the exclusion group every policy in §2 references.
Description
Place the emergency access accounts in a dedicated security group and exclude that group — never individual user objects — from every Conditional Access policy in the tenant. Excluding a group means a new policy needs one exclusion entry rather than a per-account lookup, and it makes “is break-glass excluded from this policy?” a single verifiable condition.
Rationale
Why This Matters:
- The most common way to lock every administrator out of a tenant is a Conditional Access policy authored without a break-glass exclusion — and the person who can fix it is the person who just got locked out
- Excluding accounts individually does not scale: each new policy is a new chance to forget one, and Microsoft-managed policies (2.5) auto-enable on a timer whether or not anyone remembered
- A named group is auditable — you can enumerate every policy and assert the exclusion, which you cannot do reliably against a list of UPNs
Attack Prevented: Self-inflicted tenant lockout that delays incident response; loss of recovery path during an active intrusion
Prerequisites
- Emergency access accounts created per Microsoft 365 §1.4
- Global Administrator or Conditional Access Administrator role
ClickOps Implementation
Step 1: Create the Exclusion Group
- Navigate to: Microsoft Entra admin center → Groups → All groups → + New group
- Type: Security, name it explicitly (e.g., “Emergency Access Accounts”)
- Add both emergency accounts as direct members — not dynamic membership, which depends on a rule that can itself break
Step 2: Exclude the Group From Every Policy
- Navigate to: Protection → Conditional Access → Policies
- Edit each policy — including every policy labeled Created by: Microsoft
- Under Users → Exclude → Users and groups, add the emergency access group
- Save
Step 3: Re-verify After Every Policy Change
- Treat “is the emergency group excluded?” as a required review item on any new or edited Conditional Access policy
- Re-run the check after Microsoft introduces a managed policy — those appear without an admin creating them
Time to Complete: ~20 minutes
Code Implementation
The Terraform module provisions the emergency accounts and the exclusion group together, and every Conditional Access resource in this guide references that group.
Code Pack: Terraform
# Create emergency access (break-glass) accounts
resource "azuread_user" "emergency_admin" {
count = var.emergency_account_count
user_principal_name = "${var.emergency_account_upn_prefix}-${format("%02d", count.index + 1)}@${var.domain_name}"
display_name = "Emergency Admin ${format("%02d", count.index + 1)}"
mail_nickname = "${var.emergency_account_upn_prefix}-${format("%02d", count.index + 1)}"
account_enabled = true
# Password is managed outside Terraform -- generate a 64+ character
# random password and store it in a physically secure location (safe/vault).
# Terraform manages the account lifecycle, not the credential.
password = random_password.emergency[count.index].result
force_password_change = false
disable_password_expiration = true
disable_strong_password = false
lifecycle {
ignore_changes = [password]
}
}
# Generate initial passwords for emergency accounts
resource "random_password" "emergency" {
count = var.emergency_account_count
length = 64
special = true
override_special = "!@#$%&*()-_=+[]{}|;:,.<>?"
}
# Look up the Global Administrator role
data "azuread_directory_role" "global_admin" {
display_name = "Global Administrator"
}
# Assign Global Administrator role to emergency accounts
resource "azuread_directory_role_assignment" "emergency_global_admin" {
count = var.emergency_account_count
role_id = data.azuread_directory_role.global_admin.template_id
principal_object_id = azuread_user.emergency_admin[count.index].object_id
}
# Create a group for emergency accounts (used for Conditional Access exclusions)
resource "azuread_group" "emergency_access" {
display_name = "HTH Emergency Access Accounts"
description = "Break-glass accounts excluded from Conditional Access policies"
security_enabled = true
mail_enabled = false
members = azuread_user.emergency_admin[*].object_id
}
Validation & Testing
- Enumerate all Conditional Access policies and confirm each excludes the emergency access group
- Sign in with an emergency account and confirm no Conditional Access policy applies (check the Conditional Access tab of the sign-in log entry, not just that sign-in succeeded)
- Confirm the alert configured in Microsoft 365 §1.4 fired for that sign-in
- Immediately rotate the password after any test sign-in
2. Conditional Access
2.1 Block Legacy Authentication
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.2 |
| NIST 800-53 | IA-2, AC-17 |
Tenant requirement: the org-wide decision to retire legacy authentication — including Microsoft’s firm EWS and SMTP AUTH end dates and the Exchange-side SMTP AUTH disablement — is Microsoft 365 §1.2. This control is the Conditional Access policy that enforces it at the identity layer.
Description
Author the Conditional Access policy that blocks the legacy client app types (exchangeActiveSync and other) tenant-wide. Conditional Access is the only place a legacy-auth block applies across every workload at once; per-service switches such as SMTP AUTH still have to be set in Exchange Online.
Rationale
Why This Matters:
- Legacy protocols authenticate with a password alone, so no MFA policy can apply to them — blocking the client app type is the enforcement, not a supplement to it
- A tenant that disables SMTP AUTH in Exchange but leaves the Conditional Access block unauthored is still reachable over the other legacy client types
- Report-only mode on this policy is the cheapest inventory of which applications still depend on legacy auth, before the vendor cutover forces the issue
Attack Prevented: Password spray via legacy protocols, credential replay against MFA-incapable endpoints
ClickOps Implementation
Step 1: Create Block Legacy Auth Policy
- Navigate to: Protection → Conditional Access → Policies
- Click + New policy
- Configure:
- Name: Block legacy authentication
- Users: All users (exclude emergency accounts)
- Cloud apps: All cloud apps
- Conditions → Client apps: Select “Exchange ActiveSync clients” and “Other clients”
- Grant: Block access
- Enable policy: On
- Click Create
Time to Complete: ~15 minutes
Code Implementation
Code Pack: Terraform
# Conditional Access policy to block legacy authentication protocols
# (Basic Auth, POP, IMAP, SMTP AUTH) that cannot enforce MFA
resource "azuread_conditional_access_policy" "block_legacy_auth" {
display_name = "HTH: Block legacy authentication"
state = var.legacy_auth_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["exchangeActiveSync", "other"]
}
grant_controls {
operator = "OR"
built_in_controls = ["block"]
}
}
Code Pack: CLI Script
# Create policy to block legacy auth
$params = @{
displayName = "Block legacy authentication"
state = "enabled"
conditions = @{
users = @{
includeUsers = @("All")
excludeUsers = @("EMERGENCY_ACCOUNT_1_ID", "EMERGENCY_ACCOUNT_2_ID")
}
applications = @{
includeApplications = @("All")
}
clientAppTypes = @("exchangeActiveSync", "other")
}
grantControls = @{
operator = "OR"
builtInControls = @("block")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
2.2 Require MFA for All Users
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.3 |
| NIST 800-53 | IA-2(1) |
Tenant requirement: the requirement itself, its rationale, and Microsoft’s mandatory-MFA enforcement phases are Microsoft 365 §1.1. This control is the baseline policy object; the factors it will accept come from 1.1.
Description
Author the baseline Conditional Access policy that requires multifactor authentication for all users against all cloud applications. This is the tenant floor that every narrower policy — admin roles, risk conditions, device compliance — sits on top of.
Rationale
Why This Matters:
- Microsoft’s platform-level mandatory MFA covers the admin portals and Azure Resource Manager, not every cloud application your users sign in to — this policy covers the rest
- Scoping to All cloud apps rather than an application list removes the weakest-link app as an entry point, and means a newly onboarded SaaS integration is covered on day one
- Authoring it as an explicit policy (rather than relying on Security Defaults or the Microsoft-managed equivalent) is what lets you exclude the emergency access group deliberately and keep the exclusion auditable
Attack Prevented: Password spray, credential stuffing, phishing, account takeover against non-portal applications
ClickOps Implementation
Step 1: Create MFA Policy
- Navigate to: Protection → Conditional Access → Policies
- Click + New policy
- Configure:
- Name: Require MFA for all users
- Users: All users (exclude emergency accounts)
- Cloud apps: All cloud apps
- Conditions: None (any condition)
- Grant: Require multifactor authentication
- Enable policy: On
- Click Create
Code Implementation
Code Pack: Terraform
# Conditional Access policy requiring MFA for all interactive sign-ins
resource "azuread_conditional_access_policy" "require_mfa_all_users" {
display_name = "HTH: Require MFA for all users"
state = var.mfa_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
}
grant_controls {
operator = "OR"
built_in_controls = ["mfa"]
}
}
Validation & Testing
- Sign in as a standard user to a non-portal cloud application and confirm the MFA prompt appears
- Open the sign-in log entry → Conditional Access tab and confirm this policy is listed as applied, not “Not applied”
- Confirm the emergency access group is excluded (1.2) and that a member of it sees no applied policy
Expected result: Every interactive sign-in to every cloud app satisfies MFA, with the emergency access group as the only exclusion
2.3 Require Compliant Devices for Admins
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.1, 6.4 |
| NIST 800-53 | AC-2(11), AC-6(1) |
Description
Require privileged users to reach the admin portals, Microsoft Graph, and the Intune service only from Intune-compliant or Microsoft Entra hybrid joined devices, with a phishing-resistant authentication strength and a short sign-in frequency. This is the single admin-plane Conditional Access policy for the platform — it governs Entra, Microsoft 365, and Intune administration alike.
Rationale
Why This Matters:
- Admin credentials used from unmanaged or personal devices expose the tenant to malware, keyloggers, and token theft
- Restricting admin access to managed, compliant devices ensures endpoint controls (disk encryption, EDR, patch state) are enforced before any privileged action
- A stolen admin password is useless to an attacker without an enrolled, compliant device to sign in from
- In the March 2026 Stryker attack, the intruders authenticated to the endpoint-management plane from an unmanaged device outside the corporate network — a compliant-device requirement on Microsoft Intune and Microsoft Graph would have blocked the sign-in that issued 200,000+ device wipes (Microsoft Intune guide)
Attack Prevented: Token theft, credential replay from untrusted endpoints, privilege escalation via compromised personal devices, admin-plane abuse from adversary infrastructure
Do not treat this policy as redundant with tenant-level mandatory MFA. Microsoft’s mandatory-MFA enforcement is scoped to the admin portals and to Azure Resource Manager (
https://management.azure.com/); Microsoft states that Microsoft Graph APIs are generally not in scope (mandatory MFA FAQ). Since virtually all administrative automation and every scripted admin action runs through Graph, the Microsoft Graph target below is the only thing enforcing compliant-device and phishing-resistant MFA on that surface. Removing it opens an unauthenticated-by-strength path to the same destructive actions.
Prerequisites
- Microsoft Entra ID P1 or P2 license
- Device compliance policies configured in Intune, and admin workstations already enrolled and compliant — enabling this policy before that is how you lock yourself out
- The “Phishing-Resistant MFA” authentication strength from 1.1
ClickOps Implementation
Step 1: Create the Admin-Plane Policy
- Navigate to: Protection → Conditional Access → Policies → + New policy
- Name:
HTH-AdminPortal-ComplianceRequired - Assignments:
- Users: Include Directory roles → Global Administrator, Security Administrator, Intune Administrator, Helpdesk Administrator, and any other role that can change tenant or device state
- Users → Exclude: the emergency access group (1.2)
- Target resources: Microsoft Admin Portals, Microsoft Intune, Microsoft Graph
- Grant: Require device to be marked as compliant (or Microsoft Entra hybrid joined) AND require authentication strength → Phishing-Resistant MFA
- Session: Sign-in frequency → 1 hour, so a stolen session token expires in an hour rather than a day
- Enable in Report-only first, confirm your own admin devices satisfy it, then set to On
Time to Complete: ~30 minutes plus a report-only observation window
Code Implementation
Code Pack: Terraform
# Conditional Access policy requiring compliant or Hybrid Azure AD joined
# devices for all privileged admin access to Microsoft admin portals
resource "azuread_conditional_access_policy" "require_compliant_device_admins" {
count = var.profile_level >= 2 ? 1 : 0
display_name = "HTH: Require compliant device for admins"
state = "enabled"
conditions {
users {
included_roles = length(var.admin_role_ids) > 0 ? var.admin_role_ids : [
# Default: target common privileged roles
data.azuread_directory_role.global_admin.template_id,
]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
}
grant_controls {
operator = "OR"
built_in_controls = ["compliantDevice", "domainJoinedDevice"]
}
}
Code Pack: CLI Script
# --- Create CA policy requiring compliant device for admin portal access ---
Write-Host "=== Creating CA Policy: Compliant Device for Admin Portals ===" -ForegroundColor Cyan
$intuneAdminRoleId = "3a2c62db-5318-420d-8d74-23affee5d9d5"
$globalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"
$securityAdminRoleId = "194ae4cb-b126-40b2-bd5b-6091b380977d"
# Microsoft Admin Portals service principal
$adminPortalsAppId = "c44b4083-3bb0-49c1-b47d-974e53cbdf3c"
$intuneAppId = "0000000a-0000-0000-c000-000000000000"
$graphAppId = "00000003-0000-0000-c000-000000000000"
$phishResistantStrengthId = "00000000-0000-0000-0000-000000000004"
$compliancePolicy = @{
DisplayName = "HTH-AdminPortal-ComplianceRequired"
State = "enabled"
Conditions = @{
Users = @{
IncludeRoles = @($intuneAdminRoleId, $globalAdminRoleId, $securityAdminRoleId)
}
Applications = @{
IncludeApplications = @($adminPortalsAppId, $intuneAppId, $graphAppId)
}
ClientAppTypes = @("browser", "mobileAppsAndDesktopClients")
}
GrantControls = @{
Operator = "AND"
BuiltInControls = @("compliantDevice")
AuthenticationStrength = @{
Id = $phishResistantStrengthId
}
}
SessionControls = @{
SignInFrequency = @{
Value = 1
Type = "hours"
IsEnabled = $true
}
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $compliancePolicy
Write-Host "Created CA policy: HTH-AdminPortal-ComplianceRequired" -ForegroundColor Green
# Legacy authentication is blocked tenant-wide in control 2.1 -- no admin-scoped
# duplicate policy is created here.
Validation & Testing
- Attempt an admin sign-in from a non-compliant device — it should be blocked
- Attempt a scripted Graph call (
Connect-MgGraph) as an admin from a non-compliant device — it should also be blocked, which is the point of including Graph as a target - Confirm admin sessions re-prompt after one hour
- Confirm the emergency access group is excluded and can still sign in
Expected result: Administrative access to the portals, Intune, and Graph requires a compliant device and a phishing-resistant factor; sessions expire hourly
2.4 Block High-Risk Sign-Ins
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 6.4 |
| NIST 800-53 | SI-4 |
Description
Use Entra ID Protection to automatically block sign-ins classified as high risk based on machine learning detection of suspicious patterns.
Rationale
Why This Matters:
- High-risk sign-ins reflect signals like leaked credentials, anonymous IP usage, and impossible-travel patterns that indicate active compromise
- Automated, real-time blocking responds faster than human analysts can triage and act on alerts
- Risk-based policies adapt to evolving attacker behavior without constant manual rule maintenance
Attack Prevented: Account takeover, credential-based intrusion, anomalous sign-in abuse
Prerequisites
- Microsoft Entra ID P2 license
ClickOps Implementation
Step 1: Create Risk-Based Policy
- Navigate to: Protection → Conditional Access → Policies
- Click + New policy
- Configure:
- Name: Block high-risk sign-ins
- Users: All users (exclude emergency accounts)
- Cloud apps: All cloud apps
- Conditions → Sign-in risk: High
- Grant: Block access
- Enable policy
Step 2: Create Medium-Risk MFA Policy
- Create another policy for medium risk
- Conditions → Sign-in risk: Medium
- Grant: Require MFA + Require password change
Code Implementation
Code Pack: Terraform
# Conditional Access policy to block high-risk sign-ins using
# Entra ID Protection machine learning detection (requires P2 license)
resource "azuread_conditional_access_policy" "block_high_risk_signins" {
count = var.profile_level >= 2 ? 1 : 0
display_name = "HTH: Block high-risk sign-ins"
state = var.high_risk_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
sign_in_risk_levels = ["high"]
}
grant_controls {
operator = "OR"
built_in_controls = ["block"]
}
}
# Conditional Access policy requiring MFA + password change for medium-risk sign-ins
resource "azuread_conditional_access_policy" "remediate_medium_risk_signins" {
count = var.profile_level >= 2 ? 1 : 0
display_name = "HTH: Require MFA for medium-risk sign-ins"
state = var.high_risk_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
sign_in_risk_levels = ["medium"]
}
grant_controls {
operator = "AND"
built_in_controls = ["mfa", "passwordChange"]
}
}
2.5 Review Microsoft-Managed Conditional Access Policies and Retire Per-User MFA
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 6.3, 4.1 |
| NIST 800-53 | IA-2(1), CM-2 |
Description
Review the Conditional Access policies Microsoft now auto-creates in every eligible tenant (shown as “Created by: Microsoft”), exclude your emergency access accounts from them before they auto-enable, and complete the migration of any users still on legacy per-user MFA — which Microsoft explicitly no longer recommends — onto Conditional Access.
Rationale
Why This Matters:
- Microsoft deploys these policies in report-only mode and enables them automatically after 45 days unless an admin acts — an unreviewed auto-enable can lock out break-glass accounts that were never excluded
- The managed set (“Block legacy authentication”, “Block device code flow”, “MFA for admins accessing Microsoft Admin portals”, “MFA for all users”, “MFA for per-user MFA users”) overlaps your custom policies; reconciling them prevents conflicting or redundant enforcement
- Per-user MFA is a deprecated mechanism with no conditional logic; leaving users on it fragments your MFA posture
Attack Prevented: Admin lockout from unreviewed auto-enabled policy, MFA-coverage gaps from deprecated per-user MFA
ClickOps Implementation
Step 1: Review Microsoft-Managed Policies
- Navigate to: Protection → Conditional Access → Policies
- Filter or look for policies labeled Created by: Microsoft
- For each, open and review scope and state (Report-only vs On)
Step 2: Exclude Emergency Access Accounts
- Edit each Microsoft-managed policy
- Under Users → Exclude, add your break-glass accounts
- Save
Step 3: Complete Per-User MFA Migration
- Confirm the “MFA for per-user MFA users” managed policy is in effect, or create an equivalent Conditional Access policy
- Disable per-user MFA for migrated users (legacy per-user MFA portal)
Time to Complete: ~30 minutes
Validation & Testing
- Every “Created by: Microsoft” policy has emergency accounts excluded
- No users remain in per-user MFA “Enforced”/”Enabled” state
- Managed policies are either enabled deliberately or replaced by equivalent custom policies
Expected result: Managed policies are consciously adopted, break-glass access preserved, per-user MFA retired. (Microsoft-managed policies)
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access security |
| NIST 800-53 | CM-2 | Baseline configuration |
| CISA SCuBA (Entra ID) | MS.AAD | Conditional Access baseline policies |
2.6 Block Device Code Flow
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.8 |
| NIST 800-53 | AC-3, IA-2 |
Description
Block the OAuth device code flow tenant-wide via a Conditional Access authentication-flows policy unless a specific, inventoried device-login use case (conference-room hardware, input-constrained devices) requires it — in which case scope an exception to those accounts only.
Rationale
Why This Matters:
- Device code phishing (tracked by Microsoft as Storm-2372, active since 2025) tricks users into entering an attacker-generated device code at a legitimate Microsoft URL, handing the attacker access and refresh tokens without ever capturing a password
- The stolen token satisfies MFA — this attack class bypasses MFA entirely, making preventive blocking the only strong control
-
Microsoft now ships “Block device code flow” as an auto-enabling Microsoft-managed policy; adopting it deliberately (with your exceptions) beats waiting for auto-enable
- Microsoft classifies device code flow as “a high-risk authentication method that can be part of a phishing attack or used to access corporate resources on unmanaged devices” and recommends blocking it wherever possible (Authentication flows as a condition in Conditional Access policy)
- It sidesteps device-compliance expectations too, because the token lands on whatever machine started the flow rather than the machine the administrator is sitting at — which is why blocking it closes a gap that 2.3 alone does not
Attack Prevented: Device code phishing (Storm-2372), MFA-bypassing token theft, token acquisition on unmanaged adversary infrastructure
Prerequisites
- Microsoft Entra ID P1 or P2 license
- Sign-in log review to identify legitimate device code flow usage before enforcing (shared devices, conference-room hardware, headless enrollment)
ClickOps Implementation
Step 1: Inventory Existing Device Code Flow Usage
- Navigate to: Microsoft Entra admin center → Monitoring & health → Sign-in logs
- Add the Authentication protocol filter → select Device code
- Record which users, devices, and resources legitimately depend on the flow — conference-room and digital-signage devices are the common genuine cases
Step 2: Create the Authentication-Flows Policy in Report-Only Mode
- Navigate to: Protection → Conditional Access → Policies → + New policy
- Configure:
- Name: Block device code flow
- Users: All users (exclude the emergency access group and any inventoried device-login service accounts)
- Target resources: All resources (or scope to the resources your admins touch)
- Conditions → Authentication flows → set Configure to Yes → check Device code flow
- Grant: Block access
- Enable policy: Report-only, and leave it there long enough to cover a normal work cycle
Step 3: Exempt Device Registration Service If You Target All Resources
- If the policy targets All resources and your organization uses device code flow for device registration, exempt that service or enrollment will break
- Navigate to the policy → Target resources → Exclude → Select excluded cloud apps → Device Registration Service
- Via API, exclude client ID
01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9 - Confirm the dependency first by filtering sign-in logs on that resource ID with the Device code authentication protocol
Step 4: Enforce
- After report-only shows no legitimate usage beyond your exclusions, set the policy to On
Protocol tracking — the non-obvious side effect. Once a session uses device code flow, Microsoft Entra marks it protocol tracked, and that state persists through subsequent token refreshes. A later sign-in in the same session that used a completely different authentication flow can still be blocked by this policy. Expect error
AADSTS530036on refresh tokens invalidated this way, and expect that possible impact includes full device sign-out. Microsoft documents this as expected behavior with no remediation while the policy isenabled— which is exactly why Step 2 runs report-only first.
Time to Complete: ~30 minutes (plus report-only observation period)
Validation & Testing
- Attempt a device-code sign-in (
az login --use-device-code) as a standard user — it should be blocked - In Sign-in logs, open the blocked event → Conditional Access tab → confirm this policy is the one that applied
- Confirm device enrollment still succeeds if you configured the Device Registration Service exclusion
- Check the Original transfer method property in Activity details on any unexpected block to confirm whether protocol tracking, rather than a live device code sign-in, caused it
Expected result: Device code flow blocked tenant-wide except for scoped exceptions; legitimate device-registration flows continue to work. (Microsoft Storm-2372 advisory)
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access security |
| NIST 800-53 | AC-3 | Access enforcement |
| CISA SCuBA (Entra ID) | MS.AAD | Risk-based Conditional Access |
3. Privileged Identity Management
3.1 Enable Just-In-Time Access for Admin Roles
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 5.4, 6.8 |
| NIST 800-53 | AC-2(7), AC-6(1) |
Tenant requirement: the decision to run a PIM program and convert standing admin to eligible is Microsoft 365 §1.3. This control is the per-role settings work in Identity Governance; the Intune role set and its destructive-action profile are in Microsoft Intune §3.1.
Description
Configure the PIM role settings themselves — activation duration, MFA and justification requirements, approval chain, and eligibility expiry — and convert permanent assignments to eligible. The tenant decision is “no standing admin”; this control is the set of per-role knobs that decides how much friction an attacker who holds a valid admin credential actually meets.
Rationale
Why This Matters:
- PIM is only as strong as its role settings: an eligible assignment that activates in one click with no MFA, no justification, and an 8-hour window barely slows a credential thief down
- Approval-on-activation for the highest-privilege roles turns privilege escalation into an event a second human sees in real time, which is the detection opportunity standing privilege never gives you
- Eligibility expiry forces re-justification on a clock, so the eligible-assignment list does not quietly become the old permanent-admin list
Attack Prevented: Privilege persistence, lateral movement, insider threats
Real-World Incidents:
- Midnight Blizzard: Time-limited OAuth permissions would have reduced attack duration
Prerequisites
- Microsoft Entra ID P2 license
- Global Administrator or Privileged Role Administrator
ClickOps Implementation
Step 1: Access PIM
- Navigate to: Microsoft Entra admin center → Identity governance → Privileged Identity Management
- Click Microsoft Entra roles
Step 2: Configure Role Settings
- Click Settings → Roles
- Select Global Administrator
- Click Edit
- Configure:
- Activation maximum duration: 2 hours (or 8 hours max)
- On activation, require: MFA
- Require justification on activation: Yes
- Require ticket information: Optional
- Require approval to activate: Yes (for highest privilege roles)
- Approvers: Security team members
- Click Update
- Repeat for other privileged roles (Security Admin, Exchange Admin, etc.)
Step 3: Convert Permanent to Eligible
- Navigate to Assignments → Eligible assignments
- For each permanent Global Admin:
- Click Update
- Change assignment type to Eligible
- Set eligibility period (e.g., 1 year with renewal)
- Keep only emergency accounts as permanent
Step 4: Configure Activation Requirements
- In role settings, configure:
- Maximum activation duration
- MFA requirement
- Approval workflow
Time to Complete: ~1-2 hours
Code Implementation
Code Pack: Terraform
# Create PIM eligible assignments for Global Administrator role.
# Eliminates standing admin privileges by requiring just-in-time activation
# with MFA, justification, and optional approval.
#
# NOTE: Full PIM role settings (activation duration, approval workflow,
# MFA on activation) require Microsoft Graph API or the admin center.
# Terraform manages eligible assignments; configure role settings via
# the Entra admin center or PowerShell.
resource "azuread_directory_role_eligibility_schedule_request" "pim_global_admin" {
count = var.profile_level >= 2 ? length(var.pim_eligible_user_ids) : 0
role_definition_id = data.azuread_directory_role.global_admin.template_id
principal_id = var.pim_eligible_user_ids[count.index]
directory_scope_id = "/"
justification = "HTH: PIM eligible assignment for Just-In-Time access"
schedule_info {
expiration {
duration = "P${var.pim_eligibility_duration_days}D"
type = "afterDuration"
}
}
}
Code Pack: CLI Script
# Connect with PIM permissions
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory", "RoleEligibilitySchedule.ReadWrite.Directory"
# Get role definitions
$globalAdminRole = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq 'Global Administrator'"
# Create eligible assignment (convert permanent to eligible)
$params = @{
action = "adminAssign"
justification = "Converting to PIM eligible assignment"
roleDefinitionId = $globalAdminRole.Id
directoryScopeId = "/"
principalId = "USER_OBJECT_ID"
scheduleInfo = @{
startDateTime = (Get-Date).ToUniversalTime().ToString("o")
expiration = @{
type = "afterDuration"
duration = "P365D" # 1 year eligibility
}
}
}
New-MgRoleManagementDirectoryRoleEligibilityScheduleRequest -BodyParameter $params
# Configure role settings (requires beta endpoint)
# Use Microsoft Entra admin center for full settings configuration
Validation & Testing
How to verify the control is working:
- Verify no permanent Global Admin assignments (except emergency accounts)
- Test PIM activation as eligible admin
- Confirm MFA required on activation
- Verify justification is captured in audit log
- Check activation expires after configured duration
Expected result: Admins activate roles on-demand, access expires automatically
3.2 Configure Access Reviews
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 5.1, 5.3 |
| NIST 800-53 | AC-2(3) |
Description
Enable recurring access reviews for privileged roles and group memberships to ensure continued business need for access.
Rationale
Why This Matters:
- Access tends to accumulate over time as employees change roles, leaving users with privileges they no longer need
- Recurring reviews force periodic re-justification, shrinking the standing attack surface of privileged accounts
- Removing stale privileged and group memberships limits what a compromised account can reach
Attack Prevented: Privilege creep, orphaned-access abuse, insider misuse of stale permissions
ClickOps Implementation
Step 1: Create Access Review
- Navigate to: Identity governance → Access reviews
- Click + New access review
- Configure:
- Review type: Teams + Groups or Azure AD roles
- Scope: Global Administrator (and other privileged roles)
- Reviewers: Manager or Self-review
- Recurrence: Monthly or Quarterly
- Upon completion: Remove access for denied users
- Start review
3.3 Protect High-Value Accounts with Restricted Management Administrative Units
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 5.4, 6.8 |
| NIST 800-53 | AC-6(1), AC-2(11) |
Description
Place executive, break-glass, and other high-value accounts (and sensitive groups) into a Restricted Management Administrative Unit so that only role assignments scoped to that AU can modify them — tenant-wide role holders, including Helpdesk Administrators and even unscoped Global Administrator role assignments, cannot reset their passwords or change their MFA methods.
Rationale
Why This Matters:
- Tenant-wide helpdesk and user-management roles can reset any user’s password or MFA by default — a socially engineered helpdesk agent becomes a path to your CEO’s mailbox
- PIM and access reviews govern who HOLDS roles; Restricted Management AUs govern which ACCOUNTS those roles can touch — a complementary blast-radius control
- Scoping modification rights to a small, named set of admins makes targeted account-takeover materially harder
Attack Prevented: Helpdesk social engineering against executives, privileged-account takeover via tenant-wide role abuse
Prerequisites
- Microsoft Entra ID P1 license
- Inventory of high-value accounts (executives, break-glass, service-critical)
ClickOps Implementation
Step 1: Create the Restricted Management AU
- Navigate to: Microsoft Entra admin center → Identity → Roles & admins → Admin units → + Add
- Name it (e.g., “Protected Accounts”)
- Set Restricted management administrative unit to Yes
- Create, then add the high-value users/groups as members
Step 2: Scope Management Rights
- In the AU, open Roles and administrators
- Assign the minimum roles (e.g., Privileged Authentication Administrator) scoped to this AU, to a small named set of admins
Time to Complete: ~30 minutes
Validation & Testing
- As a tenant-wide Helpdesk Administrator, attempt a password reset on a protected account — it must fail
- Confirm the AU-scoped admin can perform the same reset
- Review AU membership quarterly alongside access reviews (3.2)
Expected result: Only AU-scoped role holders can modify protected accounts. (Restricted management AUs)
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.3 | Access modification restriction |
| NIST 800-53 | AC-6(1) | Least privilege — authorize access |
4. Application Security
4.1 Restrict User Consent to Applications
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 2.5 |
| NIST 800-53 | AC-3, CM-7 |
Tenant requirement: the consent posture, the admin approval workflow, and blocking user app registration are Microsoft 365 §3.1. This control is the Entra authorization-policy object that carries the setting.
Description
Set the tenant authorization policy so the default user role holds no permission-grant policy — the object-level expression of “users cannot consent to applications.” The setting defaults to permissive in every new tenant, and it is the single Graph object an attacker’s consent-phishing lure depends on.
Rationale
Why This Matters:
- Consent phishing needs no credential theft and no MFA bypass: the victim authenticates legitimately and authorizes the attacker’s application, and the resulting token survives a password reset
- The authorization policy is tenant-wide and takes effect immediately, so it is the fastest containment lever during an active consent-phishing campaign
- Because it is a single Graph object, it is also easy to regress silently — audit it rather than assuming it stayed set
Attack Prevented: OAuth consent phishing, malicious app installation
Real-World Incidents:
- Midnight Blizzard: Leveraged malicious OAuth applications with full_access_as_app to access mailboxes
ClickOps Implementation
Step 1: Disable User Consent
- Navigate to: Applications → Enterprise applications → Consent and permissions
- Click User consent settings
- Select Do not allow user consent
- Click Save
Step 2: Configure Admin Consent Workflow
- Click Admin consent settings
- Enable Users can request admin consent to apps they are unable to consent to
- Add reviewers (Security team members)
- Configure notification settings
- Click Save
Blocking user app registration is a separate setting on a separate blade and is covered in Microsoft 365 §3.1 — restricting consent does not stop a user from registering application objects.
Time to Complete: ~15 minutes
Code Implementation
Code Pack: CLI Script
# Disable user consent
$params = @{
defaultUserRolePermissions = @{
permissionGrantPoliciesAssigned = @()
}
}
Update-MgPolicyAuthorizationPolicy -BodyParameter $params
# Note: Configure admin consent workflow through admin center
4.2 Review and Restrict Application Permissions
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 2.6 |
| NIST 800-53 | AC-6 |
Tenant requirement: the recurring review of enterprise application permissions and the revocation workflow are Microsoft 365 §3.2. This control is the Entra-side enumeration — the service principal and app registration inventory the review runs against.
Description
Enumerate service principals and app registrations and flag the Graph permissions that grant tenant-wide reach. Terraform data sources make the inventory reproducible; the judgment call on each grant is the review in the hub control.
Rationale
Why This Matters:
- OAuth applications holding broad Graph permissions can read mail, files, and directory data across the entire tenant, and their tokens are unaffected by password resets or MFA
- App registrations and service principals accumulate from forgotten pilots and departed vendors — nothing expires them, so only enumeration finds them
- The permissions that matter are a short, known list (
Mail.ReadWrite,Directory.ReadWrite.All,RoleManagement.ReadWrite.Directory,full_access_as_app), which makes the inventory automatable even though the remediation is not
Attack Prevented: OAuth application abuse, data exfiltration via excessive app permissions, persistent backdoor access
ClickOps Implementation
Step 1: Audit Applications
- Navigate to: Applications → App registrations → All applications
- For each app, click API permissions
- Flag apps with dangerous permissions:
Mail.ReadWrite- Read/write all mailFiles.ReadWrite.All- Access all filesDirectory.ReadWrite.All- Modify directoryApplication.ReadWrite.All- Manage appsRoleManagement.ReadWrite.Directory- Manage roles
Step 2: Remove Unnecessary Permissions
- For flagged apps, review business justification
- Remove permissions not required for functionality
- Or delete unused applications entirely
Code Implementation
Code Pack: Terraform
# Data source to enumerate all service principals (enterprise applications)
# for permission auditing. Use this to identify apps with dangerous permissions
# such as Mail.ReadWrite, Directory.ReadWrite.All, full_access_as_app.
#
# NOTE: Terraform is not the ideal tool for ongoing permission auditing.
# This control provides data sources for initial discovery; recurring
# audits should use PowerShell scripts or SSPM tooling.
# Retrieve all service principals for audit
data "azuread_service_principals" "all_apps" {
count = var.profile_level >= 2 ? 1 : 0
return_all = true
}
# Identify high-risk Microsoft Graph permissions for audit output
locals {
high_risk_permissions = var.profile_level >= 2 ? [
"Mail.ReadWrite",
"Mail.ReadWrite.All",
"Files.ReadWrite.All",
"Directory.ReadWrite.All",
"Application.ReadWrite.All",
"RoleManagement.ReadWrite.Directory",
"full_access_as_app",
] : []
app_permission_audit = var.profile_level >= 2 ? {
status = "AUDIT_DATA_AVAILABLE"
total_service_principals = length(try(data.azuread_service_principals.all_apps[0].service_principals, []))
high_risk_permissions = local.high_risk_permissions
instructions = "Review each app's API permissions in Entra admin center: Applications > App registrations > [App] > API permissions"
remediation = "Remove unnecessary permissions or delete unused applications"
} : null
}
4.3 Retire Azure AD Graph API Usage
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 2.2, 16.13 |
| NIST 800-53 | SA-22, SI-2 |
Description
Inventory and migrate any application, script, or automation still calling the deprecated Azure AD Graph API (graph.windows.net) to Microsoft Graph, and add detection for anomalous actor attribution in audit logs.
Rationale
Why This Matters:
- CVE-2025-55241 (CVSS 10.0) chained undocumented internal “Actor tokens” with a tenant-validation failure in the legacy Azure AD Graph API to let an attacker impersonate any user — including Global Admins — in any tenant, leaving no sign-in log trail; Microsoft patched the validation flaw (July 17, 2025) and blocked apps from requesting Actor tokens against Azure AD Graph (August 6, 2025)
- Azure AD Graph is deprecated; anything still calling it is riding an unmaintained, incident-prone surface
- The attack’s audit signature — a service display name (e.g., Exchange, SharePoint) paired with a user UPN as actor — is detectable in audit logs
Attack Prevented: Cross-tenant impersonation via legacy-API token abuse, silent Global Admin takeover
ClickOps Implementation
Step 1: Inventory Azure AD Graph Callers
- Navigate to: Monitoring → Sign-in logs → Service principal sign-ins
- Filter by Resource = Windows Azure Active Directory (
graph.windows.net) - List every application still calling the legacy API
Step 2: Migrate to Microsoft Graph
- For each caller, migrate API calls to Microsoft Graph (
graph.microsoft.com) equivalents - Remove Azure AD Graph permissions from the app registrations once migrated
Step 3: Add Actor-Mismatch Detection
- In your SIEM (see 5.1), alert on directory audit events where the initiating actor pairs a first-party service display name (Exchange, SharePoint) with a user UPN — the CVE-2025-55241 signature
Time to Complete: ~1-2 hours (plus migration effort per app)
Validation & Testing
- Service-principal sign-in logs show zero
graph.windows.netcalls from your own apps - Detection rule fires on simulated mismatched-actor audit events
Expected result: No first-party dependence on Azure AD Graph; actor-mismatch detection live. (CVE-2025-55241 research)
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.1 | Vulnerability management |
| NIST 800-53 | SA-22 | Unsupported system components |
5. Monitoring & Detection
5.1 Export Entra ID Sign-In and Audit Logs
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 8.2 |
| NIST 800-53 | AU-2, AU-3, AU-6 |
Tenant requirement: the Microsoft 365 unified audit log — workload activity across Exchange, SharePoint, Teams, and Purview — is Microsoft 365 §5.1. Intune’s audit surface is Microsoft Intune §7.1. This control covers the identity plane, which none of the others contain.
Description
Configure Entra ID diagnostic settings to export the identity log categories — SignInLogs, AuditLogs, NonInteractiveUserSignInLogs, and ServicePrincipalSignInLogs — to a Log Analytics workspace, Event Hub, or storage account. These logs are not part of the Microsoft 365 unified audit log and are not retained past the Entra default without this export.
Rationale
Why This Matters:
- Sign-in telemetry is the only record of how an account authenticated — which Conditional Access policies applied, which factor satisfied MFA, which IP and device the session came from — and it lives only in this plane
ServicePrincipalSignInLogsandNonInteractiveUserSignInLogsare off by default in many export configurations, and they are exactly where OAuth-application and token-replay abuse shows up rather than in interactive sign-ins- Entra’s built-in retention is short (7-30 days depending on license); an intrusion discovered a quarter later is investigated entirely from what was exported at the time
Attack Prevented: Undetected intrusion, delayed breach discovery, audit-trail gaps in the identity plane
ClickOps Implementation
Step 1: Configure Diagnostic Settings
- Navigate to: Monitoring → Diagnostic settings
- Click + Add diagnostic setting
- Configure:
- Name: Send to Log Analytics (or SIEM)
- Logs: SignInLogs, AuditLogs, NonInteractiveUserSignInLogs, ServicePrincipalSignInLogs
- Destination: Log Analytics workspace / Event Hub / Storage Account
- Click Save
Step 2: Create Alert Rules
- Navigate to: Monitoring → Alerts
- Create alerts for:
- Global Admin role assignment
- Conditional Access policy changes
- New OAuth app registration
- Risky sign-in detected
Code Implementation
Code Pack: Terraform
# Configure diagnostic settings to export Entra ID sign-in and audit logs.
#
# NOTE: Diagnostic settings for Entra ID require the Azure Monitor provider
# (azurerm), not the AzureAD provider. This control documents the configuration
# and provides a reference implementation. If you are also managing Azure
# resources, add the azurerm provider to providers.tf and uncomment below.
#
# Uncomment and configure when using the azurerm provider:
#
# resource "azurerm_monitor_aad_diagnostic_setting" "entra_id_logs" {
# name = "hth-entra-id-logging"
# log_analytics_workspace_id = var.log_analytics_workspace_id
#
# enabled_log {
# category = "SignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "AuditLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "NonInteractiveUserSignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "ServicePrincipalSignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "ManagedIdentitySignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "RiskyUsers"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "UserRiskEvents"
# retention_policy {
# enabled = true
# days = 90
# }
# }
# }
locals {
logging_config = {
status = var.log_analytics_workspace_id != "" ? "REQUIRES_AZURERM_PROVIDER" : "NOT_CONFIGURED"
workspace_id = var.log_analytics_workspace_id
recommended_log_categories = [
"SignInLogs",
"AuditLogs",
"NonInteractiveUserSignInLogs",
"ServicePrincipalSignInLogs",
"ManagedIdentitySignInLogs",
"RiskyUsers",
"UserRiskEvents",
]
recommended_retention_days = 90
instructions = var.log_analytics_workspace_id != "" ? "Add azurerm provider and uncomment the diagnostic setting resource" : "Set log_analytics_workspace_id variable and add azurerm provider"
alert_rules = [
"Global Admin role assignment",
"Conditional Access policy changes",
"New OAuth app registration",
"Risky sign-in detected",
"Emergency account sign-in",
]
}
}
5.2 Monitor Identity Secure Score
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.1 |
| NIST 800-53 | CA-7 |
Description
Regularly review Identity Secure Score to track security posture and identify improvement opportunities.
Rationale
Why This Matters:
- Identity Secure Score surfaces concrete, prioritized gaps in MFA, legacy authentication, and privileged access configuration
- Tracking the score over time catches configuration drift and regressions before attackers can exploit them
- It translates Microsoft’s evolving identity best practices into actionable, measurable improvements
Attack Prevented: Misconfiguration drift, unaddressed identity weaknesses, security-posture regression
ClickOps Implementation
- Navigate to: Protection → Identity Secure Score
- Review current score and recommendations
- Target score above 70%
- Implement high-impact recommendations:
- Enable MFA for all users
- Block legacy authentication
- Enable risk policies
- Use PIM for admin roles
5.3 Key Events to Monitor
| Event | Log Source | Detection Use Case |
|---|---|---|
Add member to role |
Audit | Privilege escalation |
Update conditional access policy |
Audit | Security control bypass |
Consent to application |
Audit | Malicious app installation |
User risk detected |
Sign-in | Account compromise |
Sign-in from anonymous IP |
Sign-in | Suspicious access |
Impossible travel |
Sign-in | Credential theft |
KQL Queries for Azure Sentinel
Code Pack: SIEM Queries
// Privileged role assignments
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add member to role"
| where TargetResources[0].modifiedProperties[0].newValue contains "Global Administrator"
| project TimeGenerated, InitiatedBy.user.userPrincipalName, TargetResources[0].userPrincipalName
// Conditional Access policy changes
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName has_any ("Add policy", "Update policy", "Delete policy")
| where TargetResources[0].type == "Policy"
| project TimeGenerated, InitiatedBy.user.userPrincipalName, OperationName, TargetResources[0].displayName
// High-risk sign-ins
SigninLogs
| where TimeGenerated > ago(24h)
| where RiskLevelDuringSignIn == "high"
| project TimeGenerated, UserPrincipalName, IPAddress, Location, RiskDetail
6. Third-Party Integration Security
6.1 Integration Risk Assessment
| Risk Factor | Low | Medium | High |
|---|---|---|---|
| Data Access | Directory read-only | User profile + groups | Mail, files, directory write |
| OAuth Scopes | User.Read | User.ReadWrite, Group.Read | Mail.ReadWrite, Application.ReadWrite.All |
| Token Duration | Short-lived (1 hour) | Refresh tokens (90 days) | Long-lived service principal |
| Vendor Security | SOC 2 Type II + ISO | SOC 2 Type I | No certification |
6.2 Common Integrations
Obsidian Security
Data Access: Read (directory, sign-in logs, audit logs) Recommended Controls:
- ✅ Use dedicated service principal
- ✅ Grant minimum required Graph API permissions
- ✅ Monitor service principal sign-ins
- ✅ Review permissions quarterly
7. Compliance Quick Reference
SOC 2 Trust Services Criteria Mapping
| Control ID | Entra ID Control | Guide Section |
|---|---|---|
| CC6.1 | Authentication methods & strengths | 1.1 |
| CC6.1 | Block legacy auth | 2.1 |
| CC6.2 | Privileged Identity Management | 3.1 |
| CC6.3 | Application consent controls | 4.1 |
| CC7.2 | Identity log export | 5.1 |
NIST 800-53 Rev 5 Mapping
| Control | Entra ID Control | Guide Section |
|---|---|---|
| IA-2(1) | Baseline MFA Conditional Access policy | 2.2 |
| IA-2(6) | Phishing-resistant authentication strength | 1.1 |
| AC-2(7) | Privileged account management | 3.1 |
| AC-2(3) | Access reviews | 3.2 |
| AU-2 | Identity log export | 5.1 |
CIS Microsoft 365 Foundations Benchmark Mapping
Benchmark note: Entra ID identity controls (MFA, legacy authentication, Conditional Access, consent, PIM) live in the CIS Microsoft 365 Foundations Benchmark — not the CIS Azure Foundations Benchmark this guide previously cited. CIS M365 Foundations v7.0.0 (May 2026) added 21 controls and rehomed 12 identity recommendations from the Azure benchmark; consult the current benchmark for exact recommendation IDs, which shift across major versions.
| Benchmark Area | Entra ID Control | Guide Section |
|---|---|---|
| MFA enforcement | Phishing-resistant authentication strength | 1.1 |
| Legacy authentication | Block legacy auth | 2.1 |
| Privileged access | PIM just-in-time roles | 3.1 |
| Emergency access | Break-glass Conditional Access exclusion | 1.2 |
| Application consent | Restrict user consent | 4.1 |
CISA SCuBA Secure Configuration Baseline (Entra ID) Mapping
The CISA SCuBA baseline for Entra ID (assessable with the ScubaGear tool) maps to this guide:
| SCuBA Policy Area | Guide Section |
|---|---|
| Block legacy authentication (MS.AAD.1) | 2.1 |
| Risk-based Conditional Access (MS.AAD.2) | 2.4 |
| Phishing-resistant MFA & secure registration (MS.AAD.3) | 1.1 |
| Centralized logging to SOC (MS.AAD.4) | 5.1 |
| App registration & consent restriction (MS.AAD.5) | 4.1 |
| Highly privileged / just-in-time access (MS.AAD.7) | 3.1 |
Appendix A: License Compatibility
| Control | Free | P1 | P2 | Microsoft 365 E5 |
|---|---|---|---|---|
| Security Defaults | ✅ | ✅ | ✅ | ✅ |
| Conditional Access | ❌ | ✅ | ✅ | ✅ |
| Privileged Identity Management | ❌ | ❌ | ✅ | ✅ |
| Identity Protection (risk policies) | ❌ | ❌ | ✅ | ✅ |
| Access Reviews | ❌ | ❌ | ✅ | ✅ |
| Entitlement Management | ❌ | ❌ | ✅ | ✅ |
Appendix B: References
Official Microsoft Documentation:
- Microsoft Trust Center
- Microsoft Entra ID Product Documentation
- Best Practices to Secure with Microsoft Entra ID
- Require MFA for All Users with Conditional Access
- Plan Conditional Access Deployment
- Conditional Access - Zero Trust Policy Engine
- Privileged Identity Management
API Documentation:
- Microsoft Graph Identity and Network Access Overview
- Microsoft Graph API Reference
- Microsoft Graph PowerShell SDK
Compliance Frameworks:
- SOC 1, SOC 2, SOC 3, ISO 27001, ISO 27017, ISO 27018, ISO 27701, FedRAMP — via Microsoft Service Trust Portal
- Microsoft Entra Identity Standards Overview
Hardening Benchmarks:
- CIS Microsoft 365 Foundations Benchmark — v7.0.0 houses the Entra ID identity controls (rehomed from the Azure Foundations Benchmark in May 2026)
- CISA SCuBA Secure Configuration Baseline for Entra ID — assessable with ScubaGear
- Microsoft-managed Conditional Access policies
- Mandatory MFA for Microsoft admin portals
Security Incidents:
- Midnight Blizzard Attack Guidance (January 2024) — Test account without MFA led to corporate email compromise via password spray
- CVE-2025-55241 — Actor tokens / Azure AD Graph cross-tenant impersonation (CVSS 10.0; see 4.3)
- Storm-2372 device code phishing (see 2.6)
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-08-08 | 0.3.0 | draft | Platform breakout: reframed as a product guide under the Microsoft Common Controls hub (platform frontmatter, hub pointer, scope rewrite). Six duplicated controls slimmed to their Entra admin-surface residue with hub pointers — 1.1 retitled to authentication methods and strengths, 1.2 retitled to the emergency access exclusion group, 2.1, 2.2, 3.1, 4.1, 4.2, and 5.1 retitled to identity log export. Consolidated from the Intune guide: admin-plane Conditional Access including the Microsoft Graph out-of-scope warning (2.3, gained the migrated PowerShell pack) and device code flow report-only staging, Device Registration Service exclusion, and protocol-tracking side effect (2.6) | Claude Code (Opus 4.8) |
| 2026-08-03 | 0.2.0 | draft | Currency update: mandatory-MFA enforcement floor, Microsoft-managed CA policies + per-user MFA retirement (2.5), block device code flow / Storm-2372 (2.6), Restricted Management AUs (3.3), app-registration restriction (4.1), retire Azure AD Graph + CVE-2025-55241 detection (4.3); remap compliance from CIS Azure to CIS M365 Foundations v7.0.0; add CISA SCuBA mapping | Claude Code (Sonnet 5) |
| 2026-06-29 | 0.1.1 | draft | Add cheat-sheet Description and Rationale for all controls | Claude Code (Opus 4.8) |
| 2025-02-05 | 0.1.0 | draft | Initial guide with authentication, Conditional Access, PIM, and monitoring | Claude Code (Opus 4.5) |
Contributing
Found an issue or want to improve this guide?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide